Index: binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- binaries/data/mods/public/globalscripts/Technologies.js +++ binaries/data/mods/public/globalscripts/Technologies.js @@ -20,27 +20,24 @@ { let modifications = currentTechModifications[propertyName] || []; - let multiply = 1; - let add = 0; - + let value = propertyValue; for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (modification.replace !== undefined) - return modification.replace; - if (modification.multiply) - multiply *= modification.multiply; + value = modification.replace; + else if (typeof value != "number" && (modification.multiply || modification.add)) + warn("GetTechModifiedProperty: modifying a non-numeric (modifying " + propertyName + ") by adding or multiplying it is forbidden." + uneval(modification)); + else if (modification.multiply) + value *= modification.multiply; else if (modification.add) - add += modification.add; + value += modification.add; else warn("GetTechModifiedProperty: modification format not recognised (modifying " + propertyName + "): " + uneval(modification)); } - // Note, some components pass non-numeric values (for which only the "replace" modification makes sense) - if (typeof propertyValue == "number") - return propertyValue * multiply + add; - return propertyValue; + return value; } /** 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), 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,621 @@ +// Roll The Dice trigger script. +// This is basically a port from Age of Empires'2 Roll The Dice map. + +//////////////////// +//// 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", "attack"]; + [0, 1].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 }); +} + +//////////////////// +//// 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 a currently idle hero +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; + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI && cmpUnitAI.IsIdle()) + return [ent]; + } + return []; +} +GuiInterface.prototype.HasIdleUnits = function(player) +{ + return this.FindIdleUnits(player).length; +} +//////////////////// +//// Gameplay state + +// 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 = []; + +// Player ID : damage count. +var RTD_DamageCount = {'0': 0}; +// player ID : how much extra damage they bought. +var RTD_AttackBought = {'0': 0}; +// player ID : how much extra HP they bought. +var RTD_HPBought = {'0': 0}; + +var RTD_HasPanic = {'0': false}; +// 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)]); + + 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": 10000}, + }); + 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}, + // bump attack levels across the board to not make this atrociously slow. + "Attack/Ranged/Hack": {"affects": ["Unit"], "add": 20}, + "Attack/Ranged/Pierce": {"affects": ["Unit"], "add": 20}, + "Attack/Melee/Hack": {"affects": ["Unit"], "add": 30}, + "Attack/Melee/Pierce": {"affects": ["Unit"], "add": 30}, + "Attack/Ranged/Spread": {"affects": ["Unit"], "add": -2} + }); + + // 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 listByTier = [ + // Tier 0 : special, weird, and useless units. + [ + "units/brit_war_dog_e", + "units/spart_support_female_citizen", + "units/mace_mechanical_siege_ram", + "units/pers_mechanical_siege_ram", + "units/rome_mechanical_siege_ballista_packed", + "units/sele_mechanical_siege_lithobolos_packed", + "units/sele_mechanical_siege_tower", + "units/spart_mechanical_siege_oxybeles_packed", + "units/spart_mechanical_siege_ram", + ], + // Tier 1 : regular citizen soldiers. Includes cavalry and infantry + [ + "units/athen_infantry_javelinist_e", + "units/athen_infantry_marine_archer_e", + "units/athen_infantry_slinger_e", + "units/athen_infantry_spearman_e", + "units/brit_infantry_javelinist_e", + "units/brit_infantry_slinger_e", + "units/brit_infantry_spearman_e", + "units/cart_infantry_archer_e", + "units/cart_infantry_javelinist_iber_e", + "units/cart_infantry_slinger_iber_e", + "units/cart_infantry_spearman_e", + "units/cart_infantry_swordsman_gaul_e", + "units/cart_infantry_swordsman_ital_e", + "units/gaul_infantry_javelinist_e", + "units/gaul_infantry_slinger_e", + "units/gaul_infantry_spearman_e", + "units/iber_infantry_javelinist_e", + "units/iber_infantry_slinger_e", + "units/iber_infantry_spearman_e", + "units/iber_infantry_swordsman_e", + "units/mace_infantry_archer_e", + "units/mace_infantry_javelinist_e", + "units/mace_infantry_pikeman_e", + "units/mace_infantry_slinger_e", + "units/maur_infantry_archer_e", + "units/maur_infantry_spearman_e", + "units/maur_infantry_swordsman_e", + "units/pers_infantry_archer_e", + "units/pers_infantry_javelinist_e", + "units/pers_infantry_spearman_e", + "units/ptol_infantry_archer_e", + "units/ptol_infantry_archer_nubian", + "units/ptol_infantry_javelinist_e", + "units/ptol_infantry_pikeman_e", + "units/ptol_infantry_slinger_e", + "units/ptol_infantry_spearman_merc_e", + "units/ptol_infantry_swordsman_merc_e", + "units/rome_infantry_javelinist_e", + "units/rome_infantry_spearman_e", + "units/rome_infantry_swordsman_e", + "units/sele_infantry_archer_merc_e", + "units/sele_infantry_javelinist_e", + "units/sele_infantry_pikeman_e", + "units/sele_infantry_spearman_e", + "units/sele_infantry_swordsman_merc_e", + "units/spart_champion_infantry_sword", + "units/spart_infantry_javelinist_e", + "units/spart_infantry_spearman_e", + "units/athen_cavalry_javelinist_e", + "units/athen_cavalry_swordsman_e", + "units/brit_cavalry_javelinist_e", + "units/brit_cavalry_swordsman_e", + "units/cart_cavalry_javelinist_e", + "units/cart_cavalry_spearman_ital_e", + "units/cart_cavalry_swordsman_gaul_e", + "units/cart_cavalry_swordsman_iber_e", + "units/gaul_cavalry_javelinist_e", + "units/gaul_cavalry_swordsman_e", + "units/iber_cavalry_javelinist_e", + "units/iber_cavalry_spearman_e", + "units/mace_cavalry_javelinist_e", + "units/mace_cavalry_spearman_e", + "units/maur_cavalry_javelinist_e", + "units/maur_cavalry_swordsman_e", + "units/maur_elephant_archer_e", + "units/pers_cavalry_archer_e", + "units/pers_cavalry_javelinist_e", + "units/pers_cavalry_spearman_e", + "units/pers_cavalry_swordsman_e", + "units/ptol_cavalry_archer_e", + "units/ptol_cavalry_javelinist_merc_e", + "units/ptol_cavalry_spearman_merc_e", + "units/rome_cavalry_javelinist_e", + "units/rome_cavalry_spearman_e", + "units/sele_cavalry_archer_e", + "units/sele_cavalry_javelinist_e", + "units/sele_cavalry_spearman_merc_e", + "units/spart_cavalry_javelinist_e", + "units/spart_cavalry_spearman_e", + ], + // Tier 2 : champions and such + [ + "units/athen_champion_infantry", + "units/athen_champion_marine", + "units/athen_champion_ranged", + "units/brit_champion_cavalry", + "units/brit_champion_infantry", + "units/cart_champion_cavalry", + "units/cart_champion_elephant", + "units/cart_champion_infantry", + "units/cart_champion_pikeman", + "units/gaul_champion_cavalry", + "units/gaul_champion_fanatic", + "units/gaul_champion_infantry", + "units/iber_champion_cavalry", + "units/iber_champion_infantry", + "units/mace_champion_cavalry", + "units/mace_champion_infantry_a", + "units/maur_champion_chariot", + "units/maur_champion_elephant", + "units/maur_champion_infantry", + "units/maur_champion_maiden_archer", + "units/maur_champion_maiden", + "units/merc_black_cloak", + "units/merc_thorakites", + "units/merc_thureophoros", + "units/pers_champion_cavalry_archer", + "units/pers_champion_cavalry", + "units/pers_champion_elephant", + "units/pers_champion_infantry", + "units/pers_kardakes_hoplite", + "units/pers_kardakes_skirmisher", + "units/ptol_champion_cavalry", + "units/ptol_champion_elephant", + "units/ptol_champion_infantry_pikeman", + "units/rome_champion_cavalry", + "units/rome_champion_infantry", + "units/rome_legionnaire_marian", + "units/samnite_skirmisher", + "units/samnite_spearman", + "units/samnite_swordsman", + "units/sele_champion_cavalry", + "units/sele_champion_chariot", + "units/sele_champion_elephant", + "units/sele_champion_infantry_pikeman", + "units/sele_champion_infantry_swordsman", + "units/spart_champion_infantry_pike", + "units/spart_champion_infantry_spear", + "units/thebes_sacred_band_hoplitai", + "units/thespian_melanochitones", + ], + // Tier 3 : Heroes + [ + "units/athen_hero_iphicrates", + "units/athen_hero_pericles", + "units/athen_hero_themistocles", + "units/brit_hero_boudicca_sword", + "units/brit_hero_boudicca", + "units/brit_hero_caratacos", + "units/brit_hero_cunobelin_infantry", + "units/brit_hero_cunobelin", + "units/cart_hero_hamilcar", + "units/cart_hero_hannibal", + "units/cart_hero_maharbal", + "units/gaul_hero_brennus", + "units/gaul_hero_britomartus", + "units/gaul_hero_vercingetorix", + "units/iber_hero_caros", + "units/iber_hero_indibil", + "units/iber_hero_viriato", + "units/mace_hero_alexander", + "units/mace_hero_craterus", + "units/mace_hero_demetrius", + "units/mace_hero_philip_pike", + "units/mace_hero_philip", + "units/mace_hero_pyrrhus", + "units/maur_hero_ashoka", + "units/maur_hero_maurya", + "units/pers_hero_cyrus", + "units/pers_hero_darius", + "units/pers_hero_xerxes_chariot", + "units/pers_hero_xerxes", + "units/ptol_hero_cleopatra", + "units/ptol_hero_ptolemy_I", + "units/ptol_hero_ptolemy_IV", + "units/rome_hero_marcellus", + "units/rome_hero_maximus", + "units/rome_hero_scipio", + "units/sele_hero_antiochus_great", + "units/sele_hero_antiochus_righteous", + "units/sele_hero_seleucus_victor", + "units/spart_hero_agis", + "units/spart_hero_brasidas", + "units/spart_hero_leonidas", + ] + ]; + + let probabilities = [10,60,90,100]; + + let random = randIntExclusive(0,100); + let tier = probabilities.filter(x => random < x).length - 1; + + let item = pickRandom(listByTier[tier]); + + return item; +} + +Trigger.prototype.CreateNewEntity = function(player, startPos) +{ + // TODO: validate current entity is killed? + + let entity = Engine.AddEntity(this.PickRandomEntity()); + + BroadCastMessage("You have rolled " + Engine.QueryInterface(entity, IID_Identity).GetGenericName() + ". Good Luck!", [+player]) + + 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); + + // reset rules + RTD_AttackBought[player] = 0; + let cmpPlayer = QueryPlayerIDInterface(player); + cmpPlayer.resourceCount[RESOURCES["attack"]] = 0; + + 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 increase this unit's attack by +" + RTD_ATTACK_BUFF + " Hack/Pierce, stacking. You will lose this when the unit dies.", + "Cost": {}, + "Icon": "portraits/technologies/sword.png", + "TriggerAction": {"Trigger": "RTD_PurchaseAttack"} + }; + ability.Cost[RESOURCES["kills"]] = 2*RESS_MULTIPLIER; + cmpAbilities.AddAbility("PurchaseAttack", ability); + } + { + let ability = { + "Name": "Boost Outpost HP", + "Tooltip": "This will give +1000 HP to your outpost HP, including maximum HP.", + "Cost": {}, + "Icon": "portraits/structures/outpost.png", + "TriggerAction": {"Trigger": "RTD_PurchaseHP"} + }; + ability.Cost[RESOURCES["kills"]] = 10*RESS_MULTIPLIER; + cmpAbilities.AddAbility("PurchaseHP", ability); + } + // This ability lets you trade 5K HP for a new unit. + // If you're close to 1 HP, it might be worth activating anyways. + { + let ability = { + "Name": "Living on the Edge", + "Tooltip": "Sacrifice 5000 HP on your Monument in exchange for an additional unit. If you have less than 5000HP left, this will leave you with 1 HP.", + "Cost": {}, + "Icon": "icons/die.png", + "TriggerAction": {"Trigger": "RTD_AdditionalUnit"} + }; + ability.Cost[RESOURCES["kills"]] = 5*RESS_MULTIPLIER; + cmpAbilities.AddAbility("AdditionalUnit", 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, entity) +{ + let cmpHealth = Engine.QueryInterface(entity, IID_Health); + cmpHealth.Kill(); + this.CreateNewEntity(player); +} + +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(); + this.CreateNewEntity(player); + } +} + +Trigger.prototype.RTD_AdditionalUnit = function(player) +{ + BroadCastMessage("Player " + player + " has traded HP for another unit!"); + // TODO: sound + + let cmpHealth = Engine.QueryInterface(g_MonumentByPlayer[player], IID_Health); + cmpHealth.Reduce(cmpHealth.GetHitpoints() > 5000 ? 5000 : cmpHealth.GetHitpoints() - 1); + this.CreateNewEntity(player); +} + +const RTD_ATTACK_BUFF = 10; +Trigger.prototype.RTD_PurchaseAttack = function(player, entity) +{ + RTD_AttackBought[player] += RTD_ATTACK_BUFF; + RTD_AddResource(player, "attack", RTD_ATTACK_BUFF); + + let cmpModManager = QueryPlayerIDInterface(player, IID_ModifiersManager); + cmpModManager.RemoveLocalModifiers(entity, "RTD_PurchaseAttack"); + cmpModManager.AddLocalModifiers(entity, "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/Pierce": {"affects": ["Unit"], "add": RTD_AttackBought[player]} + }); +} + +Trigger.prototype.RTD_PurchaseHP = function(player) +{ + let cmpHealth = Engine.QueryInterface(g_MonumentByPlayer[player], IID_Health); + let health = cmpHealth.GetHitpoints(); + + RTD_HPBought[player] += 1000; + let cmpModManager = QueryPlayerIDInterface(player, IID_ModifiersManager); + cmpModManager.RemoveGlobalModifiers("RTD_PurchaseHP"); + cmpModManager.AddGlobalModifiers("RTD_PurchaseHP", 1000, { + "Health/Max": {"affects": ["Structure"], "add": RTD_HPBought[player]}, + }); + let newhealth = cmpHealth.GetHitpoints(); + cmpHealth.increase(Math.max(0, 1000 - (newhealth - health))); +} + +/// Pseudo-AI, pick a random entity to attack if we're idle. +Trigger.prototype.PseudoAITick = function() +{ + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let entities = cmpRangeManager.GetNonGaiaEntities(); + for (let ent of entities) + { + let enemy = pickRandom(entities); + if (TriggerHelper.GetOwner(enemy) == TriggerHelper.GetOwner(ent)) + continue; // too bad + if (!QueryOwnerInterface(ent, IID_Player).IsAI()) + continue; + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (!cmpUnitAI || !cmpUnitAI.IsIdle()) + continue; + cmpUnitAI.Attack(enemy, false); + } +} + +/// 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); + // check if any player has won. + let standing = g_MonumentsID.map(id => Engine.QueryInterface(id, IID_Health) != undefined).filter(x => x); + if (standing.length == 1) + TriggerHelper.SetPlayerWon(TriggerHelper.GetOwner(standing[0])); + } +} + +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); + + if (cmpHealth.GetHitpoints() == 0 && TriggerHelper.GetOwner(msg.target) != 0) + { + // can't use ownership changed because packing and unpacking would give free units. + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "CreateNewEntity", 1000, TriggerHelper.GetOwner(msg.target)); + } +} + +{ + 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 }); + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.SetInterval(SYSTEM_ENTITY, IID_Trigger, "PseudoAITick", 1000, 2000, {}); + + BroadCastMessage("Welcome to Roll The Dice. Read the map description for a brief overview of the rules.", [1,2,3,4,5,6,7,8]) +} \ 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