Index: binaries/data/mods/public/maps/scenarios/Roll The Dice Abilities.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/scenarios/Roll The Dice Abilities.js @@ -0,0 +1,40 @@ +// Welcome to the wonderful world of Javascript. +// we are going to add a custom ability for this script. +// The conditions will be RTD-specific. +// The effect wil be calling a trigger function. + +function TriggerAction() +{ + this.name = "TriggerAction"; + + this.schema = + "" + + "" + + ""; +} + +TriggerAction.prototype.SetupData = function(entity, templateData) +{ + return templateData; +} + +TriggerAction.prototype.OnFire = function(entity, templateData, commandData) +{ + let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + if (!cmpTrigger[templateData.TriggerAction.Trigger]) + return; + cmpTrigger[templateData.TriggerAction.Trigger](TriggerHelper.GetOwner(entity)); +} + +TriggerAction.prototype.IsFinished = function(entity, templateData, commandData) +{ + return true; +} + +TriggerAction.prototype.Validate = function(entity, templateData, sendMessages) +{ + return true; +} + +var TriggerAction = new TriggerAction(); +AbilityEffects.RegisterEffect(TriggerAction); Index: binaries/data/mods/public/maps/scenarios/Roll The Dice.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/scenarios/Roll The Dice.js @@ -0,0 +1,463 @@ +// Roll The Dice trigger script. +// This is basically a port from Age of Empires'2 Roll The Dice map. + +// 0 A.D.'s trigger system is extremely powerful, compared to basically anything else. So this won't even require a mod, despite tight integration. + +//////////////////// +//// Generic State +// some constants about the map +const MAP_SIZE = 64*4 + 2; +const INNERLAKE = 7*4; + +// multiply resources given by this amount, for rounding convenience. +const RESS_MULTIPLIER = 10; +// map RTD resources to actual resources in an agnostic manner. +// All we need is a sufficient number of resources, but it doesn't really matter which. +var RESOURCES = {}; +{ + let ress = ["kills"]; + [0].forEach(i => RESOURCES[ress[i]] = Resources.GetCodes()[i]); +} +// helper function +var RTD_AddResource = function(player, ress, amount) +{ + let cmpPlayer = QueryPlayerIDInterface(player); + cmpPlayer.AddResource(RESOURCES[ress], amount); +} + +// helper for messages +var BroadCastMessage = function(message, players) +{ + if (!players) + { + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + players = cmpPlayerManager.playerEntities.map(x => Engine.QueryInterface(x, IID_Player).GetPlayerID()); + } + let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({ "type": "text", "players": players, "message": message }); +} + +// store a tuple for each entity starting position to avoid recomputing each time. +var g_EntityStartPositions = []; + +// player ID: monument entity ID +var g_MonumentByPlayer = {}; +var g_MonumentsID = []; + +//////////////////// +//// Hack simulation +// disable capturing because that's annoying. +Capturable.prototype.CanCapture = function() { return false }; +// disable producing +ProductionQueue.prototype.GetEntitiesList = function() { return []; } +ProductionQueue.prototype.GetTechnologiesList = function() { return []; } +Builder.prototype.GetEntitiesList = function() { return []; } +// disable auras +Auras.prototype.CanApply = function() { return false; } +// disable loot +Loot.prototype.GetResources = function() { return {}; } +// can't disable repairing entirely but this'll do. +Repairable.prototype.GetRepairRate = function() { return 0; } +// warn player +Repairable.prototype.Repair = function(builderEnt, rate) +{ + BroadCastMessage("Repairing is disabled in Roll The Dice", [TriggerHelper.GetOwner(builderEnt)]); +} + +// Instead of idle units return currently active unit. +GuiInterface.prototype.FindIdleUnits = function(player) +{ + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let entities = cmpRangeManager.GetEntitiesByPlayer(player); + for (let ent of entities) + { + if (ent == g_MonumentByPlayer[player]) + continue; + return [ent]; + } + return [] +} +GuiInterface.prototype.HasIdleUnits = function(player) +{ + return this.FindIdleUnits(player).length; +} +//////////////////// +//// Gameplay state +// Player ID : damage count. +var RTD_DamageCount = {}; +// player ID : how much extra damage they bought. +var RTD_AttackBought = {}; +// player ID : how much extra HP they bought. +var RTD_HPBought = {}; + +var RTD_HasPanic = {}; +// RULES OF ROLL THE DICE +// As adapted from Age of Empire's 2 custom scenario. + +// 1. At any time, you have a Monument and one (or potentially more) unit on the map. +// 2. If your Monument is destroyed, you lose. +// 3. A side-goal is to kill enemy units, thus accruing kills. Actually count damage dealt as a % of enemy HP, to be fair and avoid "stealing" kills. +// As you kill units, you will be able to upgrade your Monument or your units to make the game easier. + + +// Mechanics to add: +// A death streak without kill should give bonuses +// A kill streak should also give bonuses (but warn players so than can gang up on you). +// Random "drops" of stuff. +// Terrain changing +// I have this idea for water rising and sharks attacking units randomly. + +Trigger.prototype.InitRTD = function() +{ + // Fetch player information. + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager) + + let players = []; + + for (let x = 1; x < cmpPlayerManager.GetNumPlayers(); ++x) + { + let id = cmpPlayerManager.GetPlayerByID(x); + let cmpPlayer = Engine.QueryInterface(id, IID_Player); + + g_EntityStartPositions.push([MAP_SIZE/2 + 3*INNERLAKE * Math.cos(x/cmpPlayerManager.GetNumPlayers()*6.283), MAP_SIZE/2 + 3*INNERLAKE * Math.sin(x/cmpPlayerManager.GetNumPlayers()*6.283)]); + + // imperfect detection of undefined players. + if (cmpPlayer.IsAI()) + continue; + + players.push(x); + } + + // create a monument for each player. + let count = players.length; + for (let id of players) + { + let monument = Engine.AddEntity("structures/athen_outpost"); + g_MonumentByPlayer[id] = monument; + g_MonumentsID.push(monument); + + + let cmpOwner = Engine.QueryInterface(monument, IID_Ownership); + cmpOwner.SetOwner(+id); + + let cmpModManager = QueryOwnerInterface(monument, IID_ModifiersManager); + cmpModManager.AddGlobalModifiers("triggers_RTD_Struct", 100, { + "Capturable/RegenRate": {"affects": ["Structure"], "replace": 100}, + "GarrisonHolder/Max": {"affects": ["Structure"], "replace": 0}, + "Vision/Range": {"affects": ["Structure"], "replace": 50}, + "Health/Max": {"affects": ["Structure"], "replace": 2000}, + }); + cmpModManager.AddGlobalModifiers("triggers_RTD_Unit", 100, { + "Looter/Resource/food": {"affects": ["Unit"], "replace": 0}, + "Looter/Resource/wood": {"affects": ["Unit"], "replace": 0}, + "Looter/Resource/stone": {"affects": ["Unit"], "replace": 0}, + "Looter/Resource/metal": {"affects": ["Unit"], "replace": 0}, + "Vision/Range": {"affects": ["Unit"], "replace": 20}, + "Promotion/RequiredXp": {"affects": ["Unit"], "replace": 9999999} + }); + + // Share all vision. + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + cmpRangeManager.SetSharedLos(+id, players) + + let cmpPosition = Engine.QueryInterface(monument, IID_Position); + cmpPosition.JumpTo(MAP_SIZE/2 + INNERLAKE * Math.cos(players.indexOf(id)/count*6.283), MAP_SIZE/2 + INNERLAKE * Math.sin(players.indexOf(id)/count*6.283)) + cmpPosition.SetYRotation(1.5707 + players.indexOf(id)/count*6.283) + + // Init rules + RTD_DamageCount[id] = 0; + + RTD_AttackBought[id] = 0; + RTD_HPBought[id] = 0; + RTD_HasPanic[id] = true; + + let cmpPlayer = QueryOwnerInterface(monument, IID_Player); + let resources = {}; + // set to 0.01 otherwise we have rounding issues in the GUI. + Resources.GetCodes().forEach(ress => resources[ress] = 0.01); + cmpPlayer.SetResourceCounts(resources); + + // Create the starting entities for each player. + this.CreateNewEntity(id, true); + } +} + +Trigger.prototype.PickRandomEntity = function() +{ + // list of possible units. + // TODO: curate this a bit. + let list = [ + "units/athen_cavalry_swordsman_a", + "units/athen_infantry_spearman_a", + "units/brit_cavalry_swordsman_a", + "units/brit_infantry_spearman_a", + "units/cart_cavalry_spearman_ital_a", + "units/cart_cavalry_swordsman_gaul_a", + "units/cart_cavalry_swordsman_iber_a", + "units/cart_champion_elephant", + "units/cart_hero_hannibal", + "units/cart_infantry_spearman_a", + "units/cart_infantry_swordsman_gaul_a", + "units/cart_infantry_swordsman_ital_a", + "units/gaul_cavalry_swordsman_a", + "units/gaul_infantry_spearman_a", + "units/iber_cavalry_spearman_a", + "units/iber_infantry_spearman_a", + "units/iber_infantry_swordsman_a", + "units/mace_cavalry_spearman_a", + "units/mace_hero_alexander", + "units/mace_infantry_pikeman_a", + "units/maur_cavalry_swordsman_a", + "units/maur_champion_elephant", + "units/maur_champion_infantry", + "units/maur_hero_maurya", + "units/maur_infantry_spearman_a", + "units/maur_infantry_swordsman_a", + "units/merc_thorakites", + "units/pers_cavalry_spearman_a", + "units/pers_cavalry_swordsman_a", + "units/pers_champion_elephant", + "units/pers_infantry_spearman_a", + "units/pers_mechanical_siege_ram", + "units/ptol_cavalry_spearman_merc_a", + "units/ptol_champion_elephant", + "units/ptol_hero_ptolemy_I", + "units/ptol_infantry_pikeman_a", + "units/ptol_infantry_spearman_merc_a", + "units/ptol_infantry_swordsman_merc_a", + "units/rome_cavalry_spearman_a", + "units/rome_centurio_imperial", + "units/rome_infantry_spearman_a", + "units/rome_infantry_swordsman_a", + "units/rome_legionnaire_imperial", + "units/rome_mechanical_siege_ram", + "units/sele_cavalry_spearman_merc_a", + "units/sele_champion_elephant", + "units/sele_hero_seleucus_victor", + "units/sele_infantry_pikeman_a", + "units/sele_infantry_spearman_a", + "units/sele_infantry_swordsman_merc_a", + "units/spart_cavalry_spearman_a", + "units/spart_champion_infantry_sword", + "units/spart_infantry_spearman_a", + "units/spart_support_female_citizen", + "units/athen_cavalry_javelinist_a", + "units/athen_champion_ranged", + "units/athen_infantry_javelinist_a", + "units/athen_infantry_marine_archer_a", + "units/athen_infantry_slinger_a", + "units/brit_cavalry_javelinist_a", + "units/brit_infantry_javelinist_a", + "units/brit_infantry_slinger_a", + "units/cart_cavalry_javelinist_a", + "units/cart_infantry_archer_a", + "units/cart_infantry_javelinist_iber_a", + "units/cart_infantry_slinger_iber_a", + "units/gaul_cavalry_javelinist_a", + "units/gaul_infantry_javelinist_a", + "units/gaul_infantry_slinger_a", + "units/iber_cavalry_javelinist_a", + "units/iber_champion_cavalry", + "units/iber_hero_indibil", + "units/iber_infantry_javelinist_a", + "units/iber_infantry_slinger_a", + "units/mace_cavalry_javelinist_a", + "units/mace_infantry_archer_a", + "units/mace_infantry_javelinist_a", + "units/mace_infantry_slinger_a", + "units/maur_cavalry_javelinist_a", + "units/maur_elephant_archer_a", + "units/maur_infantry_archer_a", + "units/pers_cavalry_archer_a", + "units/pers_cavalry_javelinist_a", + "units/pers_infantry_archer_a", + "units/pers_infantry_javelinist_a", + "units/ptol_cavalry_archer_a", + "units/ptol_cavalry_javelinist_merc_a", + "units/ptol_infantry_archer_a", + "units/ptol_infantry_archer_nubian", + "units/ptol_infantry_javelinist_a", + "units/ptol_infantry_slinger_a", + "units/rome_cavalry_javelinist_a", + "units/rome_infantry_javelinist_a", + "units/rome_mechanical_siege_onager_unpacked", + "units/sele_cavalry_archer_a", + "units/sele_cavalry_javelinist_a", + "units/sele_infantry_archer_merc_a", + "units/sele_infantry_javelinist_a", + "units/spart_cavalry_javelinist_a", + "units/spart_infantry_javelinist_a", + "units/theb_mechanical_siege_fireraiser", + ]; + + let item = pickRandom(list); + + return item; +} + +Trigger.prototype.CreateNewEntity = function(player, startPos) +{ + // TODO: validate current entity is killed? + + let entity = Engine.AddEntity(this.PickRandomEntity()); + + let cmpOwner = Engine.QueryInterface(entity, IID_Ownership); + cmpOwner.SetOwner(+player); + + let cmpPosition = Engine.QueryInterface(entity, IID_Position); + let pos = startPos ? g_EntityStartPositions[player-1] : pickRandom(g_EntityStartPositions); + cmpPosition.JumpTo(pos[0], pos[1]); + + let cmpHealth = Engine.QueryInterface(entity, IID_Health); + cmpHealth.SetUndeletable(true); + + let cmpAbilities = Engine.QueryInterface(entity, IID_Abilities); + + { + let ability = { + "Name": "Re-roll your unit", + "Tooltip": "This allows you to kill your current unit and re-roll it.", + "Cost": {}, + "Icon": "icons/kill.png", + "TriggerAction": {"Trigger": "RTD_ReRoll"} + }; + ability.Cost[RESOURCES["kills"]] = 2*RESS_MULTIPLIER; + cmpAbilities.AddAbility("Reroll", ability); + } + // for now just do it like this it'll work OK. + if (RTD_HasPanic[player]) + { + let ability = { + "Name": "Panic", + "Tooltip": "This kills every unit on the map. Can only be used once.", + "Icon": "portraits/technologies/skull_swords.png", + "TriggerAction": {"Trigger": "RTD_Panic"} + }; + cmpAbilities.AddAbility("Panic", ability); + } + { + let ability = { + "Name": "Increase Attack", + "Tooltip": "This will permanently increase your unit attacks by +2 Hack and +2 Pierce, stacking.", + "Cost": {}, + "Icon": "portraits/technologies/sword.png", + "TriggerAction": {"Trigger": "RTD_PurchaseAttack"} + }; + ability.Cost[RESOURCES["kills"]] = 5*RESS_MULTIPLIER; + cmpAbilities.AddAbility("PurchaseAttack", ability); + } + { + let ability = { + "Name": "Boost Outpost HP", + "Tooltip": "This will give +500 HP to your outpost (including max HP)", + "Cost": {}, + "Icon": "portraits/structures/outpost.png", + "TriggerAction": {"Trigger": "RTD_PurchaseHP"} + }; + ability.Cost[RESOURCES["kills"]] = 10*RESS_MULTIPLIER; + cmpAbilities.AddAbility("PurchaseHP", ability); + } +} + +Trigger.prototype.ChangeKillCount = function(player, amount) +{ + RTD_DamageCount[player] += +amount; + RTD_AddResource(player, "kills", amount*RESS_MULTIPLIER); +} + +// Abilities triggers + +Trigger.prototype.RTD_ReRoll = function(player) +{ + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let entities = cmpRangeManager.GetEntitiesByPlayer(player); + for (let ent of entities) + { + if (ent == g_MonumentByPlayer[player]) + continue; + let cmpHealth = Engine.QueryInterface(ent, IID_Health); + cmpHealth.Kill(); + } + // no need to create a new entity since on OwnershipChanged deals with it. +} + +Trigger.prototype.RTD_Panic = function(player) +{ + RTD_HasPanic[player] = false; + + BroadCastMessage("Player " + player + " has used the PANIC button!"); + // TODO: sound + + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let entities = cmpRangeManager.GetNonGaiaEntities(); + for (let ent of entities) + { + let player = TriggerHelper.GetOwner(ent); + if (ent == g_MonumentByPlayer[player]) + continue; + let cmpHealth = Engine.QueryInterface(ent, IID_Health); + cmpHealth.Kill(); + } +} + +Trigger.prototype.RTD_PurchaseAttack = function(player) +{ + RTD_AttackBought[player] += 2; + + let cmpModManager = QueryPlayerIDInterface(player, IID_ModifiersManager); + cmpModManager.RemoveGlobalModifiers("RTD_PurchaseAttack"); + cmpModManager.AddGlobalModifiers("RTD_PurchaseAttack", 1000, { + "Attack/Ranged/Hack": {"affects": ["Unit"], "add": RTD_AttackBought[player]}, + "Attack/Ranged/Pierce": {"affects": ["Unit"], "add": RTD_AttackBought[player]}, + "Attack/Melee/Hack": {"affects": ["Unit"], "add": RTD_AttackBought[player]}, + "Attack/Melee/Hack": {"affects": ["Unit"], "add": RTD_AttackBought[player]} + }); +} + +Trigger.prototype.RTD_PurchaseHP = function(player) +{ + RTD_HPBought[player] += 500; + let cmpModManager = QueryPlayerIDInterface(player, IID_ModifiersManager); + cmpModManager.RemoveGlobalModifiers("RTD_PurchaseHP"); + cmpModManager.AddGlobalModifiers("RTD_PurchaseHP", 1000, { + "Health/Max": {"affects": ["Structure"], "add": RTD_HPBought[player]}, + }); +} +/// EVENTS + +Trigger.prototype.OnOwnershipChanged = function(msg) +{ + if (msg.to != -1) + return; + if (msg.from > 0 && g_MonumentByPlayer[msg.from] == msg.entity) + { + TriggerHelper.DefeatPlayer(msg.from); + } + else if (msg.from != 0) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "CreateNewEntity", 1000, msg.from); + } +} + +Trigger.prototype.OnAttacked = function(msg) +{ + if (g_MonumentsID.some(id => msg.target == id)) + return; + + let cmpHealth = Engine.QueryInterface(msg.target, IID_Health); + // on principle this shouldn't happen + if (!cmpHealth) + return; + let damage = msg.damage / cmpHealth.GetMaxHitpoints(); + // damage caps at 1. + this.ChangeKillCount(msg.attackerOwner, damage); +} + +{ + let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + + cmpTrigger.RegisterTrigger("OnInitGame", "InitRTD", { "enabled": true }); + cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true }); + cmpTrigger.RegisterTrigger("OnAttacked", "OnAttacked", { "enabled": true }); +} \ No newline at end of file Index: binaries/data/mods/public/maps/scenarios/Roll The Dice.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/scenarios/Roll The Dice.xml @@ -0,0 +1,1384 @@ + + + + + sunny 1 + + + + + + + 0 + 0.5 + + + + + lake + + + 79 + 1.23047 + 0.493164 + 0 + + + + 0 + 1 + 0.99 + 0.1999 + default + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + 0 + + + + + + + 0 + + + + + + + 0 + + + + + + + \ No newline at end of file Index: binaries/data/mods/public/simulation/components/Trigger.js =================================================================== --- binaries/data/mods/public/simulation/components/Trigger.js +++ binaries/data/mods/public/simulation/components/Trigger.js @@ -24,7 +24,8 @@ "StructureBuilt", "TrainingFinished", "TrainingQueued", - "TreasureCollected" + "TreasureCollected", + "Attacked" ]; Trigger.prototype.Init = function() @@ -277,6 +278,11 @@ this.CallEvent("DiplomacyChanged", msg); }; +Trigger.prototype.OnGlobalAttacked = function(msg) +{ + this.CallEvent("Attacked", msg); +}; + /** * Execute a function after a certain delay. * Index: binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit.xml +++ binaries/data/mods/public/simulation/templates/template_unit.xml @@ -1,5 +1,6 @@ + 1 1