Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_outpost.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_outpost.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_outpost.xml (revision 16550) @@ -1,102 +1,102 @@ 5 20 1 1 5 1 0.0 20.0 0.0 55.0 13.0 75.0 1200 2000 2.0 1 own neutral House 40 80 0 15.0 1 0.1 Unit Support Infantry 0 2 800 rubble/rubble_stone_2x2 Outpost Build in neutral and own territories to scout areas of the map. Slowly loses health while in neutral territory. -ConquestCritical Village Outpost structures/outpost.png 100 0 8 0 0 0.7 vision_outpost decay_outpost interface/complete/building/complete_tower.xml attack/destruction/building_collapse_large.xml 6.0 0.6 18.0 - 2 + 2 80 props/special/palisade_rocks_outpost.xml structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_tower.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_tower.xml (revision 16550) @@ -1,94 +1,95 @@ 0.0 20.0 0.0 88.0 8.0 75.0 1200 2000 1.5 Human 0 1 Infantry land-shore Wall + 120 100 8.0 2 0.1 Unit Support Infantry 0 2 5000 rubble/rubble_stone_wall_tower Wall Turret Shoots arrows. Garrison to defend a city wall against attackers. -ConquestCritical StoneWall Tower structures/tower.png phase_town 100 0 10 15 0 0.8 pair_walls_01 interface/complete/building/complete_tower.xml attack/weapon/arrowfly.xml attack/destruction/building_collapse_large.xml 20.0 false 20 65536 60 structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 16550) @@ -1,108 +1,111 @@ 0.0 20.0 0.0 72.0 8.0 75.0 1200 2000 1.5 Human 3 1 Infantry Ranged Fortress Fortress 80 + + 4000 + 10 300 0 650 8.0 20 0.075 Unit Support Infantry Cavalry Siege 0 6 4200 rubble/rubble_stone_6x6 Fortress Train heroes, champions, and siege weapons. Research siege weapon improvements. Garrison: 20. GarrisonFortress Defensive City Fortress structures/fortress.png phase_city 100 0 0 65 0 0.8 units/{civ}_mechanical_siege_ballista_packed units/{civ}_mechanical_siege_scorpio_packed units/{civ}_mechanical_siege_oxybeles_packed units/{civ}_mechanical_siege_lithobolos_packed units/{civ}_mechanical_siege_ram units/{civ}_mechanical_siege_tower attack_soldiers_will interface/complete/building/complete_fortress.xml attack/weapon/arrowfly.xml attack/destruction/building_collapse_large.xml false 100 40000 80 structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 16550) @@ -1,106 +1,111 @@ 3 1 15 + + 3 + 4 + 1000 + 100.0 0.0 0.0 4.0 1 12 80 7.5 150 CitizenSoldier Human Organic Cavalry Citizen Soldier Cavalry Basic formations/wedge 130 10 0 0 0 pitch 15000000 2.0 1.0 5 20 20 20 20 circle/128x128.png circle/128x128_mask.png voice/hellenes/civ/civ_male_ack.xml voice/hellenes/civ/civ_male_attack.xml voice/hellenes/civ/civ_male_ack.xml actor/mounted/movement/walk.xml actor/mounted/movement/walk.xml attack/weapon/sword.xml actor/fauna/death/death_horse.xml interface/alarm/alarm_create_cav.xml 2000 6.5 16.5 26.0 600.0 5.0 92 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 16550) @@ -1,75 +1,75 @@ 15.0 35.0 5.0 4.0 7.0 3.0 own neutral enemy 60 0 7.0 05.70 85.70 -85.70 45.70 -45.70 2000 rome structures/rome_wallset_siege Siege Wall Murus Circummunitionis SiegeWall structures/palisade_wall.png A wooden and turf palisade buildable in enemy and neutral territories. Convert Siege Wall into Siege Wall Gate Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia. - 1 + 1 structures/romans/siege_wall_long.xml structures/fndn_wall.xml 36.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_short.xml (revision 16550) @@ -1,55 +1,55 @@ 15.0 35.0 5.0 4.0 7.0 3.0 own neutral enemy 20 0 7.0 1000 rome structures/rome_wallset_siege Siege Wall Murus Circummunitionis SiegeWall structures/palisade_wall.png A wooden and turf palisade buildable in enemy and neutral territories. Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia. - 1 + 1 structures/romans/siege_wall_short.xml structures/fndn_1x1.xml 12.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_tent.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_tent.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_tent.xml (revision 16550) @@ -1,59 +1,59 @@ own neutral enemy 30 50 5.0 5 0.1 Unit Support Infantry 0 2 200 rome Tent Tabernāculum -Village A temporary shelter for soldiers. +5 population bonus. 1.0 -units/{civ}_support_female_citizen_house interface/complete/building/complete_universal.xml attack/destruction/building_collapse_large.xml - 1 + 1 props/structures/romans/rome_tent.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 16550) @@ -1,119 +1,124 @@ 1 1 1 1 1 1 0 0 Ranged Infantry land own + + 1000 + 0 + 3 + 0 0 10 0 0 0 0 false false 0.0 3.0 9.8 corpse 0 true true Structure Structure ConquestCritical structure true true true true true false false special/rallypoint art/textures/misc/rallypoint_line.png art/textures/misc/rallypoint_line_mask.png 0.2 square round default default outline_border.png outline_border_mask.png 0.4 interface/complete/building/complete_universal.xml attack/destruction/building_collapse_large.xml interface/alarm/alarm_attackplayer.xml attack/weapon/arrowfly.xml attack/impact/arrow_metal.xml 6.0 0.6 12.0 - 5 + 5 true false false 40 false true false Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml (revision 16550) @@ -1,80 +1,83 @@ House + + 300 + 5 30 75 5.0 3 0 0.1 Unit Support 1 800 rubble/rubble_stone_3x3 House Increase the population limit. -ConquestCritical Village House structures/house.png 100 0 10 10 0 1.0 units/{civ}_support_female_citizen_house health_females_01 pop_house_01 unlock_females_house interface/complete/building/complete_house.xml attack/destruction/building_collapse_large.xml 6.0 0.6 8.0 false 20 40000 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_gate.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall_gate.xml (revision 16550) @@ -1,67 +1,68 @@ 5.0 Wall + 0 60 8.0 20 3000 rubble/rubble_stone_wall_long City Gate Allow units access through a city wall. Can be locked to prevent access. -ConquestCritical StoneWall Gates structures/gate.png phase_town 100 0 20 20 0 interface/complete/building/complete_gate.xml attack/destruction/building_collapse_large.xml actor/gate/stonegate_close.xml actor/gate/stonegate_open.xml interface/select/building/sel_gate.xml interface/select/building/sel_gate.xml false 20 65536 structures/fndn_wall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_storehouse.xml (revision 16550) @@ -1,81 +1,84 @@ Storehouse + + 300 + 40 100 8.0 800 rubble/rubble_stone_3x3 Storehouse Dropsite for wood, stone, and metal resources. Research gathering improvements for these resources. DropsiteWood DropsiteMetal DropsiteStone -ConquestCritical Village Storehouse structures/storehouse.png 100 0 10 0 0 0.7 gather_lumbering_ironaxes gather_lumbering_strongeraxes gather_lumbering_sharpaxes gather_mining_servants gather_mining_serfs gather_mining_slaves gather_mining_wedgemallet gather_mining_shaftmining gather_mining_silvermining gather_capacity_basket gather_capacity_wheelbarrow gather_capacity_carts wood stone metal interface/complete/building/complete_storehouse.xml attack/destruction/building_collapse_large.xml false 16 30000 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_wonder.xml (revision 16550) @@ -1,87 +1,90 @@ 5 25 5 2 10 2 Wonder + + 5000 + 1000 0 1000 1000 1000 10.0 5000 rubble/rubble_stone_6x6 Wonder Bring glory to your civilization and add large tracts of land to your empire. City Wonder structures/wonder.png phase_city 200 0 100 100 100 0.7 pop_wonder interface/complete/building/complete_wonder.xml attack/destruction/building_collapse_large.xml 6.0 0.6 12.0 true 100 65536 72 structures/fndn_6x6.xml 300 Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 16550) @@ -1,303 +1,313 @@ /** * Gets an array of all classes for this identity template */ function GetIdentityClasses(template) { var classList = []; if (template.Classes && template.Classes._string) classList = classList.concat(template.Classes._string.split(/\s+/)); if (template.VisibleClasses && template.VisibleClasses._string) classList = classList.concat(template.VisibleClasses._string.split(/\s+/)); if (template.Rank) classList = classList.concat(template.Rank); return classList; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { if (template.VisibleClasses && template.VisibleClasses._string) return template.VisibleClasses._string.split(/\s+/); return []; } /** * Check if the classes given in the identity template * match a list of classes * @param classes List of the classes to check against * @param match Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (var sublist of match) { // if the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(function(c) { return classes.indexOf(c) != -1; })) return true; } return false; } /** * Get information about a template with or without technology modifications. * @param template A valid template as returned by the template loader. * @param player An optional player id to get the technology modifications * of properties. */ function GetTemplateDataHelper(template, player) { var ret = {}; var func; if (player) func = ApplyValueModificationsToTemplate; else func = function(a, val, c, d) { return val; } if (template.Armour) { ret.armour = { "hack": func("Armour/Hack", +template.Armour.Hack, player, template), "pierce": func("Armour/Pierce", +template.Armour.Pierce, player, template), "crush": func("Armour/Crush", +template.Armour.Crush, player, template), }; } if (template.Attack) { + let getAttackStat = function(type, stat) + { + return func("Attack/"+type+"/"+stat, +(template.Attack[type][stat] || 0), player, template); + }; + ret.attack = {}; - for (var type in template.Attack) + for (let type in template.Attack) { - ret.attack[type] = { - "hack": func("Attack/"+type+"/Hack", +(template.Attack[type].Hack || 0), player, template), - "pierce": func("Attack/"+type+"/Pierce", +(template.Attack[type].Pierce || 0), player, template), - "crush": func("Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0), player, template), - "minRange": func("Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0), player, template), - "maxRange": func("Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange, player, template), - "elevationBonus": func("Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0), player, template), - "repeatTime": +(template.Attack[type].RepeatTime || 0), - }; + if (type == "Capture") + ret.attack.Capture = { + "value": getAttackStat(type,"Value"), + }; + else + ret.attack[type] = { + "hack": getAttackStat(type, "Hack"), + "pierce": getAttackStat(type, "Pierce"), + "crush": getAttackStat(type, "Crush"), + "minRange": getAttackStat(type, "MinRange"), + "maxRange": getAttackStat(type, "MaxRange"), + "elevationBonus": getAttackStat(type, "ElevationBonus"), + }; + ret.attack[type].repeatTime = +(template.Attack[type].RepeatTime || 0); } } if (template.Auras) { ret.auras = {}; for each (var aura in template.Auras) if (aura.AuraName) ret.auras[aura.AuraName] = aura.AuraDescription || null; } if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromCategory": template.BuildRestrictions.Distance.FromCategory, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance; if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance; } } if (template.TrainingRestrictions) { ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; } if (template.Cost) { ret.cost = {}; if (template.Cost.Resources.food) ret.cost.food = func("Cost/Resources/food", +template.Cost.Resources.food, player, template); if (template.Cost.Resources.wood) ret.cost.wood = func("Cost/Resources/wood", +template.Cost.Resources.wood, player, template); if (template.Cost.Resources.stone) ret.cost.stone = func("Cost/Resources/stone", +template.Cost.Resources.stone, player, template); if (template.Cost.Resources.metal) ret.cost.metal = func("Cost/Resources/metal", +template.Cost.Resources.metal, player, template); if (template.Cost.Population) ret.cost.population = func("Cost/Population", +template.Cost.Population, player, template); if (template.Cost.PopulationBonus) ret.cost.populationBonus = func("Cost/PopulationBonus", +template.Cost.PopulationBonus, player, template); if (template.Cost.BuildTime) ret.cost.time = func("Cost/BuildTime", +template.Cost.BuildTime, player, template); } if (template.Footprint) { ret.footprint = {"height": template.Footprint.Height}; if (template.Footprint.Square) ret.footprint.square = {"width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"]}; else if (template.Footprint.Circle) ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]}; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else { ret.obstruction.shape.type = "cluster"; } } if (template.Pack) { ret.pack = { "state": template.Pack.State, "time": func("Pack/Time", +template.Pack.Time, player, template), }; } if (template.Health) ret.health = Math.round(func("Health/Max", +template.Health.Max, player, template)); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.gateConversionTooltip = template.Identity.GateConversionTooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); } if (template.UnitMotion) { ret.speed = { "walk": func("UnitMotion/WalkSpeed", +template.UnitMotion.WalkSpeed, player, template), }; if (template.UnitMotion.Run) ret.speed.run = func("UnitMotion/Run/Speed", +template.UnitMotion.Run.Speed, player, template); } if (template.Trader) ret.trader = template.Trader; if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort, }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap, }; } if (template.WallPiece) ret.wallPiece = {"length": +template.WallPiece.Length}; return ret; } /** * Get information about a technology template. * @param template A valid template as obtained by loading the tech JSON file. * @param civ Civilization for which the specific name should be returned. */ function GetTechnologyDataHelper(template, civ) { var ret = {}; // Get specific name for this civ or else the generic specific name var specific = undefined; if (template.specificName) { if (template.specificName[civ]) specific = template.specificName[civ]; else specific = template.specificName['generic']; } ret.name = { "specific": specific, "generic": template.genericName, }; if (template.icon) ret.icon = "technologies/" + template.icon; else ret.icon = null; ret.cost = { "food": template.cost ? (+template.cost.food) : 0, "wood": template.cost ? (+template.cost.wood) : 0, "metal": template.cost ? (+template.cost.metal) : 0, "stone": template.cost ? (+template.cost.stone) : 0, "time": template.researchTime ? (+template.researchTime) : 0, } ret.tooltip = template.tooltip; if (template.requirementsTooltip) ret.requirementsTooltip = template.requirementsTooltip; else ret.requirementsTooltip = ""; if (template.requirements && template.requirements.class) ret.classRequirements = {"class": template.requirements.class, "number": template.requirements.number}; ret.description = template.description; return ret; } Index: ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js (revision 16550) @@ -1,286 +1,290 @@ /* DESCRIPTION : Generic utility functions. NOTES : */ // ==================================================================== function getRandom(randomMin, randomMax) { // Returns a random whole number in a min..max range. // NOTE: There should probably be an engine function for this, // since we'd need to keep track of random seeds for replays. var randomNum = randomMin + (randomMax-randomMin)*Math.random(); // num is random, from A to B return Math.round(randomNum); } // ==================================================================== // Get list of XML files in pathname with recursion, excepting those starting with _ function getXMLFileList(pathname) { var files = Engine.BuildDirEntList(pathname, "*.xml", true); var result = []; // Get only subpath from filename and discard extension for (var i = 0; i < files.length; ++i) { var file = files[i]; file = file.substring(pathname.length, file.length-4); // Split path into directories so we can check for beginning _ character var tokens = file.split("/"); if (tokens[tokens.length-1][0] != "_") result.push(file); } return result; } // ==================================================================== // Get list of JSON files in pathname function getJSONFileList(pathname) { var files = Engine.BuildDirEntList(pathname, "*.json", false); // Remove the path and extension from each name, since we just want the filename files = [ n.substring(pathname.length, n.length-5) for each (n in files) ]; return files; } // ==================================================================== // A sorting function for arrays of objects with 'name' properties, ignoring case function sortNameIgnoreCase(x, y) { var lowerX = x.name.toLowerCase(); var lowerY = y.name.toLowerCase(); if (lowerX < lowerY) return -1; else if (lowerX > lowerY) return 1; else return 0; } // ==================================================================== /** * Escape tag start and escape characters, so users cannot use special formatting. * Also limit string length to 256 characters (not counting escape characters). */ function escapeText(text) { if (!text) return text; return text.substr(0, 255).replace(/\\/g, "\\\\").replace(/\[/g, "\\["); } // ==================================================================== // Load default player data, for when it's not otherwise specified function initPlayerDefaults() { var data = Engine.ReadJSONFile("simulation/data/player_defaults.json"); if (!data || !data.PlayerData) { error("Failed to parse player defaults in player_defaults.json (check for valid JSON data)"); return []; } return data.PlayerData; } // ==================================================================== // Load map size data function initMapSizes() { var sizes = { "shortNames":[], "names":[], "tiles": [], "default": 0 }; var data = Engine.ReadJSONFile("simulation/data/map_sizes.json"); if (!data || !data.Sizes) { error("Failed to parse map sizes in map_sizes.json (check for valid JSON data)"); return sizes; } translateObjectKeys(data, ["Name", "LongName"]); for (var i = 0; i < data.Sizes.length; ++i) { sizes.shortNames.push(data.Sizes[i].Name); sizes.names.push(data.Sizes[i].LongName); sizes.tiles.push(data.Sizes[i].Tiles); if (data.Sizes[i].Default) sizes["default"] = i; } return sizes; } // ==================================================================== // Load game speed data function initGameSpeeds() { var gameSpeeds = { "names": [], "speeds": [], "default": 0 }; var data = Engine.ReadJSONFile("simulation/data/game_speeds.json"); if (!data || !data.Speeds) { error("Failed to parse game speeds in game_speeds.json (check for valid JSON data)"); return gameSpeeds; } translateObjectKeys(data, ["Name"]); for (var i = 0; i < data.Speeds.length; ++i) { gameSpeeds.names.push(data.Speeds[i].Name); gameSpeeds.speeds.push(data.Speeds[i].Speed); if (data.Speeds[i].Default) gameSpeeds["default"] = i; } return gameSpeeds; } // ==================================================================== // Convert integer color values to string (for use in GUI objects) -function rgbToGuiColor(color) +function rgbToGuiColor(color, alpha) { + var ret; if (color && ("r" in color) && ("g" in color) && ("b" in color)) - return color.r + " " + color.g + " " + color.b; - - return "0 0 0"; + ret = color.r + " " + color.g + " " + color.b; + else + ret = "0 0 0"; + if (alpha) + ret += " " + alpha; + return ret; } // ==================================================================== /** * Convert time in milliseconds to [hh:]mm:ss string representation. * @param time Time period in milliseconds (integer) * @return String representing time period */ function timeToString(time) { if (time < 1000 * 60 * 60) var format = translate("mm:ss"); else var format = translate("HH:mm:ss"); return Engine.FormatMillisecondsIntoDateString(time, format); } // ==================================================================== function removeDupes(array) { // loop backwards to make splice operations cheaper var i = array.length; while (i--) { if (array.indexOf(array[i]) != i) array.splice(i, 1); } } // ==================================================================== // "Inside-out" implementation of Fisher-Yates shuffle function shuffleArray(source) { if (!source.length) return []; var result = [source[0]]; for (var i = 1; i < source.length; ++i) { var j = Math.floor(Math.random() * i); result[i] = result[j]; result[j] = source[i]; } return result; } // ==================================================================== // Filter out conflicting characters and limit the length of a given name. // @param name Name to be filtered. // @param stripUnicode Whether or not to remove unicode characters. // @param stripSpaces Whether or not to remove whitespace. function sanitizePlayerName(name, stripUnicode, stripSpaces) { // We delete the '[', ']' characters (GUI tags) and delete the ',' characters (player name separators) by default. var sanitizedName = name.replace(/[\[\],]/g, ""); // Optionally strip unicode if (stripUnicode) sanitizedName = sanitizedName.replace(/[^\x20-\x7f]/g, ""); // Optionally strip whitespace if (stripSpaces) sanitizedName = sanitizedName.replace(/\s/g, ""); // Limit the length to 20 characters return sanitizedName.substr(0,20); } function tryAutoComplete(text, autoCompleteList) { if (!text.length) return text; var wordSplit = text.split(/\s/g); if (!wordSplit.length) return text; var lastWord = wordSplit.pop(); if (!lastWord.length) return text; for (var word of autoCompleteList) { if (word.toLowerCase().indexOf(lastWord.toLowerCase()) != 0) continue; text = wordSplit.join(" ") if (text.length > 0) text += " "; text += word; break; } return text; } function autoCompleteNick(guiName, playerList) { var input = Engine.GetGUIObjectByName(guiName); var text = input.caption; if (!text.length) return; var autoCompleteList = []; for (var player of playerList) autoCompleteList.push(player.name); var bufferPosition = input.buffer_position; var textTillBufferPosition = text.substring(0, bufferPosition); var newText = tryAutoComplete(textTillBufferPosition, autoCompleteList); input.caption = newText + text.substring(bufferPosition); input.buffer_position = bufferPosition + (newText.length - textTillBufferPosition.length); } Index: ps/trunk/binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/gui/common/tooltips.js (revision 16550) @@ -1,472 +1,482 @@ const COST_DISPLAY_NAMES = { "food": '[icon="iconFood"]', "wood": '[icon="iconWood"]', "stone": '[icon="iconStone"]', "metal": '[icon="iconMetal"]', "population": '[icon="iconPopulation"]', "time": '[icon="iconTime"]' }; const txtFormats = { "unit": ['[font="sans-10"][color="orange"]', '[/color][/font]'], "header": ['[font="sans-bold-13"]', '[/font]'], "body": ['[font="sans-13"]', '[/font]'] }; function damageValues(dmg) { if (!dmg) return [0, 0, 0]; return [dmg.hack || 0, dmg.pierce || 0, dmg.crush || 0]; } function damageTypeDetails(dmg) { if (!dmg) return '[font="sans-12"]' + translate("(None)") + '[/font]'; var dmgArray = []; if (dmg.hack) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { damage: dmg.hack.toFixed(1), damageType: txtFormats.unit[0] + translate("Hack") + txtFormats.unit[1] })); if (dmg.pierce) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { damage: dmg.pierce.toFixed(1), damageType: txtFormats.unit[0] + translate("Pierce") + txtFormats.unit[1] })); if (dmg.crush) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { damage: dmg.crush.toFixed(1), damageType: txtFormats.unit[0] + translate("Crush") + txtFormats.unit[1] })); return dmgArray.join(translate(", ")); } function attackRateDetails(entState, type) { var time = entState.attack[type].repeatTime / 1000; var timeString = sprintf(translate("%(time)s %(second)s"), { time: time, second: txtFormats.unit[0] + translatePlural("second", "seconds", time) + txtFormats.unit[1] }); if (!entState.buildingAI) return timeString; var arrows = Math.max(entState.buildingAI.arrowCount, entState.buildingAI.defaultArrowCount); var arrowString = sprintf(translate("%(arrowcount)s %(arrow)s"), { arrowcount: arrows, arrow: txtFormats.unit[0] + translatePlural("arrow", "arrows", arrows) + txtFormats.unit[1] }); return sprintf(translate("%(arrowString)s / %(timeString)s"), { arrowString: arrowString, timeString: timeString }); } // Converts an armor level into the actual reduction percentage function armorLevelToPercentageString(level) { return (100 - Math.round(Math.pow(0.9, level) * 100)) + "%"; // return sprintf(translate("%(armorPercentage)s%"), { armorPercentage: (100 - Math.round(Math.pow(0.9, level) * 100)) }); // Not supported by our sprintf implementation. } function getArmorTooltip(dmg) { var label = txtFormats.header[0] + translate("Armor:") + txtFormats.header[1]; if (!dmg) return sprintf(translate("%(label)s %(details)s"), { "label": label, "details": '[font="sans-12"]' + translate("(None)") + '[/font]' }); var dmgArray = []; if (dmg.hack) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), { damage: dmg.hack, damageType: txtFormats.unit[0] + translate("Hack") + txtFormats.unit[1], armorPercentage: '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { armorPercentage: armorLevelToPercentageString(dmg.hack) }) + '[/font]' })); if (dmg.pierce) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), { damage: dmg.pierce, damageType: txtFormats.unit[0] + translate("Pierce") + txtFormats.unit[1], armorPercentage: '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { armorPercentage: armorLevelToPercentageString(dmg.pierce) }) + '[/font]' })); if (dmg.crush) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s %(armorPercentage)s"), { damage: dmg.crush, damageType: txtFormats.unit[0] + translate("Crush") + txtFormats.unit[1], armorPercentage: '[font="sans-10"]' + sprintf(translate("(%(armorPercentage)s)"), { armorPercentage: armorLevelToPercentageString(dmg.crush) }) + '[/font]' })); return sprintf(translate("%(label)s %(details)s"), { "label": label, "details": dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]') }); } function damageTypesToText(dmg) { if (!dmg) return '[font="sans-12"]' + translate("(None)") + '[/font]'; var dmgArray = []; if (dmg.hack) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { damage: dmg.hack.toFixed(1), damageType: txtFormats.unit[0] + translate("Hack") + txtFormats.unit[1] })); if (dmg.pierce) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { damage: dmg.pierce.toFixed(1), damageType: txtFormats.unit[0] + translate("Pierce") + txtFormats.unit[1] })); if (dmg.crush) dmgArray.push(sprintf(translate("%(damage)s %(damageType)s"), { damage: dmg.crush.toFixed(1), damageType: txtFormats.unit[0] + translate("Crush") + txtFormats.unit[1] })); return dmgArray.join('[font="sans-12"]' + translate(", ") + '[/font]'); } function getAttackTypeLabel(type) { if (type === "Charge") return translate("Charge Attack:"); if (type === "Melee") return translate("Melee Attack:"); if (type === "Ranged") return translate("Ranged Attack:"); + if (type === "Capture") return translate("Capture Attack:"); warn(sprintf("Internationalization: Unexpected attack type found with code ‘%(attackType)s’. This attack type must be internationalized.", { attackType: type })); return translate("Attack:"); } function getAttackTooltip(template) { var attacks = []; if (!template.attack) return ""; if (template.buildingAI) var rateLabel = txtFormats.header[0] + translate("Interval:") + txtFormats.header[1]; else var rateLabel = txtFormats.header[0] + translate("Rate:") + txtFormats.header[1]; for (var type in template.attack) { if (type == "Slaughter") continue; // Slaughter is not a real attack, so do not show it. if (type == "Charge") continue; // Charging isn't implemented yet and shouldn't be displayed. var rate = sprintf(translate("%(label)s %(details)s"), { label: rateLabel, details: attackRateDetails(template, type) }); var attackLabel = txtFormats.header[0] + getAttackTypeLabel(type) + txtFormats.header[1]; + if (type == "Capture") + { + attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), { + attackLabel: attackLabel, + details: template.attack[type].value, + rate: rate + })); + continue; + } if (type != "Ranged") { attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), { attackLabel: attackLabel, details: damageTypesToText(template.attack[type]), rate: rate })); continue; } var realRange = template.attack[type].elevationAdaptedRange; var range = Math.round(template.attack[type].maxRange); var rangeLabel = txtFormats.header[0] + translate("Range:") + txtFormats.header[1]; var relativeRange = Math.round((realRange - range)); var meters = txtFormats.unit[0] + translate("meters") + txtFormats.unit[1]; if (relativeRange) // show if it is non-zero attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rangeLabel)s %(range)s %(meters)s (%(relative)s), %(rate)s"), { attackLabel: attackLabel, details: damageTypesToText(template.attack[type]), rangeLabel: rangeLabel, range: range, meters: meters, relative: relativeRange > 0 ? "+" + relativeRange : relativeRange, rate: rate })); else attacks.push(sprintf(translate("%(attackLabel)s %(damageTypes)s, %(rangeLabel)s %(range)s %(meters)s, %(rate)s"), { attackLabel: attackLabel, damageTypes: damageTypesToText(template.attack[type]), rangeLabel: rangeLabel, range: range, meters: meters, rate: rate })); } - return attacks.join(translate(", ")); + return attacks.join("\n"); } /** * Translates a cost component identifier as they are used internally * (e.g. "population", "food", etc.) to proper display names. */ function getCostComponentDisplayName(costComponentName) { if (costComponentName in COST_DISPLAY_NAMES) return COST_DISPLAY_NAMES[costComponentName]; warn(sprintf("The specified cost component, ‘%(component)s’, is not currently supported.", { component: costComponentName })); return ""; } /** * Multiplies the costs for a template by a given batch size. */ function multiplyEntityCosts(template, trainNum) { var totalCosts = {}; for (var r in template.cost) totalCosts[r] = Math.floor(template.cost[r] * trainNum); return totalCosts; } /** * Helper function for getEntityCostTooltip. */ function getEntityCostComponentsTooltipString(template, trainNum, entity) { if (!trainNum) trainNum = 1; var totalCosts = multiplyEntityCosts(template, trainNum); totalCosts.time = Math.ceil(template.cost.time * (entity ? Engine.GuiInterfaceCall("GetBatchTime", {"entity": entity, "batchSize": trainNum}) : 1)); var costs = []; if (totalCosts.food) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("food"), cost: totalCosts.food })); if (totalCosts.wood) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("wood"), cost: totalCosts.wood })); if (totalCosts.metal) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("metal"), cost: totalCosts.metal })); if (totalCosts.stone) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("stone"), cost: totalCosts.stone })); if (totalCosts.population) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("population"), cost: totalCosts.population })); if (totalCosts.time) costs.push(sprintf(translate("%(component)s %(cost)s"), { component: getCostComponentDisplayName("time"), cost: totalCosts.time })); return costs; } /** * Returns an array of strings for a set of wall pieces. If the pieces share * resource type requirements, output will be of the form '10 to 30 Stone', * otherwise output will be, e.g. '10 Stone, 20 Stone, 30 Stone'. */ function getWallPieceTooltip(wallTypes) { var out = []; var resourceCount = {}; // Initialize the acceptable types for '$x to $y $resource' mode. for (var resource in wallTypes[0].cost) if (wallTypes[0].cost[resource]) resourceCount[resource] = [wallTypes[0].cost[resource]]; var sameTypes = true; for (var i = 1; i < wallTypes.length; ++i) { for (var resource in wallTypes[i].cost) { // Break out of the same-type mode if this wall requires // resource types that the first didn't. if (wallTypes[i].cost[resource] && !resourceCount[resource]) { sameTypes = false; break; } } for (var resource in resourceCount) { if (wallTypes[i].cost[resource]) resourceCount[resource].push(wallTypes[i].cost[resource]); else { sameTypes = false; break; } } } if (sameTypes) { for (var resource in resourceCount) { var resourceMin = Math.min.apply(Math, resourceCount[resource]); var resourceMax = Math.max.apply(Math, resourceCount[resource]); // Translation: This string is part of the resources cost string on // the tooltip for wall structures. out.push(sprintf(translate("%(resourceIcon)s %(minimum)s to %(resourceIcon)s %(maximum)s"), { resourceIcon: getCostComponentDisplayName(resource), minimum: resourceMin, maximum: resourceMax })); } } else for (var i = 0; i < wallTypes.length; ++i) out.push(getEntityCostComponentsTooltipString(wallTypes[i]).join(", ")); return out; } /** * Returns the cost information to display in the specified entity's construction button tooltip. */ function getEntityCostTooltip(template, trainNum, entity) { // Entities with a wallset component are proxies for initiating wall placement and as such do not have a cost of // their own; the individual wall pieces within it do. if (template.wallSet) { var templateLong = GetTemplateData(template.wallSet.templates.long); var templateMedium = GetTemplateData(template.wallSet.templates.medium); var templateShort = GetTemplateData(template.wallSet.templates.short); var templateTower = GetTemplateData(template.wallSet.templates.tower); var wallCosts = getWallPieceTooltip([templateShort, templateMedium, templateLong]); var towerCosts = getEntityCostComponentsTooltipString(templateTower); return sprintf(translate("Walls: %(costs)s"), { costs: wallCosts.join(" ") }) + "\n" + sprintf(translate("Towers: %(costs)s"), { costs: towerCosts.join(" ") }); } if (template.cost) return getEntityCostComponentsTooltipString(template, trainNum, entity).join(" "); return ""; } /** * Returns the population bonus information to display in the specified entity's construction button tooltip. */ function getPopulationBonusTooltip(template) { var popBonus = ""; if (template.cost && template.cost.populationBonus) popBonus = "\n" + sprintf(translate("%(label)s %(populationBonus)s"), { label: txtFormats.header[0] + translate("Population Bonus:") + txtFormats.header[1], populationBonus: template.cost.populationBonus }); return popBonus; } /** * Returns a message with the amount of each resource needed to create an entity. */ function getNeededResourcesTooltip(resources) { var formatted = []; for (var resource in resources) formatted.push(sprintf(translate("%(component)s %(cost)s"), { component: '[font="sans-12"]' + getCostComponentDisplayName(resource) + '[/font]', cost: resources[resource] })); return '\n\n[font="sans-bold-13"][color="red"]' + translate("Insufficient resources:") + '[/color][/font]\n' + formatted.join(" "); } function getSpeedTooltip(template) { if (!template.speed) return ""; var label = txtFormats.header[0] + translate("Speed:") + txtFormats.header[1]; var speeds = []; if (template.speed.walk) speeds.push(sprintf(translate("%(speed)s %(movementType)s"), { speed: template.speed.walk, movementType: txtFormats.unit[0] + translate("Walk") + txtFormats.unit[1]})); if (template.speed.run) speeds.push(sprintf(translate("%(speed)s %(movementType)s"), { speed: template.speed.run, movementType: txtFormats.unit[0] + translate("Run") + txtFormats.unit[1]})); return sprintf(translate("%(label)s %(speeds)s"), { label: label, speeds: speeds.join(translate(", ")) }); } function getHealerTooltip(template) { if (!template.healer) return ""; var healer = [ sprintf(translate("%(label)s %(val)s %(unit)s"), { label: txtFormats.header[0] + translate("Heal:") + txtFormats.header[1], val: template.healer.HP, // Translation: Short for Health Points (that are healed in one healing action) unit: txtFormats.unit[0] + translate("HP") + txtFormats.unit[1] }), sprintf(translate("%(label)s %(val)s %(unit)s"), { label: txtFormats.header[0] + translate("Range:") + txtFormats.header[1], val: template.healer.Range, unit: txtFormats.unit[0] + translate("meters") + txtFormats.unit[1] }), sprintf(translate("%(label)s %(val)s %(unit)s"), { label: txtFormats.header[0] + translate("Rate:") + txtFormats.header[1], val: template.healer.Rate/1000, unit: txtFormats.unit[0] + translatePlural("second", "seconds", template.healer.Rate/1000) + txtFormats.unit[1] }) ]; return healer.join(translate(", ")); } function getEntityNames(template) { if (template.name.specific) { if (template.name.generic && template.name.specific != template.name.generic) return sprintf(translate("%(specificName)s (%(genericName)s)"), { specificName: template.name.specific, genericName: template.name.generic }); return template.name.specific; } if (template.name.generic) return template.name.generic; warn("Entity name requested on an entity without a name, specific or generic."); return translate("???"); } function getEntityNamesFormatted(template) { var names = ""; var generic = template.name.generic; var specific = template.name.specific; if (specific) { // drop caps for specific name names += '[font="sans-bold-16"]' + specific[0] + '[/font]' + '[font="sans-bold-12"]' + specific.slice(1).toUpperCase() + '[/font]'; if (generic) names += '[font="sans-bold-16"] (' + generic + ')[/font]'; } else if (generic) names = '[font="sans-bold-16"]' + generic + "[/font]"; else names = "???"; return names; } function getVisibleEntityClassesFormatted(template) { var r = "" if (template.visibleIdentityClasses && template.visibleIdentityClasses.length) { r += '\n' + txtFormats.header[0] + translate("Classes:") + txtFormats.header[1]; var classes = []; for (var c of template.visibleIdentityClasses) classes.push(translate(c)); r += ' ' + txtFormats.body[0] + classes.join(translate(", ")) + txtFormats.body[1]; } return r; } Index: ps/trunk/binaries/data/mods/public/gui/session/selection_details.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_details.js (revision 16550) @@ -1,386 +1,434 @@ function layoutSelectionSingle() { Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false; Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; } function layoutSelectionMultiple() { Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; } function getResourceTypeDisplayName(resourceType) { var resourceCode = resourceType["generic"]; var displayName = ""; if (resourceCode == "treasure") displayName = getLocalizedResourceName(resourceType["specific"], "firstWord"); else displayName = getLocalizedResourceName(resourceCode, "firstWord"); return displayName; } // Fills out information that most entities have function displaySingle(entState, template) { // Get general unit and player data var specificName = template.name.specific; var genericName = template.name.generic != template.name.specific ? template.name.generic : ""; // If packed, add that to the generic name (reduces template clutter) if (genericName && template.pack && template.pack.state == "packed") genericName = sprintf(translate("%(genericName)s — Packed"), { genericName: genericName }); var playerState = g_Players[entState.player]; var civName = g_CivData[playerState.civ].Name; var civEmblem = g_CivData[playerState.civ].Emblem; var playerName = playerState.name; var playerColor = playerState.color.r + " " + playerState.color.g + " " + playerState.color.b + " 128"; // Indicate disconnected players by prefixing their name if (g_Players[entState.player].offline) { playerName = sprintf(translate("\\[OFFLINE] %(player)s"), { player: playerName }); } // Rank if (entState.identity && entState.identity.rank && entState.identity.classes) { Engine.GetGUIObjectByName("rankIcon").tooltip = sprintf(translate("%(rank)s Rank"), { rank: translateWithContext("Rank", entState.identity.rank) }); Engine.GetGUIObjectByName("rankIcon").sprite = getRankIconSprite(entState); Engine.GetGUIObjectByName("rankIcon").hidden = false; } else { Engine.GetGUIObjectByName("rankIcon").hidden = true; Engine.GetGUIObjectByName("rankIcon").tooltip = ""; } // Hitpoints + Engine.GetGUIObjectByName("healthSection").hidden = !entState.hitpoints; if (entState.hitpoints) { var unitHealthBar = Engine.GetGUIObjectByName("healthBar"); var healthSize = unitHealthBar.size; healthSize.rright = 100*Math.max(0, Math.min(1, entState.hitpoints / entState.maxHitpoints)); unitHealthBar.size = healthSize; if (entState.foundation && entState.visibility == "visible" && entState.foundation.numBuilders !== 0) { // logic comes from Foundation component. var speed = Math.pow(entState.foundation.numBuilders, 0.7); var timeLeft = (1.0 - entState.foundation.progress / 100.0) * template.cost.time; Engine.GetGUIObjectByName("health").tooltip = sprintf(translate("This foundation will be completed in %(numb)s seconds."), { numb : Math.ceil(timeLeft/speed) }); } else Engine.GetGUIObjectByName("health").tooltip = ""; Engine.GetGUIObjectByName("healthStats").caption = sprintf(translate("%(hitpoints)s / %(maxHitpoints)s"), { hitpoints: Math.ceil(entState.hitpoints), maxHitpoints: entState.maxHitpoints }); - Engine.GetGUIObjectByName("healthSection").hidden = false; } - else + + // CapturePoints + Engine.GetGUIObjectByName("captureSection").hidden = !entState.capturePoints; + if (entState.capturePoints) { - Engine.GetGUIObjectByName("healthSection").hidden = true; + let setCaptureBarPart = function(playerID, startSize) + { + var unitCaptureBar = Engine.GetGUIObjectByName("captureBar["+playerID+"]"); + var sizeObj = unitCaptureBar.size; + sizeObj.rleft = startSize; + + var size = 100*Math.max(0, Math.min(1, entState.capturePoints[playerID] / entState.maxCapturePoints)); + sizeObj.rright = startSize + size; + unitCaptureBar.size = sizeObj; + unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[playerID].color, 128); + unitCaptureBar.hidden=false; + return startSize + size; + } + + // first handle the owner's points, to keep those points on the left for clarity + let size = setCaptureBarPart(entState.player, 0); + + for (let i in entState.capturePoints) + if (i != entState.player) + size = setCaptureBarPart(i, size); + + + Engine.GetGUIObjectByName("captureStats").caption = sprintf(translate("%(capturePoints)s / %(maxCapturePoints)s"), { + capturePoints: Math.ceil(entState.capturePoints[entState.player]), + maxCapturePoints: entState.maxCapturePoints + }); } // TODO: Stamina - var player = Engine.GetPlayerID(); - if (entState.stamina && (entState.player == player || g_DevSettings.controlAll)) - Engine.GetGUIObjectByName("staminaSection").hidden = false; - else - Engine.GetGUIObjectByName("staminaSection").hidden = true; // Experience + Engine.GetGUIObjectByName("experience").hidden = !entState.promotion; if (entState.promotion) { var experienceBar = Engine.GetGUIObjectByName("experienceBar"); var experienceSize = experienceBar.size; experienceSize.rtop = 100 - (100 * Math.max(0, Math.min(1, 1.0 * +entState.promotion.curr / +entState.promotion.req))); experienceBar.size = experienceSize; if (entState.promotion.curr < entState.promotion.req) Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s / %(required)s"), { experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]", current: Math.floor(entState.promotion.curr), required: entState.promotion.req }); else Engine.GetGUIObjectByName("experience").tooltip = sprintf(translate("%(experience)s %(current)s"), { experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]", current: Math.floor(entState.promotion.curr) }); - Engine.GetGUIObjectByName("experience").hidden = false; - } - else - { - Engine.GetGUIObjectByName("experience").hidden = true; } // Resource stats + Engine.GetGUIObjectByName("resourceSection").hidden = !entState.resourceSupply; if (entState.resourceSupply) { var resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol sprintf(translate("%(amount)s / %(max)s"), { amount: Math.ceil(+entState.resourceSupply.amount), max: entState.resourceSupply.max }); var resourceType = getResourceTypeDisplayName(entState.resourceSupply.type); var unitResourceBar = Engine.GetGUIObjectByName("resourceBar"); var resourceSize = unitResourceBar.size; resourceSize.rright = entState.resourceSupply.isInfinite ? 100 : 100 * Math.max(0, Math.min(1, +entState.resourceSupply.amount / +entState.resourceSupply.max)); unitResourceBar.size = resourceSize; Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), { resource: resourceType }); Engine.GetGUIObjectByName("resourceStats").caption = resources; if (entState.hitpoints) - Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("staminaSection").size; + Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("captureSection").size; else Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("healthSection").size; - Engine.GetGUIObjectByName("resourceSection").hidden = false; - } - else - { - Engine.GetGUIObjectByName("resourceSection").hidden = true; } // Resource carrying if (entState.resourceCarrying && entState.resourceCarrying.length) { // We should only be carrying one resource type at once, so just display the first var carried = entState.resourceCarrying[0]; Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+carried.type+".png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: carried.amount, max: carried.max }); Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = ""; } // Use the same indicators for traders else if (entState.trader && entState.trader.goods.amount) { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/resources/"+entState.trader.goods.type+".png"; var totalGain = entState.trader.goods.amount.traderGain; if (entState.trader.goods.amount.market1Gain) totalGain += entState.trader.goods.amount.market1Gain; if (entState.trader.goods.amount.market2Gain) totalGain += entState.trader.goods.amount.market2Gain; Engine.GetGUIObjectByName("resourceCarryingText").caption = totalGain; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Gain: %(amount)s"), { amount: getTradingTooltip(entState.trader.goods.amount) }); } // And for number of workers else if (entState.foundation && entState.visibility == "visible") { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " "; if (entState.foundation.numBuilders !== 0) { var speedup = Math.pow((entState.foundation.numBuilders+1)/entState.foundation.numBuilders, 0.7); var timeLeft = (1.0 - entState.foundation.progress / 100.0) * template.cost.time; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Number of builders.\nTasking another to this foundation would speed construction up by %(numb)s seconds."), { numb : Math.ceil(timeLeft - timeLeft/speedup) }); } else { Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Number of builders."); } } else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints) && entState.visibility == "visible") { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; Engine.GetGUIObjectByName("resourceCarryingIcon").sprite = "stretched:session/icons/repair.png"; Engine.GetGUIObjectByName("resourceCarryingText").caption = sprintf(translate("%(amount)s / %(max)s"), { amount: entState.resourceSupply.gatherers.length, max: entState.resourceSupply.maxGatherers }) + " "; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = translate("Current/max gatherers"); } else { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = true; Engine.GetGUIObjectByName("resourceCarryingText").hidden = true; } // Set Player details Engine.GetGUIObjectByName("specific").caption = specificName; Engine.GetGUIObjectByName("player").caption = playerName; Engine.GetGUIObjectByName("playerColorBackground").sprite = "color: " + playerColor; if (genericName) { Engine.GetGUIObjectByName("generic").caption = sprintf(translate("(%(genericName)s)"), { genericName: genericName }); } else { Engine.GetGUIObjectByName("generic").caption = ""; } if ("gaia" != playerState.civ) { Engine.GetGUIObjectByName("playerCivIcon").sprite = "stretched:grayscale:" + civEmblem; Engine.GetGUIObjectByName("player").tooltip = civName; } else { Engine.GetGUIObjectByName("playerCivIcon").sprite = ""; Engine.GetGUIObjectByName("player").tooltip = ""; } // Icon image if (template.icon) { Engine.GetGUIObjectByName("icon").sprite = "stretched:session/portraits/" + template.icon; } else { // TODO: we should require all entities to have icons, so this case never occurs Engine.GetGUIObjectByName("icon").sprite = "bkFillBlack"; } var armorString = getArmorTooltip(entState.armour); // Attack and Armor if ("attack" in entState && entState.attack) Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = getAttackTooltip(entState) + "\n" + armorString; else Engine.GetGUIObjectByName("attackAndArmorStats").tooltip = armorString; // Icon Tooltip var iconTooltip = ""; if (genericName) iconTooltip = "[font=\"sans-bold-16\"]" + genericName + "[/font]"; if (template.visibleIdentityClasses && template.visibleIdentityClasses.length) { iconTooltip += "\n[font=\"sans-bold-13\"]" + translate("Classes:") + "[/font] "; iconTooltip += "[font=\"sans-13\"]" + translate(template.visibleIdentityClasses[0]) ; for (var i = 1; i < template.visibleIdentityClasses.length; i++) iconTooltip += ", " + translate(template.visibleIdentityClasses[i]); iconTooltip += "[/font]"; } if (template.auras) { for (var auraName in template.auras) { iconTooltip += "\n[font=\"sans-bold-13\"]" + translate(auraName) + "[/font]"; if (template.auras[auraName]) iconTooltip += ": " + translate(template.auras[auraName]); } } if (template.tooltip) iconTooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]"; Engine.GetGUIObjectByName("iconBorder").tooltip = iconTooltip; // Unhide Details Area Engine.GetGUIObjectByName("detailsAreaSingle").hidden = false; Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; } // Fills out information for multiple entities function displayMultiple(selection, template) { var averageHealth = 0; var maxHealth = 0; + var maxCapturePoints = 0; + var capturePoints = (new Array(9)).fill(0); + var playerID = 0; for (var i = 0; i < selection.length; i++) { var entState = GetEntityState(selection[i]) - if (entState) + if (!entState) + continue; + playerID = entState.player; // trust that all selected entities have the same owner + if (entState.hitpoints) + { + averageHealth += entState.hitpoints; + maxHealth += entState.maxHitpoints; + } + if (entState.capturePoints) { - if (entState.hitpoints) - { - averageHealth += entState.hitpoints; - maxHealth += entState.maxHitpoints; - } + maxCapturePoints += entState.maxCapturePoints; + capturePoints = entState.capturePoints.map(function(v, i) { return v + capturePoints[i]; }); } } + Engine.GetGUIObjectByName("healthMultiple").hidden = averageHealth <= 0; if (averageHealth > 0) { var unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple"); var healthSize = unitHealthBar.size; healthSize.rtop = 100-100*Math.max(0, Math.min(1, averageHealth / maxHealth)); unitHealthBar.size = healthSize; var hitpointsLabel = "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]" var hitpoints = sprintf(translate("%(label)s %(current)s / %(max)s"), { label: hitpointsLabel, current: averageHealth, max: maxHealth }); - var healthMultiple = Engine.GetGUIObjectByName("healthMultiple"); - healthMultiple.tooltip = hitpoints; - healthMultiple.hidden = false; + Engine.GetGUIObjectByName("healthMultiple").tooltip = hitpoints; } - else + + Engine.GetGUIObjectByName("captureMultiple").hidden = maxCapturePoints <= 0; + if (maxCapturePoints > 0) { - Engine.GetGUIObjectByName("healthMultiple").hidden = true; + let setCaptureBarPart = function(playerID, startSize) + { + var unitCaptureBar = Engine.GetGUIObjectByName("captureBarMultiple["+playerID+"]"); + var sizeObj = unitCaptureBar.size; + sizeObj.rtop = startSize; + + var size = 100*Math.max(0, Math.min(1, capturePoints[playerID] / maxCapturePoints)); + sizeObj.rbottom = startSize + size; + unitCaptureBar.size = sizeObj; + unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[playerID].color, 128); + unitCaptureBar.hidden=false; + return startSize + size; + } + + let size = 0; + for (let i in entState.capturePoints) + if (i != playerID) + size = setCaptureBarPart(i, size); + + // last handle the owner's points, to keep those points on the bottom for clarity + setCaptureBarPart(playerID, size); + + var capturePointsLabel = "[font=\"sans-bold-13\"]" + translate("Capture points:") + "[/font]" + var capturePointsTooltip = sprintf(translate("%(label)s %(current)s / %(max)s"), { label: capturePointsLabel, current: Math.ceil(capturePoints[playerID]), max: Math.ceil(maxCapturePoints) }); + Engine.GetGUIObjectByName("captureMultiple").tooltip = capturePointsTooltip; } // TODO: Stamina // Engine.GetGUIObjectByName("staminaBarMultiple"); Engine.GetGUIObjectByName("numberOfUnits").caption = selection.length; // Unhide Details Area Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = false; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; } // Updates middle entity Selection Details Panel function updateSelectionDetails() { var supplementalDetailsPanel = Engine.GetGUIObjectByName("supplementalSelectionDetails"); var detailsPanel = Engine.GetGUIObjectByName("selectionDetails"); var commandsPanel = Engine.GetGUIObjectByName("unitCommands"); g_Selection.update(); var selection = g_Selection.toList(); if (selection.length == 0) { Engine.GetGUIObjectByName("detailsAreaMultiple").hidden = true; Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true; hideUnitCommands(); supplementalDetailsPanel.hidden = true; detailsPanel.hidden = true; commandsPanel.hidden = true; return; } /* If the unit has no data (e.g. it was killed), don't try displaying any data for it. (TODO: it should probably be removed from the selection too; also need to handle multi-unit selections) */ var entState = GetExtendedEntityState(selection[0]); if (!entState) return; var template = GetTemplateData(entState.template); // Fill out general info and display it if (selection.length == 1) displaySingle(entState, template); else displayMultiple(selection, template); // Show basic details. detailsPanel.hidden = false; if (g_IsObserver) { // Observers don't need these displayed. supplementalDetailsPanel.hidden = true; commandsPanel.hidden = true; } else { // Fill out commands panel for specific unit selected (or first unit of primary group) updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection); } } Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/multiple_details_area.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/multiple_details_area.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/multiple_details_area.xml (revision 16550) @@ -1,46 +1,47 @@ Hitpoints - - - Stamina + + + Capture points - - + + Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/single_details_area.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/single_details_area.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_middle/single_details_area.xml (revision 16550) @@ -1,108 +1,109 @@ Health: - - - - Stamina: + + + + Capture points: - - + + - - + + Attack and Armor Experience Rank Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 16550) @@ -1,904 +1,909 @@ /** * List of different actions units can execute, * this is mostly used to determine which actions can be executed * * "execute" is meant to send the command to the engine * * The next functions will always return false * in case you have to continue to seek * (i.e. look at the next entity for getActionInfo, the next * possible action for the actionCheck ...) * They will return an object when the searching is finished * * "getActionInfo" is used to determine if the action is possible, * and also give visual feedback to the user (tooltips, cursors, ...) * * "preSelectedActionCheck" is used to select actions when the gui buttons * were used to set them, but still require a target (like the guard button) * * "hotkeyActionCheck" is used to check the possibility of actions when * a hotkey is pressed * * "actionCheck" is used to check the possibilty of actions without specific * command. For that, the specificness variable is used * * "specificness" is used to determine how specific an action is, * The lower the number, the more specific an action is, and the bigger * the chance of selecting that action when multiple actions are possible */ var unitActions = { "move": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { return {"possible": true}; }, "actionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); if (haveUnitAI && getActionInfo("move", target).possible) return {"type": "move"}; return false; }, "specificness": 12, }, "attack-move": { "execute": function(target, action, selection, queued) { if (Engine.HotkeyIsPressed("session.attackmoveUnit")) var targetClasses = { "attack": ["Unit"] }; else var targetClasses = { "attack": ["Unit", "Structure"] }; Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "targetClasses": targetClasses, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { return {"possible": true}; }, "hotkeyActionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); if (haveUnitAI && Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible) return {"type": "attack-move", "cursor": "action-attack-move"}; return false; }, "specificness": 30, }, "attack": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.attack || !targetState.hitpoints) return false; - if (playerCheck(entState, targetState, ["Neutral", "Enemy"])) - return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})}; - return false; + return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})}; }, "hotkeyActionCheck": function(target) { if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; return false }, "actionCheck": function(target) { if (getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; return false; }, "specificness": 10, }, "heal": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!entState.healer) return false; if (!hasClass(targetState, "Unit") || !targetState.needsHeal) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; // Healers can't heal themselves. if (entState.id == targetState.id) return false; var unhealableClasses = entState.healer.unhealableClasses; if (MatchesClassList(targetState.identity.classes, unhealableClasses)) return false; var healableClasses = entState.healer.healableClasses; if (!MatchesClassList(targetState.identity.classes, healableClasses)) return false; return {"possible": true}; }, "actionCheck": function(target) { if (getActionInfo("heal", target).possible) return {"type": "heal", "cursor": "action-heal", "target": target}; return false; }, "specificness": 7, }, "build": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (targetState.foundation && entState.builder && playerCheck(entState, targetState, ["Player", "Ally"])) return {"possible": true}; return false; }, "actionCheck": function(target) { if (getActionInfo("build", target).possible) return {"type": "build", "cursor": "action-build", "target": target}; return false; }, "specificness": 3, }, "repair": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (entState.builder && targetState.needsRepair && playerCheck(entState, targetState, ["Player", "Ally"])) return {"possible": true}; return false; }, "preSelectedActionCheck" : function(target) { if (preSelectedAction != ACTION_REPAIR) return false; if (getActionInfo("repair", target).possible) return {"type": "repair", "cursor": "action-repair", "target": target}; return {"type": "none", "cursor": "action-repair-disabled", "target": undefined}; }, "actionCheck": function(target) { if (getActionInfo("repair", target).possible) return {"type": "build", "cursor": "action-repair", "target": target}; return false; }, "specificness": 4, }, "gather": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState.resourceSupply) return false; var resource = findGatherType(entState, targetState.resourceSupply); if (resource) return {"possible": true, "cursor": "action-gather-" + resource}; return false; }, "actionCheck": function(target) { var actionInfo = getActionInfo("gather", target); if (!actionInfo.possible) return false; return {"type": "gather", "cursor": actionInfo.cursor, "target": target}; }, "specificness": 1, }, "returnresource": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState.resourceDropsite) return false; if (!playerCheck(entState, targetState, ["Player"])) return false; if (!entState.resourceCarrying || !entState.resourceCarrying.length) return false; var carriedType = entState.resourceCarrying[0].type; if (targetState.resourceDropsite.types.indexOf(carriedType) == -1) return false; return {"possible": true, "cursor": "action-return-" + carriedType}; }, "actionCheck": function(target) { var actionInfo = getActionInfo("returnresource", target); if (!actionInfo.possible) return false; return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target}; }, "specificness": 2, }, "setup-trade-route": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target, "source": undefined, "route": undefined, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (targetState.foundation || !entState.trader) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; if (!(hasClass(entState, "Organic") && hasClass(targetState, "Market")) && !(hasClass(entState, "Ship") && hasClass(targetState, "NavalMarket"))) return false; var tradingData = {"trader": entState.id, "target": targetState.id}; var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData); if (!tradingDetails) return false; var tooltip; switch (tradingDetails.type) { case "is first": tooltip = translate("Origin trade market."); if (tradingDetails.hasBothMarkets) tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(tradingDetails.gain) }); else tooltip += "\n" + translate("Right-click on another market to set it as a destination trade market.") break; case "is second": tooltip = translate("Destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(tradingDetails.gain) }); break; case "set first": tooltip = translate("Right-click to set as origin trade market"); break; case "set second": if (tradingDetails.gain.traderGain == 0) // markets too close return false; tooltip = translate("Right-click to set as destination trade market.") + "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(tradingDetails.gain) }); break; } return {"possible": true, "tooltip": tooltip}; }, "actionCheck": function(target) { var actionInfo = getActionInfo("setup-trade-route", target); if (!actionInfo.possible) return false; return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target}; }, "specificness": 0, }, "garrison": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!hasClass(entState, "Unit") || !targetState.garrisonHolder) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; var tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount, capacity: targetState.garrisonHolder.capacity }); var extraCount = 0; if (entState.garrisonHolder) extraCount += entState.garrisonHolder.garrisonedEntitiesCount; if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; if (MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) return {"possible": true, "tooltip": tooltip}; return false; }, "preSelectedActionCheck": function(target) { if (preSelectedAction != ACTION_GARRISON) return false; var actionInfo = getActionInfo("garrison", target); if (actionInfo.possible) return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target}; return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined}; }, "hotkeyActionCheck": function(target) { var actionInfo = getActionInfo("garrison", target); if (Engine.HotkeyIsPressed("session.garrison") && actionInfo.possible) return {"type": "garrison", "cursor": "action-garrison", "tooltip": actionInfo.tooltip, "target": target}; return false; }, "specificness": 20, }, "guard": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] }); return true; }, "getActionInfo": function(entState, targetState) { if (!targetState.guard) return false; if (!playerCheck(entState, targetState, ["Player", "Ally"])) return false; if (!entState.unitAI || !entState.unitAI.canGuard) return false; if (targetState.unitAI && targetState.unitAI.isGuarding) return false; return {"possible": true}; }, "preSelectedActionCheck" : function(target) { if (preSelectedAction != ACTION_GUARD) return false; if (getActionInfo("guard", target).possible) return {"type": "guard", "cursor": "action-guard", "target": target}; return {"type": "none", "cursor": "action-guard-disabled", "target": undefined}; }, "hotkeyActionCheck": function(target) { if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible) return {"type": "guard", "cursor": "action-guard", "target": target}; return false; }, "specificness": 40, }, "remove-guard": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "remove-guard", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] }); return true; }, "hotkeyActionCheck": function(target, selection) { if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("remove-guard", target).possible) { var isGuarding = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI && entState.unitAI.isGuarding; }); if (isGuarding) return {"type": "remove-guard", "cursor": "action-remove-guard"}; } return false; }, "specificness": 41, }, "set-rallypoint": { "execute": function(target, action, selection, queued) { // if there is a position set in the action then use this so that when setting a // rally point on an entity it is centered on that entity if (action.position) target = action.position; Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z, "data": action.data, "queued": queued}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z, "queued": queued }); return true; }, "getActionInfo": function(entState, targetState) { var tooltip; // default to walking there (or attack-walking if hotkey pressed) var data = {command: "walk"}; var cursor = ""; if (Engine.HotkeyIsPressed("session.attackmove")) { if (Engine.HotkeyIsPressed("session.attackmoveUnit")) var targetClasses = { "attack": ["Unit"] }; else var targetClasses = { "attack": ["Unit", "Structure"] }; data.command = "attack-walk"; data.targetClasses = targetClasses; cursor = "action-attack-move"; } if (targetState.garrisonHolder && playerCheck(entState, targetState, ["Player", "Ally"])) { data.command = "garrison"; data.target = targetState.id; cursor = "action-garrison"; tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { garrisoned: targetState.garrisonHolder.garrisonedEntitiesCount, capacity: targetState.garrisonHolder.capacity }); if (targetState.garrisonHolder.garrisonedEntitiesCount >= targetState.garrisonHolder.capacity) tooltip = "[color=\"orange\"]" + tooltip + "[/color]"; } else if (targetState.resourceSupply) { var resourceType = targetState.resourceSupply.type; if (resourceType.generic == "treasure") cursor = "action-gather-" + resourceType.generic; else cursor = "action-gather-" + resourceType.specific; data.command = "gather"; data.resourceType = resourceType; data.resourceTemplate = targetState.template; } else if (targetState.foundation && entState.builder) { data.command = "build"; data.target = targetState.id; cursor = "action-build"; } else if (hasClass(entState, "Market") && hasClass(targetState, "Market") && entState.id != targetState.id && (!hasClass(entState, "NavalMarket") || hasClass(targetState, "NavalMarket")) && !playerCheck(entState, targetState, ["Enemy"])) { // Find a trader (if any) that this building can produce. var trader; if (entState.production && entState.production.entities.length) for (var i = 0; i < entState.production.entities.length; ++i) if ((trader = GetTemplateData(entState.production.entities[i]).trader)) break; var traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader }; var gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData); if (gain && gain.traderGain) { data.command = "trade"; data.target = traderData.secondMarket; data.source = traderData.firstMarket; cursor = "action-setup-trade-route"; tooltip = translate("Right-click to establish a default route for new traders."); if (trader) tooltip += "\n" + sprintf(translate("Gain: %(gain)s"), { gain: getTradingTooltip(gain) }); else // Foundation or cannot produce traders tooltip += "\n" + sprintf(translate("Expected gain: %(gain)s"), { gain: getTradingTooltip(gain) }); } } else if (targetState.needsRepair && playerCheck(entState, targetState, ["Ally"])) { data.command = "repair"; data.target = targetState.id; cursor = "action-repair"; } // Don't allow the rally point to be set on any of the currently selected entities (used for unset) // except if the autorallypoint hotkey is pressed and the target can produce entities var selection = g_Selection.toList(); if (!Engine.HotkeyIsPressed("session.autorallypoint") || !targetState.production || !targetState.production.entities.length) { for (var i = 0; i < selection.length; i++) if (targetState.id === selection[i]) return false; } return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor, "tooltip": tooltip}; }, "actionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); if (haveUnitAI) return false; // Work out whether at least part the selection have rally points // while none have UnitAI var haveRallyPoints = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && ("rallyPoint" in entState) && entState.rallyPoint; }); if (!haveRallyPoints) return false; var actionInfo = getActionInfo("set-rallypoint", target); if (!actionInfo.possible) return false; return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position}; }, "specificness": 6, }, "unset-rallypoint": { "execute": function(target, action, selection, queued) { Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection}); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; }, "getActionInfo": function(entState, targetState) { if (entState.id != targetState.id) return false; if (!entState.rallyPoint || !entState.rallyPoint.position) return false; return {"possible": true}; }, "actionCheck": function(target, selection) { // Work out whether at least part of the selection have UnitAI var haveUnitAI = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && entState.unitAI; }); // Work out whether at least part the selection have rally points // while none have UnitAI var haveRallyPoints = selection.some(function(ent) { var entState = GetEntityState(ent); return entState && ("rallyPoint" in entState) && entState.rallyPoint; }); if (!haveUnitAI && haveRallyPoints && getActionInfo("unset-rallypoint", target).possible) return {"type": "unset-rallypoint", "cursor": "action-unset-rally"}; return false; }, "specificness": 11, }, "none": { "execute": function(target, action, selection, queued) { return true; }, "specificness": 100, }, }; /** * Info and actions for the entity commands * Currently displayed in the bottom of the central pane */ var g_EntityCommands = { // Unload "unload-all": { "getInfo": function(entState) { if (!entState.garrisonHolder) return false; var selection = g_Selection.toList(); var count = 0; for (var ent of selection) { var state = GetEntityState(ent); if (state.garrisonHolder) count += state.garrisonHolder.entities.length; } return { "tooltip": translate("Unload All"), "icon": "garrison-out.png", "count": count, }; }, "execute": function(entState) { unloadAll(); }, }, // Delete "delete": { "getInfo": function(entState) { if (entState.mirage) return { "tooltip": translate("You cannot destroy this entity because it is in the fog-of-war"), "icon": "kill_small.png" }; + if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2) + return { + "tooltip": translate("You cannot destroy this entity as you own less than half the capture points"), + "icon": "kill_small.png" + }; + + return { "tooltip": translate("Delete"), "icon": "kill_small.png" }; }, "execute": function(entState) { if (entState.mirage) return; var selection = g_Selection.toList(); if (selection.length < 1) return; if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather) openDeleteDialog(selection); }, }, // Stop "stop": { "getInfo": function(entState) { if (!entState.unitAI) return false; return { "tooltip": translate("Stop"), "icon": "stop.png" }; }, "execute": function(entState) { var selection = g_Selection.toList(); if (selection.length > 0) stopUnits(selection); }, }, // Garrison "garrison": { "getInfo": function(entState) { if (!entState.unitAI || entState.turretParent) return false; return { "tooltip": translate("Garrison"), "icon": "garrison.png" }; }, "execute": function(entState) { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; }, }, // Ungarrison "unload": { "getInfo": function(entState) { if (!entState.unitAI || !entState.turretParent) return false; var p = GetEntityState(entState.turretParent); if (!p.garrisonHolder || p.garrisonHolder.entities.indexOf(entState.id) == -1) return false; return { "tooltip": translate("Unload"), "icon": "garrison-out.png" }; }, "execute": function(entState) { unloadSelection(); }, }, // Repair "repair": { "getInfo": function(entState) { if (!entState.builder) return false; return { "tooltip": translate("Repair"), "icon": "repair.png" }; }, "execute": function(entState) { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; }, }, // Focus on rally point "focus-rally": { "getInfo": function(entState) { if (!entState.rallyPoint) return false; return { "tooltip": translate("Focus on Rally Point"), "icon": "focus-rally.png" }; }, "execute": function(entState) { var focusTarget = null; if (entState.rallyPoint && entState.rallyPoint.position) focusTarget = entState.rallyPoint.position; else if (entState.position) focusTarget = entState.position; if (focusTarget) Engine.CameraMoveTo(focusTarget.x, focusTarget.z); }, }, // Back to work "back-to-work": { "getInfo": function(entState) { if (!entState.unitAI || !entState.unitAI.hasWorkOrders) return false; return { "tooltip": translate("Back to Work"), "icon": "production.png" }; }, "execute": function(entState) { backToWork(); }, }, // Guard "add-guard": { "getInfo": function(entState) { if (!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding) return false; return { "tooltip": translate("Guard"), "icon": "add-guard.png" }; }, "execute": function(entState) { inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GUARD; }, }, // Remove guard "remove-guard": { "getInfo": function(entState) { if (!entState.unitAI || !entState.unitAI.isGuarding) return false; return { "tooltip": translate("Remove guard"), "icon": "remove-guard.png" }; }, "execute": function(entState) { removeGuard(); }, }, // Trading "select-trading-goods": { "getInfo": function(entState) { if (!hasClass(entState, "Market")) return false; return { "tooltip": translate("Select trading goods"), "icon": "economics.png" }; }, "execute": function(entState) { toggleTrade(); }, }, // Raise alert "increase-alert-level": { "getInfo": function(entState) { if(!entState.alertRaiser || !entState.alertRaiser.canIncreaseLevel) return false; if(entState.alertRaiser.hasRaisedAlert) var tooltip = translate("Increase the alert level to protect more units"); else var tooltip = translate("Raise an alert!"); return { "tooltip": tooltip, "icon": "bell_level1.png" }; }, "execute": function(entState) { increaseAlertLevel(); }, }, // End alert "alert-end": { "getInfo": function(entState) { if(!entState.alertRaiser || !entState.alertRaiser.hasRaisedAlert) return false return { "tooltip": translate("End of alert."), "icon": "bell_level0.png" }; }, "execute": function(entState) { endOfAlert(); }, }, }; function playerCheck(entState, targetState, validPlayers) { var playerState = GetSimState().players[entState.player]; for (var player of validPlayers) { if (player == "Gaia" && targetState.player == 0) return true; if (player == "Player" && targetState.player == entState.player) return true; if (playerState["is"+player] && playerState["is"+player][targetState.player]) return true; } return false; }; Index: ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 16550) @@ -1,364 +1,371 @@ function AIProxy() {} AIProxy.prototype.Schema = ""; /** * AIProxy passes its entity's state data to AI scripts. * * Efficiency is critical: there can be many thousands of entities, * and the data returned by this component is serialized and copied to * the AI thread every turn, so it can be quite expensive. * * We omit all data that can be derived statically from the template XML * files - the AI scripts can parse the templates themselves. * This violates the component interface abstraction and is potentially * fragile if the template formats change (since both the component code * and the AI will have to be updated in sync), but it's not *that* bad * really and it helps performance significantly. * * We also add an optimisation to avoid copying non-changing values. * The first call to GetRepresentation calls GetFullRepresentation, * which constructs the complete entity state representation. * After that, we simply listen to events from the rest of the gameplay code, * and store the changed data in this.changes. * Properties in this.changes will override those previously returned * from GetRepresentation; if a property isn't overridden then the AI scripts * will keep its old value. * * The event handlers should set this.changes.whatever to exactly the * same as GetFullRepresentation would set. */ AIProxy.prototype.Init = function() { this.changes = null; this.needsFullGet = true; // cache some data across turns this.owner = -1; this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); // Let the AIInterface know that we exist and that it should query us this.NotifyChange(); }; AIProxy.prototype.Serialize = null; // we have no dynamic state to save AIProxy.prototype.Deserialize = function () { this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); }; AIProxy.prototype.GetRepresentation = function() { // Return the full representation the first time we're called var ret; if (this.needsFullGet) { ret = this.GetFullRepresentation(); this.needsFullGet = false; } else { ret = this.changes; } // Initialise changes to null instead of {}, to avoid memory allocations in the // common case where there will be no changes; event handlers should each reset // it to {} if needed this.changes = null; return ret; }; AIProxy.prototype.NotifyChange = function() { if (this.needsFullGet) { // not yet notified, be sure that the owner is set before doing so // as the Create event is sent only on first ownership changed let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() < 0) return false; } if (!this.changes) { this.changes = {}; this.cmpAIInterface.ChangedEntity(this.entity); } return true; }; // AI representation-updating event handlers: AIProxy.prototype.OnPositionChanged = function(msg) { if (!this.NotifyChange()) return; if (msg.inWorld) this.changes.position = [msg.x, msg.z]; else this.changes.position = undefined; }; AIProxy.prototype.OnHealthChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.hitpoints = msg.to; }; +AIProxy.prototype.OnCapturePointsChanged = function(msg) +{ + if (!this.NotifyChange()) + return; + this.changes.capturePoints = msg.capturePoints; +}; + AIProxy.prototype.OnUnitIdleChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.idle = msg.idle; }; AIProxy.prototype.OnUnitAIStateChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.unitAIState = msg.to; }; AIProxy.prototype.OnUnitAIOrderDataChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.unitAIOrderData = msg.to; }; AIProxy.prototype.OnProductionQueueChanged = function(msg) { if (!this.NotifyChange()) return; var cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); this.changes.trainingQueue = cmpProductionQueue.GetQueue(); }; AIProxy.prototype.OnGarrisonedUnitsChanged = function(msg) { if (!this.NotifyChange()) return; var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); this.changes.garrisoned = cmpGarrisonHolder.GetEntities(); // Send a message telling a unit garrisoned or ungarrisoned. // I won't check if the unit is still alive so it'll be up to the AI. var added = msg.added; var removed = msg.removed; for each (var ent in added) this.cmpAIInterface.PushEvent("Garrison", {"entity" : ent, "holder": this.entity}); for each (var ent in removed) this.cmpAIInterface.PushEvent("UnGarrison", {"entity" : ent, "holder": this.entity}); }; AIProxy.prototype.OnResourceSupplyChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.resourceSupplyAmount = msg.to; }; AIProxy.prototype.OnResourceSupplyGatherersChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.resourceSupplyGatherers = msg.to; }; AIProxy.prototype.OnResourceCarryingChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.resourceCarrying = msg.to; }; AIProxy.prototype.OnFoundationProgressChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.foundationProgress = msg.to; }; AIProxy.prototype.OnFoundationBuildersChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.foundationBuilders = msg.to; }; AIProxy.prototype.OnTerritoryDecayChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.decaying = msg.to; }; // TODO: event handlers for all the other things AIProxy.prototype.GetFullRepresentation = function() { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var ret = { // These properties are constant and won't need to be updated "id": this.entity, "template": cmpTemplateManager.GetCurrentTemplateName(this.entity) } var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) { // Updated by OnPositionChanged if (cmpPosition.IsInWorld()) { var pos = cmpPosition.GetPosition2D(); ret.position = [pos.x, pos.y]; } else { ret.position = undefined; } } var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (cmpHealth) { // Updated by OnHealthChanged ret.hitpoints = cmpHealth.GetHitpoints(); } var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) { // Updated by OnOwnershipChanged ret.owner = cmpOwnership.GetOwner(); if (!this.owner) this.owner = ret.owner; } var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (cmpUnitAI) { // Updated by OnUnitIdleChanged ret.idle = cmpUnitAI.IsIdle(); // Updated by OnUnitAIStateChanged ret.unitAIState = cmpUnitAI.GetCurrentState(); // Updated by OnUnitAIOrderDataChanged ret.unitAIOrderData = cmpUnitAI.GetOrderData(); } var cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); if (cmpProductionQueue) { // Updated by OnProductionQueueChanged ret.trainingQueue = cmpProductionQueue.GetQueue(); } var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) { // Updated by OnFoundationProgressChanged ret.foundationProgress = cmpFoundation.GetBuildPercentage(); } var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); if (cmpResourceSupply) { // Updated by OnResourceSupplyChanged ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount(); ret.resourceSupplyGatherers = cmpResourceSupply.GetGatherers(); } var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { // Updated by OnResourceCarryingChanged ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Updated by OnGarrisonedUnitsChanged ret.garrisoned = cmpGarrisonHolder.GetEntities(); } var cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); if (cmpTerritoryDecay) ret.decaying = cmpTerritoryDecay.IsDecaying(); return ret; }; // AI event handlers: // (These are passed directly as events to the AI scripts, rather than updating // our proxy representation.) // (This shouldn't include extremely high-frequency events, like PositionChanged, // because that would be very expensive and AI will rarely care about all those // events.) // special case: this changes the state and sends an event. AIProxy.prototype.OnOwnershipChanged = function(msg) { this.NotifyChange(); if (msg.from === -1) { this.cmpAIInterface.PushEvent("Create", {"entity" : msg.entity}); return; } else if (msg.to === -1) { this.cmpAIInterface.PushEvent("Destroy", {"entity" : msg.entity}); this.needsFullGet = true; return; } this.owner = msg.to; this.changes.owner = msg.to; this.cmpAIInterface.PushEvent("OwnershipChanged", msg); }; AIProxy.prototype.OnAttacked = function(msg) { this.cmpAIInterface.PushEvent("Attacked", msg); }; /* Deactivated for actually not really being practical for most uses. AIProxy.prototype.OnRangeUpdate = function(msg) { msg.owner = this.owner; this.cmpAIInterface.PushEvent("RangeUpdate", msg); warn(uneval(msg)); };*/ AIProxy.prototype.OnConstructionFinished = function(msg) { this.cmpAIInterface.PushEvent("ConstructionFinished", msg); }; AIProxy.prototype.OnTrainingStarted = function(msg) { this.cmpAIInterface.PushEvent("TrainingStarted", msg); }; AIProxy.prototype.OnTrainingFinished = function(msg) { this.cmpAIInterface.PushEvent("TrainingFinished", msg); }; AIProxy.prototype.OnAIMetadata = function(msg) { this.cmpAIInterface.PushEvent("AIMetadata", msg); }; Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy); Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 16550) @@ -1,627 +1,685 @@ function Attack() {} Attack.prototype.bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Attack.prototype.preferredClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.restrictedClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + "" + "10.0" + "0.0" + "5.0" + "4.0" + "1000" + "" + "" + "pers" + "Infantry" + "1.5" + "" + "" + "Cavalry Melee" + "1.5" + "" + "" + "Champion" + "Cavalry Infantry" + "" + "" + "0.0" + "10.0" + "0.0" + "44.0" + "20.0" + "15.0" + "800" + "1600" + "50.0" + "2.5" + "" + "" + "Cavalry" + "2" + "" + "" + "Champion" + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "10.0" + "0.0" + "50.0" + "24.0" + "20.0" + "" + "" + "1000.0" + "0.0" + "0.0" + "4.0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.bonusesSchema + "" + "" + "" + "" + "" + "" + "" + + "" + + "" + + "" + + "" + + "" + // TODO: it shouldn't be stretched + "" + + "" + + Attack.prototype.bonusesSchema + + Attack.prototype.preferredClassesSchema + + Attack.prototype.restrictedClassesSchema + + "" + + "" + + "" + + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.Serialize = null; // we have no dynamic state to save Attack.prototype.GetAttackTypes = function() { var ret = []; if (this.template.Charge) ret.push("Charge"); if (this.template.Melee) ret.push("Melee"); if (this.template.Ranged) ret.push("Ranged"); + if (this.template.Capture) ret.push("Capture"); return ret; }; Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && this.template[type].PreferredClasses._string) { return this.template[type].PreferredClasses._string.split(/\s+/); } return []; }; Attack.prototype.GetRestrictedClasses = function(type) { if (this.template[type] && this.template[type].RestrictedClasses && this.template[type].RestrictedClasses._string) { return this.template[type].RestrictedClasses._string.split(/\s+/); } return []; }; Attack.prototype.CanAttack = function(target) { + var cmpArmour = Engine.QueryInterface(target, IID_DamageReceiver); + if (!cmpArmour) + return false; + var cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) return true; var cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; // Check if the relative height difference is larger than the attack range // If the relative height is bigger, it means they will never be able to // reach each other, no matter how close they come. var heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); for each (var type in this.GetAttackTypes()) { if (heightDiff > this.GetRange(type).max) continue; var canAttack = true; var restrictedClasses = this.GetRestrictedClasses(type); for each (var targetClass in targetClasses) { if (restrictedClasses.indexOf(targetClass) != -1) { canAttack = false; break; } } if (canAttack) { return true; } } return false; }; /** * Returns null if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); var minPref = null; for (var type of this.GetAttackTypes()) { var preferredClasses = this.GetPreferredClasses(type); for (var targetClass of targetClasses) { var pref = preferredClasses.indexOf(targetClass); if (pref != -1 && (minPref === null || minPref > pref)) minPref = pref; } } return minPref; }; /** * Return the type of the best attack. * TODO: this should probably depend on range, target, etc, * so we can automatically switch between ranged and melee */ Attack.prototype.GetBestAttack = function() { return this.GetAttackTypes().pop(); }; Attack.prototype.GetBestAttackAgainst = function(target) { var cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) return this.GetBestAttack(); - const cmpIdentity = Engine.QueryInterface(target, IID_Identity); + var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; - const targetClasses = cmpIdentity.GetClassesList(); - const isTargetClass = function (value, i, a) { return targetClasses.indexOf(value) != -1; }; - const types = this.GetAttackTypes(); - const attack = this; - const isAllowed = function (value, i, a) { return !attack.GetRestrictedClasses(value).some(isTargetClass); } - const isPreferred = function (value, i, a) { return attack.GetPreferredClasses(value).some(isTargetClass); } - const byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); } + + var targetClasses = cmpIdentity.GetClassesList(); + var isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; }; // Always slaughter domestic animals instead of using a normal attack if (isTargetClass("Domestic") && this.template.Slaughter) return "Slaughter"; - return types.filter(isAllowed).sort(byPreference).pop(); + var attack = this; + var isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass); } + + var types = this.GetAttackTypes().filter(isAllowed); + + // check if the target is capturable + var captureIndex = types.indexOf("Capture") + if (captureIndex != -1) + { + var cmpCapturable = Engine.QueryInterface(target, IID_Capturable); + var cmpPlayer = QueryOwnerInterface(this.entity); + if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) + return "Capture"; + // not captureable, so remove this attack + types.splice(captureIndex, 1); + } + + var isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); } + var byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); } + + + return types.sort(byPreference).pop(); }; Attack.prototype.CompareEntitiesByPreference = function(a, b) { var aPreference = this.GetPreference(a); var bPreference = this.GetPreference(b); if (aPreference === null && bPreference === null) return 0; if (aPreference === null) return 1; if (bPreference === null) return -1; return aPreference - bPreference; }; Attack.prototype.GetTimers = function(type) { var prepare = +(this.template[type].PrepareTime || 0); prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity); var repeat = +(this.template[type].RepeatTime || 1000); repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity); return { "prepare": prepare, "repeat": repeat, "recharge": repeat - prepare }; }; Attack.prototype.GetAttackStrengths = function(type) { // Work out the attack values with technology effects var self = this; var template = this.template[type]; var splash = ""; if (!template) { template = this.template[type.split(".")[0]].Splash; splash = "/Splash"; } var applyMods = function(damageType) { return ApplyValueModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), self.entity); }; - + + if (type == "Capture") + return {value: applyMods("Value")}; + return { hack: applyMods("Hack"), pierce: applyMods("Pierce"), crush: applyMods("Crush") }; }; Attack.prototype.GetRange = function(type) { var max = +this.template[type].MaxRange; max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity); var min = +(this.template[type].MinRange || 0); min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); var elevationBonus = +(this.template[type].ElevationBonus || 0); elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); return { "max": max, "min": min, "elevationBonus": elevationBonus}; }; // Calculate the attack damage multiplier against a target Attack.prototype.GetAttackBonus = function(type, target) { var attackBonus = 1; var template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; if (template.Bonuses) { var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return 1; // Multiply the bonuses for all matching classes for (var key in template.Bonuses) { var bonus = template.Bonuses[key]; var hasClasses = true; if (bonus.Classes){ var classes = bonus.Classes.split(/\s+/); for (var key in classes) hasClasses = hasClasses && cmpIdentity.HasClass(classes[key]); } if (hasClasses && (!bonus.Civ || bonus.Civ === cmpIdentity.GetCiv())) attackBonus *= bonus.Multiplier; } } return attackBonus; }; // Returns a 2d random distribution scaled for a spread of scale 1. // The current implementation is a 2d gaussian with sigma = 1 Attack.prototype.GetNormalDistribution = function(){ // Use the Box-Muller transform to get a gaussian distribution var a = Math.random(); var b = Math.random(); var c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b); var d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b); return [c, d]; }; /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ Attack.prototype.PerformAttack = function(type, target) { // If this is a ranged attack, then launch a projectile if (type == "Ranged") { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var turnLength = cmpTimer.GetLatestTurnLength()/1000; // In the future this could be extended: // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely // Get some data about the entity var horizSpeed = +this.template[type].ProjectileSpeed; var gravity = 9.81; // this affects the shape of the curve; assume it's constant for now var spread = +this.template.Ranged.Spread; spread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", spread, this.entity); //horizSpeed /= 2; gravity /= 2; // slow it down for testing var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var selfPosition = cmpPosition.GetPosition(); var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; var targetPosition = cmpTargetPosition.GetPosition(); var relativePosition = Vector3D.sub(targetPosition, selfPosition); var previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); var targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); // the component of the targets velocity radially away from the archer var radialSpeed = relativePosition.dot(targetVelocity) / relativePosition.length(); var horizDistance = targetPosition.horizDistanceTo(selfPosition); // This is an approximation of the time ot the target, it assumes that the target has a constant radial // velocity, but since units move in straight lines this is not true. The exact value would be more // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was // about 5% of the units radius out in the worst case) var timeToTarget = horizDistance / (horizSpeed - radialSpeed); // Predict where the unit is when the missile lands. var predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); // Compute the real target point (based on spread and target speed) var range = this.GetRange(type); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var elevationAdaptedMaxRange = cmpRangeManager.GetElevationAdaptedRange(selfPosition, cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); var distanceModifiedSpread = spread * horizDistance/elevationAdaptedMaxRange; var randNorm = this.GetNormalDistribution(); var offsetX = randNorm[0] * distanceModifiedSpread * (1 + targetVelocity.length() / 20); var offsetZ = randNorm[1] * distanceModifiedSpread * (1 + targetVelocity.length() / 20); var realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ); // Calculate when the missile will hit the target position var realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition); var timeToTarget = realHorizDistance / horizSpeed; var missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); // Make the arrow appear to land slightly behind the target so that arrows landing next to a guys foot don't count but arrows that go through the torso do var graphicalPosition = Vector3D.mult(missileDirection, 2).add(realTargetPosition); // Launch the graphical projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); var id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); var playerId = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "playerId":playerId}); } + else if (type == "Capture") + { + var multiplier = this.GetAttackBonus(type, target); + var cmpHealth = Engine.QueryInterface(target, IID_Health); + if (!cmpHealth || cmpHealth.GetHitpoints() == 0) + return; + multiplier *= cmpHealth.GetMaxHitpoints() / cmpHealth.GetHitpoints(); + + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + return; + var owner = cmpOwnership.GetOwner(); + var cmpCapturable = Engine.QueryInterface(target, IID_Capturable); + if (!cmpCapturable || !cmpCapturable.CanCapture(owner)) + return; + + var strength = this.GetAttackStrengths("Capture").value; + cmpCapturable.Reduce(strength * multiplier, owner); + } else { // Melee attack - hurt the target immediately Damage.CauseDamage({"strengths":this.GetAttackStrengths(type), "target":target, "attacker":this.entity, "multiplier":this.GetAttackBonus(type, target), "type":type}); } // TODO: charge attacks (need to design how they work) }; Attack.prototype.InterpolatedLocation = function(ent, lateness) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var turnLength = cmpTimer.GetLatestTurnLength()/1000; var cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly return undefined; var curPos = cmpTargetPosition.GetPosition(); var prevPos = cmpTargetPosition.GetPreviousPosition(); lateness /= 1000; return new Vector3D((curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength, 0, (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength); }; // Tests whether it point is inside of ent's footprint Attack.prototype.testCollision = function(ent, point, lateness) { var targetPosition = this.InterpolatedLocation(ent, lateness); if (!targetPosition) return false; var cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); if (!cmpFootprint) return false; var targetShape = cmpFootprint.GetShape(); if (!targetShape || !targetPosition) return false; if (targetShape.type === 'circle') { // Use VectorDistanceSquared and square targetShape.radius to avoid square roots. return (targetPosition.horizDistanceTo(point) < (targetShape.radius * targetShape.radius)); } else { var angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y; var d = Vector3D.sub(point, targetPosition); d = Vector2D.from3D(d).rotate(-angle); return d.x < Math.abs(targetShape.width/2) && d.y < Math.abs(targetShape.depth/2); } }; Attack.prototype.MissileHit = function(data, lateness) { var targetPosition = this.InterpolatedLocation(data.target, lateness); if (!targetPosition) return; if (this.template.Ranged.Splash) // splash damage, do this first in case the direct hit kills the target { var friendlyFire = this.template.Ranged.Splash.FriendlyFire; var splashRadius = this.template.Ranged.Splash.Range; var splashShape = this.template.Ranged.Splash.Shape; var playersToDamage; // If friendlyFire isn't enabled, get all player enemies to pass to "Damage.CauseSplashDamage". if (friendlyFire == "false") { - var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player) + var cmpPlayer = QueryPlayerIDInterface(data.playerId); playersToDamage = cmpPlayer.GetEnemies(); } // Damage the units. Damage.CauseSplashDamage({"attacker":this.entity, "origin":Vector2D.from3D(data.position), "radius":splashRadius, "shape":splashShape, "strengths":this.GetAttackStrengths(data.type), "direction":data.direction, "playersToDamage":playersToDamage, "type":data.type}); } if (this.testCollision(data.target, data.position, lateness)) { data.attacker = this.entity data.multiplier = this.GetAttackBonus(data.type, data.target) data.strengths = this.GetAttackStrengths(data.type) // Hit the primary target Damage.CauseDamage(data); // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); } else { // If we didn't hit the main target look for nearby units - var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player) + var cmpPlayer = QueryPlayerIDInterface(data.playerId); var ents = Damage.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies()); for (var i = 0; i < ents.length; i++) { if (this.testCollision(ents[i], data.position, lateness)) { var newData = {"strengths":this.GetAttackStrengths(data.type), "target":ents[i], "attacker":this.entity, "multiplier":this.GetAttackBonus(data.type, ents[i]), "type":data.type}; Damage.CauseDamage(newData); // Remove the projectile var cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); cmpProjectileManager.RemoveProjectile(data.projectileId); } } } }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 16550) @@ -0,0 +1,212 @@ +function Capturable() {} + +Capturable.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + +Capturable.prototype.Init = function() +{ + // Cache this value + this.maxCp = +this.template.CapturePoints; + this.cp = []; + this.startRegenTimer(); +}; + +//// Interface functions //// + +/** + * Returns the current capture points array + */ +Capturable.prototype.GetCapturePoints = function() +{ + return this.cp; +}; + +Capturable.prototype.GetMaxCapturePoints = function() +{ + return this.maxCp; +}; + +/** + * Set the new capture points, used for cloning entities + * The caller should assure that the sum of capture points + * matches the max. + */ +Capturable.prototype.SetCapturePoints = function(capturePointsArray) +{ + this.cp = capturePointsArray; +}; + +/** + * Reduces the amount of capture points of an entity, + * in favour of the player of the source + * Returns the number of capture points actually taken + */ +Capturable.prototype.Reduce = function(amount, playerID) +{ + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + return 0; + + var cmpPlayerSource = QueryPlayerIDInterface(playerID); + if (!cmpPlayerSource) + return 0; + + // Before changing the value, activate Fogging if necessary to hide changes + var cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); + if (cmpFogging) + cmpFogging.Activate(); + + var enemiesFilter = function(v, i) { return v > 0 && !cmpPlayerSource.IsAlly(i); }; + var numberOfEnemies = this.cp.filter(enemiesFilter).length; + + if (numberOfEnemies == 0) + return 0; + + // distribute the capture points over all enemies + var distributedAmount = amount / numberOfEnemies; + for (let i in this.cp) + { + if (cmpPlayerSource.IsAlly(i)) + continue; + if (this.cp[i] > distributedAmount) + this.cp[i] -= distributedAmount; + else + this.cp[i] = 0; + } + + // give all cp taken to the player + var takenCp = this.maxCp - this.cp.reduce(function(a, b) { return a + b; }); + this.cp[playerID] += takenCp; + + this.startRegenTimer(); + + Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp }) + + if (this.cp[cmpOwnership.GetOwner()] > 0) + return takenCp; + + // if all cp has been taken from the owner, convert it to the best player + var bestPlayer = 0; + for (let i in this.cp) + if (this.cp[i] >= this.cp[bestPlayer]) + bestPlayer = +i; + + cmpOwnership.SetOwner(bestPlayer); + + return takenCp; +}; + +/** + * Check if the source can (re)capture points from this building + */ +Capturable.prototype.CanCapture = function(playerID) +{ + var cmpPlayerSource = QueryPlayerIDInterface(playerID); + + if (!cmpPlayerSource) + warn(source + " has no player component defined on its owner "); + var cp = this.GetCapturePoints() + var sourceEnemyCp = 0; + for (let i in this.GetCapturePoints()) + if (!cmpPlayerSource.IsAlly(i)) + sourceEnemyCp += cp[i]; + return sourceEnemyCp > 0; +}; + +//// Private functions //// + +Capturable.prototype.GetRegenRate = function() +{ + var regenRate = +this.template.RegenRate; + regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", regenRate, this.entity); + + var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (!cmpGarrisonHolder) + return regenRate; + + var garrisonRegenRate = +this.template.GarrisonRegenRate; + garrisonRegenRate = ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", garrisonRegenRate, this.entity); + var garrisonedUnits = cmpGarrisonHolder.GetEntities().length; + return regenRate + garrisonedUnits * garrisonRegenRate; +}; + +Capturable.prototype.RegenCapturePoints = function() +{ + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + return; + + var takenCp = this.Reduce(this.GetRegenRate(), cmpOwnership.GetOwner()) + if (takenCp > 0) + return; + + // no capture points taken, stop the timer + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.regenTimer); + this.regenTimer = 0; +}; + +/** + * Start the regeneration timer when no timer exists + * When nothing can be regenerated (f.e. because the + * rate is 0, or because it is fully regenerated), + * the timer stops automatically after one execution. + */ +Capturable.prototype.startRegenTimer = function() +{ + if (this.regenTimer) + return; + var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.regenTimer = cmpTimer.SetInterval(this.entity, IID_Capturable, "RegenCapturePoints", 1000, 1000, null); +}; + +//// Message Listeners //// + +Capturable.prototype.OnValueModification = function(msg) +{ + if (msg.component != "Capturable") + return; + + var oldMaxCp = this.GetMaxCapturePoints(); + this.maxCp = ApplyValueModificationsToEntity("Capturable/Max", +this.template.Max, this.entity); + if (oldMaxCp == this.maxCp) + return; + + var scale = this.maxCp / oldMaxCp; + for (let i in this.cp) + this.cp[i] *= scale; + Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp }); + this.startRegenTimer(); +}; + +Capturable.prototype.OnGarrisonedUnitsChanged = function(msg) +{ + this.startRegenTimer(); +}; + +Capturable.prototype.OnOwnershipChanged = function(msg) +{ + this.startRegenTimer(); + + if (msg.from != -1) + return; + + // initialise the capture points when created + this.cp = []; + var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + for (let i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) + if (i == msg.to) + this.cp[i] = this.maxCp; + else + this.cp[i] = 0; +}; + +Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable); Index: ps/trunk/binaries/data/mods/public/simulation/components/Fogging.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Fogging.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/Fogging.js (revision 16550) @@ -1,218 +1,225 @@ const VIS_HIDDEN = 0; const VIS_FOGGED = 1; const VIS_VISIBLE = 2; function Fogging() {} Fogging.prototype.Schema = "Allows this entity to be replaced by mirage entities in the fog-of-war." + ""; Fogging.prototype.Init = function() { this.activated = false; this.mirages = []; this.miraged = []; this.seen = []; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); for (let player = 0; player < cmpPlayerManager.GetNumPlayers(); ++player) { this.mirages.push(INVALID_ENTITY); this.miraged.push(false); this.seen.push(false); } }; Fogging.prototype.Activate = function() { let mustUpdate = !this.activated; this.activated = true; if (mustUpdate) { // Load a mirage for each player who has already seen the entity let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); for (let player = 0; player < cmpPlayerManager.GetNumPlayers(); ++player) { if (this.seen[player]) this.LoadMirage(player); } } }; Fogging.prototype.IsActivated = function() { return this.activated; }; Fogging.prototype.LoadMirage = function(player) { if (!this.activated) { error("LoadMirage called for an entity with fogging deactivated"); return; } this.miraged[player] = true; if (this.mirages[player] == INVALID_ENTITY) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var templateName = "mirage|" + cmpTemplateManager.GetCurrentTemplateName(this.entity); this.mirages[player] = Engine.AddEntity(templateName); } var cmpMirage = Engine.QueryInterface(this.mirages[player], IID_Mirage); if (!cmpMirage) { error("Failed to load mirage entity for template " + templateName); this.mirages[player] = INVALID_ENTITY; return; } // Copy basic mirage properties cmpMirage.SetPlayer(player); cmpMirage.SetParent(this.entity); // Copy cmpOwnership data var cmpParentOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpMirageOwnership = Engine.QueryInterface(this.mirages[player], IID_Ownership); if (!cmpParentOwnership || !cmpMirageOwnership) { error("Failed to copy the ownership data of the fogged entity " + templateName); return; } cmpMirageOwnership.SetOwner(cmpParentOwnership.GetOwner()); // Copy cmpPosition data var cmpParentPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpMiragePosition = Engine.QueryInterface(this.mirages[player], IID_Position); if (!cmpParentPosition || !cmpMiragePosition) { error("Failed to copy the position data of the fogged entity " + templateName); return; } if (!cmpParentPosition.IsInWorld()) return; var pos = cmpParentPosition.GetPosition(); cmpMiragePosition.JumpTo(pos.x, pos.z); var rot = cmpParentPosition.GetRotation(); cmpMiragePosition.SetYRotation(rot.y); cmpMiragePosition.SetXZRotation(rot.x, rot.z); // Copy cmpVisualActor data var cmpParentVisualActor = Engine.QueryInterface(this.entity, IID_Visual); var cmpMirageVisualActor = Engine.QueryInterface(this.mirages[player], IID_Visual); if (!cmpParentVisualActor || !cmpMirageVisualActor) { error("Failed to copy the visual data of the fogged entity " + templateName); return; } cmpMirageVisualActor.SetActorSeed(cmpParentVisualActor.GetActorSeed()); // Store valuable information into the mirage component (especially for the GUI) var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) cmpMirage.CopyFoundation(cmpFoundation.GetBuildPercentage()); var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (cmpHealth) cmpMirage.CopyHealth( cmpHealth.GetMaxHitpoints(), cmpHealth.GetHitpoints(), cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()) ); + var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable); + if (cmpCapturable) + cmpMirage.CopyCapturable( + cmpCapturable.GetCapturePoints(), + cmpCapturable.GetMaxCapturePoints() + ); + var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); if (cmpResourceSupply) cmpMirage.CopyResourceSupply( cmpResourceSupply.GetMaxAmount(), cmpResourceSupply.GetCurrentAmount(), cmpResourceSupply.GetType(), cmpResourceSupply.IsInfinite() ); // Notify the GUI the entity has been replaced by a mirage, in case it is selected at this moment var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.AddMiragedEntity(player, this.entity, this.mirages[player]); // Notify the range manager the visibility of this entity must be updated let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.RequestVisibilityUpdate(this.entity); }; Fogging.prototype.ForceMiraging = function(player) { if (!this.activated) return; this.seen[player] = true; this.LoadMirage(player); }; Fogging.prototype.IsMiraged = function(player) { if (player < 0 || player >= this.mirages.length) return false; return this.miraged[player]; }; Fogging.prototype.GetMirage = function(player) { if (player < 0 || player >= this.mirages.length) return INVALID_ENTITY; return this.mirages[player]; }; Fogging.prototype.WasSeen = function(player) { if (player < 0 || player >= this.seen.length) return false; return this.seen[player]; }; Fogging.prototype.OnDestroy = function(msg) { for (var player = 0; player < this.mirages.length; ++player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager.GetLosVisibility(this.mirages[player], player) == "hidden") { Engine.DestroyEntity(this.mirages[player]); continue; } var cmpMirage = Engine.QueryInterface(this.mirages[player], IID_Mirage); if (cmpMirage) cmpMirage.SetParent(INVALID_ENTITY); } }; Fogging.prototype.OnOwnershipChanged = function(msg) { // Always activate fogging for non-Gaia entities if (msg.to > 0) this.Activate(); }; Fogging.prototype.OnVisibilityChanged = function(msg) { if (msg.player < 0 || msg.player >= this.mirages.length) return; if (msg.newVisibility == VIS_VISIBLE) { this.miraged[msg.player] = false; this.seen[msg.player] = true; } if (msg.newVisibility == VIS_FOGGED && this.activated) this.LoadMirage(msg.player); }; Engine.RegisterComponentType(IID_Fogging, "Fogging", Fogging); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 16550) @@ -1,1869 +1,1895 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised so we mustn't serialise // its non-deterministic data. Instead just return an empty object. return {}; }; GuiInterface.prototype.Deserialize = function(obj) { this.Init(); }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function(player) { var ret = { "players": [] }; var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); // Work out what phase we are in var phase = ""; var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } // store player ally/neutral/enemy data as arrays var allies = []; var mutualAllies = []; var neutrals = []; var enemies = []; for (var j = 0; j < n; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } var playerData = { "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "color": cmpPlayer.GetColor(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "heroes": cmpPlayer.GetHeroes(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedResearch() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null }; ret.players.push(playerData); } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = 4 * cmpTerrain.GetTilesPerSide(); // Add timeElapsed var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); // Add the game type var cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.gameType = cmpEndGameManager.GetGameType(); // Add bartering prices var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); ret.barterPrices = cmpBarter.GetPrices(); // Add basic statistics to each player var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function(player) { // Get basic simulation info var ret = this.GetSimulationState(); // Add statistics to each player var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics(); } return ret; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); else return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function(player) { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({"entity": entity, "newentity": mirage}); }; /** * Get common entity info, often used in the gui */ GuiInterface.prototype.GetEntityState = function(player, ent) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // All units must have a template; if not then it's a nonexistent entity id var template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; var ret = { "id": ent, "template": template, "alertRaiser": null, "builder": null, "identity": null, "fogging": null, "foundation": null, "garrisonHolder": null, "gate": null, "guard": null, "mirage": null, "pack": null, "player": -1, "position": null, "production": null, "rallyPoint": null, "resourceCarrying": null, "rotation": null, "trader": null, "unitAI": null, "visibility": null, }; var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "visibleClasses": cmpIdentity.GetVisibleClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName() }; } var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { ret.position = cmpPosition.GetPosition(); ret.rotation = cmpPosition.GetRotation(); } var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = Math.ceil(cmpHealth.GetHitpoints()); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); ret.needsHeal = !cmpHealth.IsUnhealable(); } if (cmpMirage && cmpMirage.Health()) { ret.hitpoints = cmpMirage.GetHitpoints(); ret.maxHitpoints = cmpMirage.GetMaxHitpoints(); ret.needsRepair = cmpMirage.NeedsRepair(); } + var cmpCapturable = Engine.QueryInterface(ent, IID_Capturable); + if (cmpCapturable) + { + ret.capturePoints = cmpCapturable.GetCapturePoints(); + ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); + } + if (cmpMirage && cmpMirage.Capturable()) + { + ret.capturePoints = cmpMirage.GetCapturePoints(); + ret.maxCapturePoints = cmpMirage.GetMaxCapturePoints(); + } + var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; var cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) { ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress(), }; } var cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) { ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "queue": cmpProductionQueue.GetQueue(), }; } var cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) { ret.trader = { "goods": cmpTrader.GetGoods(), "requiredGoods": cmpTrader.GetRequiredGoods() }; } var cmpFogging = Engine.QueryInterface(ent, IID_Fogging); if (cmpFogging) { if (cmpFogging.IsMiraged(player)) ret.fogging = {"mirage": cmpFogging.GetMirage(player)}; else ret.fogging = {"mirage": null}; } var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation) { ret.foundation = { "progress": cmpFoundation.GetBuildPercentage(), "numBuilders": cmpFoundation.GetNumBuilders() }; } if (cmpMirage && cmpMirage.Foundation()) { ret.foundation = { "progress": cmpMirage.GetBuildPercentage() }; } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) { ret.player = cmpOwnership.GetOwner(); } var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { ret.rallyPoint = {'position': cmpRallyPoint.GetPositions()[0]}; // undefined or {x,z} object } var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) { ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; } var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "possibleStances": cmpUnitAI.GetPossibleStances(), "isIdle":cmpUnitAI.IsIdle(), }; // Add some information needed for ungarrisoning if (cmpUnitAI.IsGarrisoned() && ret.player !== undefined) ret.template = "p" + ret.player + "&" + ret.template; } var cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) { ret.guard = { "entities": cmpGuard.GetEntities(), }; } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) { ret.gate = { "locked": cmpGate.IsLocked(), }; } var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) { ret.alertRaiser = { "level": cmpAlertRaiser.GetLevel(), "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(), "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(), }; } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); return ret; }; /** * Get additionnal entity info, rarely used in the gui */ GuiInterface.prototype.GetExtendedEntityState = function(player, ent) { var ret = { "armour": null, "attack": null, "barterMarket": null, "buildingAI": null, "healer": null, "obstruction": null, "turretParent":null, "promotion": null, "resourceDropsite": null, "resourceGatherRates": null, "resourceSupply": null, }; var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); var cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { var types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (var type of types) { ret.attack[type] = cmpAttack.GetAttackStrengths(type); var range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; var timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { // not a ranged attack, set some defaults ret.attack[type].elevationBonus = 0; ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } ret.attack[type].elevationBonus = range.elevationBonus; var cmpPosition = Engine.QueryInterface(ent, IID_Position); var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) { // For units, take the rage in front of it, no spread. So angle = 0 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); } else if(cmpPosition && cmpPosition.IsInWorld()) { // For buildings, take the average elevation around it. So angle = 2*pi ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI); } else { // not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } } var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); if (cmpArmour) { ret.armour = cmpArmour.GetArmourStrengths(); } var cmpAuras = Engine.QueryInterface(ent, IID_Auras) if (cmpAuras) { ret.auras = cmpAuras.GetDescriptions(); } var cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) { ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; } var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (cmpObstruction) { ret.obstruction = { "controlGroup": cmpObstruction.GetControlGroup(), "controlGroup2": cmpObstruction.GetControlGroup2(), }; } var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) { ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "gatherers": cmpResourceSupply.GetGatherers() }; } if (cmpMirage && cmpMirage.ResourceSupply()) { ret.resourceSupply = { "max": cmpMirage.GetMaxAmount(), "amount": cmpMirage.GetAmount(), "type": cmpMirage.GetType(), "isInfinite": cmpMirage.IsInfinite() }; } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) { ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes() }; } var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) { ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; } var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); ret.barterMarket = { "prices": cmpBarter.GetPrices() }; } var cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) { ret.healer = { "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses(), }; } return ret; }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var rot = {x:0, y:0, z:0}; var pos = {x:cmd.x,z:cmd.z}; pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z); var elevationBonus = cmd.elevationBonus || 0; var range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, extendedName) { var name = extendedName; // Special case for garrisoned units which have a extended template if (extendedName.indexOf("&") != -1) name = extendedName.slice(extendedName.indexOf("&")+1); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(name); if (!template) return null; return GetTemplateDataHelper(template, player); }; GuiInterface.prototype.GetTechnologyData = function(player, name) { var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager); var template = cmpTechTempMan.GetTemplate(name); if (!template) { warn("Tried to get data for invalid technology: " + name); return null; } var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); return GetTechnologyDataHelper(template, cmpPlayer.GetCiv()); }; GuiInterface.prototype.IsTechnologyResearched = function(player, tech) { if (!tech) return true; var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(tech); }; // Checks whether the requirements for this technology have been met GuiInterface.prototype.CheckTechnologyRequirements = function(player, tech) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(tech); }; // Returns technologies that are being actively researched, along with // which entity is researching them and how far along the research is. GuiInterface.prototype.GetStartedResearch = function(player) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; var ret = {}; for (var tech in cmpTechnologyManager.GetTechsStarted()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; var cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); if (cmpProductionQueue) ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress; else ret[tech].progress = 0; } return ret; }; // Returns the battle state of the player. GuiInterface.prototype.GetBattleState = function(player) { var cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (cmpBattleDetection) return cmpBattleDetection.GetState(); else return false; }; // Returns a list of ongoing attacks against the player. GuiInterface.prototype.GetIncomingAttacks = function(player) { var cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); return cmpAttackDetection.GetIncomingAttacks(); }; // Used to show a red square over GUI elements you can't yet afford. GuiInterface.prototype.GetNeededResources = function(player, amounts) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); return cmpPlayer.GetNeededResources(amounts); }; GuiInterface.prototype.AddTimeNotification = function(notification) { var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); notification.endTime = notification.duration + time; notification.id = ++this.timeNotificationID; this.timeNotifications.push(notification); this.timeNotifications.sort(function (n1, n2){return n2.endTime - n1.endTime}); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { for (var i in this.timeNotifications) { if (this.timeNotifications[i].id == notificationID) { this.timeNotifications.splice(i); return; } } }; GuiInterface.prototype.GetTimeNotifications = function(playerID) { var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); var toDelete = []; for (var n of this.timeNotifications) { n.time = n.endTime - time; if (n.time < 0) toDelete.push(n.id); } for (var id of toDelete) this.DeleteTimeNotification(id); return this.timeNotifications; }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") { if (!notification.duration) notification.duration = 10000; this.AddTimeNotification(notification); return; } this.notifications.push(notification); }; GuiInterface.prototype.GetNextNotification = function() { if (this.notifications.length) return this.notifications.pop(); else return false; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(wantedPlayer), IID_Player); return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { var r = {}; var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return r; r.name = template.Formation.FormationName; r.tooltip = template.Formation.DisabledTooltip || ""; r.icon = template.Formation.Icon; return r; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { // GetLastFormationName is named in a strange way as it (also) is // the value of the current formation (see Formation.js LoadFormation) if (cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate) return true; } } return false; }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmpUnitAI.GetStanceName() == data.stance) return true; } } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { var buildableEnts = []; for each (var ent in cmd.entities) { var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (var building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerColors = {}; // cache of owner -> color map for each (var ent in cmd.entities) { var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color: var owner = -1; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); var color = playerColors[owner]; if (!color) { color = {"r":1, "g":1, "b":1}; var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player); if (cmpPlayer) color = cmpPlayer.GetColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({"r":color.r, "g":color.g, "b":color.b, "a":cmd.alpha}, cmd.selected); } }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { for each (var ent in cmd.entities) { var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.SetEnabled(cmd.enabled); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); // If there are some rally points already displayed, first hide them for each (var ent in this.entsRallyPointsDisplayed) { var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities for each (var ent in cmd.entities) { var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location) var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position var pos; if (cmd.x && cmd.z) pos = cmd; else pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set if (pos) { // Only update the position if we changed it (cmd.queued is set) if ("queued" in cmd) if (cmd.queued == true) cmpRallyPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z else cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z // rebuild the renderer when not set (when reading saved game or in case of building update) else if (!cmpRallyPointRenderer.IsSet()) for each (var posi in cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition({'x': posi.x, 'y': posi.z}); cmpRallyPointRenderer.SetDisplayed(true); // remember which entities have their rally points displayed so we can hide them again this.entsRallyPointsDisplayed.push(ent); } } }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { var result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [], } // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") { this.placementEntity = undefined; } else { this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } } if (this.placementEntity) { var ent = this.placementEntity[1]; // Move the preview into the right location var pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); // Set it to a red shade if this is an invalid location var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * 'populationBonus': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { var wallSet = cmd.wallSet; var start = { "pos": cmd.start, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; var end = { "pos": cmd.end, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; // -------------------------------------------------------------------------------- // do some entity cache management and check for snapping if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // we're clearing the preview, clear the entity cache and bail var numCleared = 0; for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // keep template data around } return false; } else { // Move all existing cached entities outside of the world and reset their use count for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) { var pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before for each (var tpl in wallSet.templates) { if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, tpl), }; // ensure that the loaded template data contains a wallPiece component if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } } // prevent division by zero errors further on if the start and end positions are the same if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { var snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error var startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { var endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // clear the single-building preview entity (we'll be rolling our own) this.SetBuildingPlacementPreview(player, {"template": ""}); // -------------------------------------------------------------------------------- // calculate wall placement and position preview entities var result = { "pieces": [], "cost": {"food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0}, }; var previewEntities = []; if (end.pos) previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length > 0 && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group var startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { var cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) { previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true, // preview only, must not appear in the result }); } } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle) }); } if (end.pos) { // Analogous to the starting side case above if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { var endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []); previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup()); } // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group var endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { var cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) { previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true }); } } } else { previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle) }); } } var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. var allPiecesValid = true; var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity for (var i = 0; i < previewEntities.length; ++i) { var entInfo = previewEntities[i]; var ent = null; var tpl = entInfo.template; var tplData = this.placementWallEntities[tpl].templateData; var entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { // allocate new entity ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else { // reuse an existing one ent = entPool.entities[entPool.numUsed]; } if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // move piece to right location // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces if (tpl === wallSet.templates.tower) { var terrainGroundPrev = null; var terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { var targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. var primaryControlGroup = ent; var secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); // check whether this wall piece can be validly positioned here var validPlacement = false; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta var visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden"); if (visible) { var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success); // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) numRequiredPieces++; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. result.cost.food += tplData.cost.food; result.cost.wood += tplData.cost.wood; result.cost.stone += tplData.cost.stone; result.cost.metal += tplData.cost.metal; result.cost.population += tplData.cost.population; result.cost.populationBonus += tplData.cost.populationBonus; result.cost.time += tplData.cost.time; } var canAfford = true; var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) var canAfford = false; var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } entPool.numUsed++; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateMgr.GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest // (TODO: break unlikely ties by choosing the lowest entity ID) var minDist2 = -1; var minDistEntitySnapData = null; var radius2 = data.snapRadius * data.snapRadius; for each (var ent in data.snapEntities) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; var pos = cmpPosition.GetPosition(); var dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = {"x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent}; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (template.BuildRestrictions.Category == "Dock") { var angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return {"x": data.x, "z": data.z, "angle": angle}; } return false; }; GuiInterface.prototype.PlaySound = function(player, data) { // Ignore if no entity was passed if (!data.entity) return; PlaySound(data.name, data.entity); }; GuiInterface.prototype.FindIdleUnits = function(player, data) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerEntities = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) { var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned()) return false; var cmpIdentity = Engine.QueryInterface(e, IID_Identity); if (!cmpIdentity || !cmpIdentity.HasClass(data.idleClass)) return false; return true; }); var idleUnits = []; for (var j = 0; j < playerEntities.length; ++j) { var ent = playerEntities[j]; if (ent <= data.prevUnit|0 || data.excludeUnits.indexOf(ent) > -1) continue; idleUnits.push(ent); playerEntities.splice(j--, 1); if (data.limit && idleUnits.length >= data.limit) break; } return idleUnits; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { var cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; var firstMarket = cmpEntityTrader.GetFirstMarket(); var secondMarket = cmpEntityTrader.GetSecondMarket(); var result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGain(); } else if (data.target === secondMarket) { result = { "type": "is second", "gain": cmpEntityTrader.GetGain(), }; } else if (!firstMarket) { result = {"type": "set first"}; } else if (!secondMarket) { result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; } else { // Else both markets are not null and target is different from them result = {"type": "set first"}; } return result; }; GuiInterface.prototype.CanAttack = function(player, data) { var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); if (!cmpAttack) return false; + var cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player); + var cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player); + if (!cmpEntityPlayer || !cmpTargetPlayer) + return false; + - return cmpAttack.CanAttack(data.target); + // if the owner is an enemy, it's up to the attack component to decide + if (!cmpEntityPlayer.IsAlly(cmpTargetPlayer.GetPlayerID())) + return cmpAttack.CanAttack(data.target); + + // if the owner is an ally, we could still want to capture some capture points back + var cmpCapturable = Engine.QueryInterface(data.target, IID_Capturable); + if (cmpCapturable && cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID()) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1) + return cmpAttack.CanAttack(data.target); + + return false; }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { var cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); if (!cmpProductionQueue) return 0; return cmpProductionQueue.GetBatchTime(data.batchSize); }; GuiInterface.prototype.IsMapRevealed = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); cmpPathfinder.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); cmpObstructionManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for each (var ent in data.entities) { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var traders = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) { return Engine.QueryInterface(e, IID_Trader); }); var landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; var shipTrader = { "total": 0, "trading": 0 }; for each (var ent in traders) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { var holder = cmpUnitAI.order.data.target; var cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player, tradingGoods) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); return cmpPlayer.GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is // trusted and indicates the player associated with the current client; no data should // be returned unless this player is meant to be able to see it.) var exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetExtendedEntityState": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "GetTechnologyData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNextNotification": 1, "GetTimeNotifications": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "DisplayRallyPoint": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); else throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 16550) @@ -1,146 +1,174 @@ const VIS_HIDDEN = 0; const VIS_FOGGED = 1; const VIS_VISIBLE = 2; function Mirage() {} Mirage.prototype.Schema = "Mirage entities replace real entities in the fog-of-war." + ""; Mirage.prototype.Init = function() { this.player = null; this.parent = INVALID_ENTITY; this.foundation = false; this.buildPercentage = null; this.health = false; this.maxHitpoints = null; this.hitpoints = null; this.needsRepair = null; + this.capturable = false; + this.capturePoints = []; + this.maxCapturePoints = 0; + this.resourceSupply = false; this.maxAmount = null; this.amount = null; this.type = null; this.isInfinite = null; }; Mirage.prototype.SetParent = function(ent) { this.parent = ent; }; Mirage.prototype.GetPlayer = function() { return this.player; }; Mirage.prototype.SetPlayer = function(player) { this.player = player; }; // ============================ // Parent entity data // Foundation data Mirage.prototype.CopyFoundation = function(buildPercentage) { this.foundation = true; this.buildPercentage = buildPercentage; }; Mirage.prototype.Foundation = function() { return this.foundation; }; Mirage.prototype.GetBuildPercentage = function() { return this.buildPercentage; }; // Health data Mirage.prototype.CopyHealth = function(maxHitpoints, hitpoints, needsRepair) { this.health = true; this.maxHitpoints = maxHitpoints; this.hitpoints = Math.ceil(hitpoints); this.needsRepair = needsRepair; }; Mirage.prototype.Health = function() { return this.health; }; Mirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; }; Mirage.prototype.GetHitpoints = function() { return this.hitpoints; }; Mirage.prototype.NeedsRepair = function() { return this.needsRepair; }; +// Capture data + +Mirage.prototype.CopyCapturable = function(capturePoints, maxCapturePoints) +{ + this.capturable = true; + this.capturePoints = capturePoints; + this.maxCapturePoints = maxCapturePoints; +}; + +Mirage.prototype.Capturable = function() +{ + return this.capturable; +}; + +Mirage.prototype.GetMaxCapturePoints = function() +{ + return this.maxCapturePoints; +}; + +Mirage.prototype.GetCapturePoints = function() +{ + return this.capturePoints; +}; + // ResourceSupply data Mirage.prototype.CopyResourceSupply = function(maxAmount, amount, type, isInfinite) { this.resourceSupply = true; this.maxAmount = maxAmount; this.amount = amount; this.type = type; this.isInfinite = isInfinite; }; Mirage.prototype.ResourceSupply = function() { return this.resourceSupply; }; Mirage.prototype.GetMaxAmount = function() { return this.maxAmount; }; Mirage.prototype.GetAmount = function() { return this.amount; }; Mirage.prototype.GetType = function() { return this.type; }; Mirage.prototype.IsInfinite = function() { return this.isInfinite; }; // ============================ Mirage.prototype.OnVisibilityChanged = function(msg) { if (msg.player != this.player || msg.newVisibility != VIS_HIDDEN) return; if (this.parent == INVALID_ENTITY) Engine.DestroyEntity(this.entity); else Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: this.parent }); }; Engine.RegisterComponentType(IID_Mirage, "Mirage", Mirage); Index: ps/trunk/binaries/data/mods/public/simulation/components/Pack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Pack.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/Pack.js (revision 16550) @@ -1,191 +1,201 @@ function Pack() {} const PACKING_INTERVAL = 250; Pack.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "packed" + "unpacked" + "" + ""; Pack.prototype.Init = function() { this.packed = (this.template.State == "packed"); this.packing = false; this.elapsedTime = 0; this.timer = undefined; }; Pack.prototype.OnDestroy = function() { this.CancelTimer(); }; Pack.prototype.CancelTimer = function() { if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } }; Pack.prototype.IsPacked = function() { return this.packed; }; Pack.prototype.IsPacking = function() { return this.packing; }; Pack.prototype.Pack = function() { // Ignore pointless pack command if (this.IsPacked() || this.IsPacking()) return; this.packing = true; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, {"packing": true}); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("packing", true, 1.0, "packing"); }; Pack.prototype.Unpack = function() { // Ignore pointless unpack command if (!this.IsPacked() || this.IsPacking()) return; this.packing = true; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Pack, "PackProgress", 0, PACKING_INTERVAL, {"packing": false}); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("unpacking", true, 1.0, "unpacking"); }; Pack.prototype.CancelPack = function() { // Ignore pointless cancel command if (!this.IsPacking()) return; this.CancelTimer(); this.packing = false; this.SetElapsedTime(0); // Clear animation var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0, ""); }; Pack.prototype.GetPackTime = function() { return ApplyValueModificationsToEntity("Pack/Time", +this.template.Time, this.entity); }; Pack.prototype.GetElapsedTime = function() { return this.elapsedTime; }; Pack.prototype.GetProgress = function() { return this.elapsedTime / this.GetPackTime(); }; Pack.prototype.SetElapsedTime = function(time) { this.elapsedTime = time; Engine.PostMessage(this.entity, MT_PackProgressUpdate, { progress: this.elapsedTime }); }; Pack.prototype.PackProgress = function(data, lateness) { if (this.elapsedTime >= this.GetPackTime()) { this.CancelTimer(); this.packed = !this.packed; Engine.PostMessage(this.entity, MT_PackFinished, { packed: this.packed }); // Done un/packing, copy our parameters to the final entity var newEntity = Engine.AddEntity(this.template.Entity); if (newEntity == INVALID_ENTITY) { // Error (e.g. invalid template names) error("PackProgress: Error creating entity for '" + this.template.Entity + "'"); return; } var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpNewPosition = Engine.QueryInterface(newEntity, IID_Position); if (cmpPosition.IsInWorld()) { var pos = cmpPosition.GetPosition2D(); cmpNewPosition.JumpTo(pos.x, pos.y); } var rot = cmpPosition.GetRotation(); cmpNewPosition.SetYRotation(rot.y); cmpNewPosition.SetXZRotation(rot.x, rot.z); cmpNewPosition.SetHeightOffset(cmpPosition.GetHeightOffset()); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpNewOwnership = Engine.QueryInterface(newEntity, IID_Ownership); cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); + // rescale capture points + var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable); + var cmpNewCapturable = Engine.QueryInterface(newEntity, IID_Capturable); + if (cmpCapturable && cmpNewCapturable) + { + let scale = cmpCapturable.GetMaxCapturePoints() / cmpNewCapturable.GetMaxCapturePoints(); + let newCp = cmpCapturable.GetCapturePoints().map(function (v) { return v / scale; }); + cmpNewCapturable.SetCapturePoints(newCp); + } + // Maintain current health level var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); var cmpNewHealth = Engine.QueryInterface(newEntity, IID_Health); var healthLevel = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints())); cmpNewHealth.SetHitpoints(Math.round(cmpNewHealth.GetMaxHitpoints() * healthLevel)); var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); var cmpNewUnitAI = Engine.QueryInterface(newEntity, IID_UnitAI); if (cmpUnitAI && cmpNewUnitAI) { var pos = cmpUnitAI.GetHeldPosition(); if (pos) cmpNewUnitAI.SetHeldPosition(pos.x, pos.z); if (cmpUnitAI.GetStanceName()) cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName()); cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders()); cmpNewUnitAI.SetGuardOf(cmpUnitAI.IsGuardOf()); } // Maintain the list of guards var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard); var cmpNewGuard = Engine.QueryInterface(newEntity, IID_Guard); if (cmpGuard && cmpNewGuard) cmpNewGuard.SetEntities(cmpGuard.GetEntities()); Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: newEntity }); // Play notification sound var sound = this.packed ? "packed" : "unpacked"; PlaySound(sound, newEntity); // Destroy current entity Engine.DestroyEntity(this.entity); } else { this.SetElapsedTime(this.GetElapsedTime() + PACKING_INTERVAL + lateness); } }; Engine.RegisterComponentType(IID_Pack, "Pack", Pack); Index: ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 16550) @@ -1,100 +1,102 @@ function TerritoryDecay() {} TerritoryDecay.prototype.Schema = - "" + + "" + "" + ""; TerritoryDecay.prototype.Init = function() { this.timer = undefined; this.decaying = false; }; TerritoryDecay.prototype.IsConnected = function() { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; // Prevent special gaia buildings from decaying (e.g. fences, ruins) if (cmpOwnership.GetOwner() == 0) return true; var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); if (!cmpTerritoryManager) return false; var pos = cmpPosition.GetPosition2D(); var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); if (tileOwner != cmpOwnership.GetOwner()) return false; - // TODO: this should probably use the same territory restriction - // logic as BuildRestrictions, to handle allies etc return cmpTerritoryManager.IsConnected(pos.x, pos.y); }; TerritoryDecay.prototype.IsDecaying = function() { return this.decaying; }; TerritoryDecay.prototype.UpdateDecayState = function() { var connected = this.IsConnected(); if (!connected && !this.timer) { // Start decaying var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_TerritoryDecay, "Decay", 1000, 1000, {}); } else if (connected && this.timer) { // Stop decaying var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } if (connected) var decaying = false; else - var decaying = (Math.round(ApplyValueModificationsToEntity("TerritoryDecay/HealthDecayRate", +this.template.HealthDecayRate, this.entity)) > 0); + var decaying = (Math.round(ApplyValueModificationsToEntity("TerritoryDecay/DecayRate", +this.template.DecayRate, this.entity)) > 0); if (decaying === this.decaying) return; this.decaying = decaying; Engine.PostMessage(this.entity, MT_TerritoryDecayChanged, { "to": decaying }); }; TerritoryDecay.prototype.OnTerritoriesChanged = function(msg) { this.UpdateDecayState(); }; TerritoryDecay.prototype.OnTerritoryPositionChanged = function(msg) { this.UpdateDecayState(); }; TerritoryDecay.prototype.OnOwnershipChanged = function(msg) { this.UpdateDecayState(); }; TerritoryDecay.prototype.Decay = function() { - var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); - if (!cmpHealth) + var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable); + if (!cmpCapturable) return; // error - var decayRate = ApplyValueModificationsToEntity("TerritoryDecay/HealthDecayRate", +this.template.HealthDecayRate, this.entity); + var decayRate = ApplyValueModificationsToEntity( + "TerritoryDecay/DecayRate", + +this.template.DecayRate, + this.entity); - cmpHealth.Reduce(Math.round(decayRate)); + // Reduce capture points in favour of Gaia + cmpCapturable.Reduce(decayRate, 0); }; Engine.RegisterComponentType(IID_TerritoryDecay, "TerritoryDecay", TerritoryDecay); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 16550) @@ -1,5867 +1,5872 @@ function UnitAI() {} UnitAI.prototype.Schema = "Controls the unit's movement, attacks, etc, in response to commands from the player." + "" + "" + "" + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "standground" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "skittish" + "domestic" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + ""; // Unit stances. // There some targeting options: // targetVisibleEnemies: anything in vision range is a viable target // targetAttackersAlways: anything that hurts us is a viable target, // possibly overriding user orders! // targetAttackersPassive: anything that hurts us is a viable target, // if we're on a passive/unforced order (e.g. gathering/building) // There are some response options, triggered when targets are detected: // respondFlee: run away // respondChase: start chasing after the enemy // respondChaseBeyondVision: start chasing, and don't stop even if it's out // of this unit's vision range (though still visible to the player) // respondStandGround: attack enemy but don't move at all // respondHoldGround: attack enemy but don't move far from current position // TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts, // do worry around armies slaughtering the guy standing next to you), etc. var g_Stances = { "violent": { targetVisibleEnemies: true, targetAttackersAlways: true, targetAttackersPassive: true, respondFlee: false, respondChase: true, respondChaseBeyondVision: true, respondStandGround: false, respondHoldGround: false, }, "aggressive": { targetVisibleEnemies: true, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: false, respondChase: true, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: false, }, "defensive": { targetVisibleEnemies: true, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: false, respondChase: false, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: true, }, "passive": { targetVisibleEnemies: false, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: true, respondChase: false, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: false, }, "standground": { targetVisibleEnemies: true, targetAttackersAlways: false, targetAttackersPassive: true, respondFlee: false, respondChase: false, respondChaseBeyondVision: false, respondStandGround: true, respondHoldGround: false, }, }; // See ../helpers/FSM.js for some documentation of this FSM specification syntax UnitAI.prototype.UnitFsmSpec = { // Default event handlers: "MoveCompleted": function() { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }, "MoveStarted": function() { // ignore spurious movement messages }, "ConstructionFinished": function(msg) { // ignore uninteresting construction messages }, "LosRangeUpdate": function(msg) { // ignore newly-seen units by default }, "LosHealRangeUpdate": function(msg) { // ignore newly-seen injured units by default }, "Attacked": function(msg) { // ignore attacker }, "HealthChanged": function(msg) { // ignore }, "PackFinished": function(msg) { // ignore }, "PickupCanceled": function(msg) { // ignore }, "GuardedAttacked": function(msg) { // ignore }, // Formation handlers: "FormationLeave": function(msg) { // ignore when we're not in FORMATIONMEMBER }, // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z); this.SetNextStateAlwaysEntering("FORMATIONMEMBER.WALKING"); }, // Special orders: // (these will be overridden by various states) "Order.LeaveFoundation": function(msg) { // If foundation is not ally of entity, or if entity is unpacked siege, // ignore the order if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack() || this.IsTurret()) { this.FinishOrder(); return; } // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range); if (ok) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, // Individual orders: // (these will switch the unit out of formation mode) "Order.Stop": function(msg) { // We have no control over non-domestic animals. if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // Stop moving immediately. this.StopMoving(); this.FinishOrder(); // No orders left, we're an individual now if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); }, "Order.Walk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } this.SetHeldPosition(this.order.data.x, this.order.data.z); if (!this.order.data.max) this.MoveToPoint(this.order.data.x, this.order.data.z); else this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else this.SetNextState("INDIVIDUAL.WALKING"); }, "Order.WalkAndFight": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } this.SetHeldPosition(this.order.data.x, this.order.data.z); this.MoveToPoint(this.order.data.x, this.order.data.z); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); // WalkAndFight not applicable for animals else this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING"); }, "Order.WalkToTarget": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } var ok = this.MoveToTarget(this.order.data.target); if (ok) { // We've started walking to the given point if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }, "Order.PickupUnit": function(msg) { var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) { this.FinishOrder(); return; } // Check if we need to move TODO implement a better way to know if we are on the shoreline var needToMove = true; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x) && (this.lastShorelinePosition.z == cmpPosition.GetPosition().z)) { // we were already on the shoreline, and have not moved since if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50) needToMove = false; } // TODO: what if the units are on a cliff ? the ship will go below the cliff // and the units won't be able to garrison. Should go to the nearest (accessible) shore if (needToMove && this.MoveToTarget(this.order.data.target)) { this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); } }, "Order.Guard": function(msg) { if (!this.AddGuard(this.order.data.target)) { this.FinishOrder(); return; } if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("INDIVIDUAL.GUARD.ESCORTING"); else this.SetNextState("INDIVIDUAL.GUARD.GUARDING"); }, "Order.Flee": function(msg) { // We use the distance between the entities to account for ranged attacks var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1)) { // We've started fleeing from the given target if (this.IsAnimal()) this.SetNextState("ANIMAL.FLEEING"); else this.SetNextState("INDIVIDUAL.FLEEING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }, "Order.Attack": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Work out how to attack the given target var type = this.GetBestAttackAgainst(this.order.data.target); if (!type) { // Oops, we can't attack at all this.FinishOrder(); return; } this.order.data.attackType = type; // If we are already at the target, try attacking it from here if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.StopMoving(); // For packable units within attack range: // 1. If unpacked, we can attack the target. // 2. If packed, we first need to unpack, then follow case 1. if (this.CanUnpack()) { // Ignore unforced attacks // TODO: use special stances instead? if (!this.order.data.force) { this.FinishOrder(); return; } // Case 2: unpack this.PushOrderFront("Unpack", { "force": true }); return; } if (this.order.data.attackType == this.oldAttackType) { if (this.IsAnimal()) this.SetNextState("ANIMAL.COMBAT.ATTACKING"); else this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); } else { if (this.IsAnimal()) this.SetNextStateAlwaysEntering("ANIMAL.COMBAT.ATTACKING"); else this.SetNextStateAlwaysEntering("INDIVIDUAL.COMBAT.ATTACKING"); } return; } // For packable units out of attack range: // 1. If packed, we need to move to attack range and then unpack. // 2. If unpacked, we first need to pack, then follow case 1. var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) { // Ignore unforced attacks // TODO: use special stances instead? if (!this.order.data.force) { this.FinishOrder(); return; } if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } } // If we can't reach the target, but are standing ground, then abandon this attack order. // Unless we're hunting, that's a special case where we should continue attacking our target. if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret()) { this.FinishOrder(); return; } // Try to move within attack range if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { // We've started walking to the given point if (this.IsAnimal()) this.SetNextState("ANIMAL.COMBAT.APPROACHING"); else this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); return; } // We can't reach the target, and can't move towards it, // so abandon this attack order this.FinishOrder(); }, "Order.Heal": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Healers can't heal themselves. if (this.order.data.target == this.entity) { this.FinishOrder(); return; } // Check if the target is in range if (this.CheckTargetRange(this.order.data.target, IID_Heal)) { this.StopMoving(); this.SetNextState("INDIVIDUAL.HEAL.HEALING"); return; } // If we can't reach the target, but are standing ground, // then abandon this heal order if (this.GetStance().respondStandGround && !this.order.data.force) { this.FinishOrder(); return; } // Try to move within heal range if (this.MoveToTargetRange(this.order.data.target, IID_Heal)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); return; } // We can't reach the target, and can't move towards it, // so abandon this heal order this.FinishOrder(); }, "Order.Gather": function(msg) { // If the target is still alive, we need to kill it first if (this.MustKillGatherTarget(this.order.data.target)) { // Make sure we can attack the target, else we'll get very stuck if (!this.GetBestAttackAgainst(this.order.data.target)) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed this.FinishOrder(); return; } // The target was visible when this order was issued, // but could now be invisible again. if (!this.CheckTargetVisible(this.order.data.target)) { if (this.order.data.secondTry === undefined) { this.order.data.secondTry = true; this.PushOrderFront("Walk", this.order.data.lastPos); } else { // We couldn't move there, or the target moved away this.FinishOrder(); } return; } this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true }); return; } // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.GATHER.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try gathering it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING"); } }, "Order.GatherNearPosition": function(msg) { // Move the unit to the position to gather from. this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("INDIVIDUAL.GATHER.WALKING"); }, "Order.ReturnResource": function(msg) { // Check if the dropsite is already in range if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true)) { var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes); // Our next order should always be a Gather, // so just switch back to that order this.FinishOrder(); return; } } // Try to move to the dropsite if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) { // We've started walking to the target this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING"); return; } // Oops, we can't reach the dropsite. // Maybe we should try to pick another dropsite, to find an // accessible one? // For now, just give up. this.StopMoving(); this.FinishOrder(); return; }, "Order.Trade": function(msg) { // We must check if this trader has both markets in case it was a back-to-work order var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader || ! cmpTrader.HasBothMarkets()) { this.FinishOrder(); return; } var nextMarket = cmpTrader.GetNextMarket(); if (nextMarket == this.order.data.firstMarket) var state = "TRADE.APPROACHINGFIRSTMARKET"; else var state = "TRADE.APPROACHINGSECONDMARKET"; // TODO find the nearest way-point from our position, and start with it this.waypoints = undefined; if (this.MoveToMarket(nextMarket)) // We've started walking to the next market this.SetNextState(state); else this.FinishOrder(); }, "Order.Repair": function(msg) { // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_Builder)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try repairing it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); this.SetNextStateAlwaysEntering("INDIVIDUAL.REPAIR.REPAIRING"); } }, "Order.Garrison": function(msg) { if (this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move to the garrison target. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } if (this.MoveToGarrisonRange(this.order.data.target)) { this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); } else { // We do a range check before actually garrisoning this.StopMoving(); this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); } }, "Order.Autogarrison": function(msg) { this.SetNextState("INDIVIDUAL.AUTOGARRISON"); }, "Order.Alert": function(msg) { this.alertRaiser = this.order.data.raiser; // Find a target to garrison into, if we don't already have one if (!this.alertGarrisoningTarget) this.alertGarrisoningTarget = this.FindNearbyGarrisonHolder(); if (this.alertGarrisoningTarget) this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); else this.FinishOrder(); }, "Order.Cheering": function(msg) { this.SetNextState("INDIVIDUAL.CHEERING"); }, "Order.Pack": function(msg) { if (this.CanPack()) { this.StopMoving(); this.SetNextState("INDIVIDUAL.PACKING"); } }, "Order.Unpack": function(msg) { if (this.CanUnpack()) { this.StopMoving(); this.SetNextState("INDIVIDUAL.UNPACKING"); } }, "Order.CancelPack": function(msg) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) cmpPack.CancelPack(); this.FinishOrder(); }, "Order.CancelUnpack": function(msg) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) cmpPack.CancelPack(); this.FinishOrder(); }, // States for the special entity representing a group of units moving in formation: "FORMATIONCONTROLLER": { "Order.Walk": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKING"); }, "Order.WalkAndFight": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKINGANDFIGHTING"); }, "Order.MoveIntoFormation": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("FORMING"); }, // Only used by other orders to walk there in formation "Order.WalkToTargetRange": function(msg) { if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.WalkToTarget": function(msg) { if (this.MoveToTarget(this.order.data.target)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.WalkToPointRange": function(msg) { if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.Guard": function(msg) { this.CallMemberFunction("Guard", [msg.data.target, false]); var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.Disband(); }, "Order.Stop": function(msg) { this.CallMemberFunction("Stop", [false]); this.FinishOrder(); }, "Order.Attack": function(msg) { var target = msg.data.target; var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) target = cmpTargetUnitAI.GetFormationController(); var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); // Check if we are already in range, otherwise walk there if (!this.CheckTargetAttackRange(target, target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { if (this.MoveToTargetAttackRange(target, target)) { this.SetNextState("COMBAT.APPROACHING"); return; } } this.FinishOrder(); return; } this.CallMemberFunction("Attack", [target, false]); if (cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); }, "Order.Garrison": function(msg) { if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder)) { this.FinishOrder(); return; } // Check if we are already in range, otherwise walk there if (!this.CheckGarrisonRange(msg.data.target)) { if (!this.CheckTargetVisible(msg.data.target)) { this.FinishOrder(); return; } else { // Out of range; move there in formation if (this.MoveToGarrisonRange(msg.data.target)) { this.SetNextState("GARRISON.APPROACHING"); return; } } } this.SetNextState("GARRISON.GARRISONING"); }, "Order.Gather": function(msg) { if (this.MustKillGatherTarget(msg.data.target)) { // The target was visible when this order was given, // but could now be invisible. if (!this.CheckTargetVisible(msg.data.target)) { if (msg.data.secondTry === undefined) { msg.data.secondTry = true; this.PushOrderFront("Walk", msg.data.lastPos); } else { // We couldn't move there, or the target moved away this.FinishOrder(); } return; } this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true }); return; } // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target isn't gatherable or not visible any more. this.FinishOrder(); // TODO: Should we issue a gather-near-position order // if the target isn't gatherable/doesn't exist anymore? else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("Gather", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.GatherNearPosition": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20)) { // Out of range; move there in formation this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 }); return; } this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Heal": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("Heal", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Repair": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The building was finished or destroyed this.FinishOrder(); else // Out of range move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.ReturnResource": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("ReturnResource", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Pack": function(msg) { this.CallMemberFunction("Pack", [false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Unpack": function(msg) { this.CallMemberFunction("Unpack", [false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "IDLE": { "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(false); }, }, "WALKING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { if (this.FinishOrder()) this.CallMemberFunction("ResetFinishOrder", []); }, }, "WALKINGANDFIGHTING": { "enter": function(msg) { this.StartTimer(0, 1000); }, "Timer": function(msg) { // check if there are no enemies to attack this.FindWalkAndFightTargets(); }, "leave": function(msg) { this.StopTimer(); }, "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { if (this.FinishOrder()) this.CallMemberFunction("ResetFinishOrder", []); }, }, "GARRISON":{ "enter": function() { // If the garrisonholder should pickup, warn it so it can take needed action var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder); if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity)) { this.pickup = this.order.data.target; // temporary, deleted in "leave" Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity }); } }, "leave": function() { // If a pickup has been requested and not yet canceled, cancel it if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } }, "APPROACHING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { this.SetNextState("GARRISONING"); }, }, "GARRISONING": { "enter": function() { // If a pickup has been requested, cancel it as it will be requested by members if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } this.CallMemberFunction("Garrison", [this.order.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, }, }, "FORMING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, false); }, "MoveCompleted": function(msg) { if (this.FinishOrder()) { this.CallMemberFunction("ResetFinishOrder", []); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.FindInPosition(); } }, "COMBAT": { "APPROACHING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.CallMemberFunction("Attack", [this.order.data.target, false]); if (cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); }, }, "ATTACKING": { // Wait for individual members to finish "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(false, false); this.StartTimer(200, 200); var target = this.order.data.target; // Check if we are already in range, otherwise walk there if (!this.CheckTargetAttackRange(target, target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); var range = cmpAttack.GetRange(target); this.PushOrderFront("WalkToTargetRange", { "target": target, "min": range.min, "max": range.max }); return; } this.FinishOrder(); return; } }, "Timer": function(msg) { var target = this.order.data.target; // Check if we are already in range, otherwise walk there if (!this.CheckTargetAttackRange(target, target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); var range = cmpAttack.GetRange(target); this.FinishOrder(); this.PushOrderFront("Attack", { "target": target, "force": false }); return; } this.FinishOrder(); return; } }, "leave": function(msg) { this.StopTimer(); }, }, }, "MEMBER": { // Wait for individual members to finish "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(false); this.StartTimer(1000, 1000); }, "Timer": function(msg) { // Have all members finished the task? if (!this.TestAllMemberFunction("HasFinishedOrder", [])) return; this.CallMemberFunction("ResetFinishOrder", []); // Execute the next order if (this.FinishOrder()) { // if WalkAndFight order, look for new target before moving again if (this.IsWalkingAndFighting()) this.FindWalkAndFightTargets(); return; } }, "leave": function(msg) { this.StopTimer(); }, }, }, // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { // We're not in a formation anymore, so no need to track this. this.finishedOrder = false; // Stop moving as soon as the formation disbands this.StopMoving(); // If the controller handled an order but some members rejected it, // they will have no orders and be in the FORMATIONMEMBER.IDLE state. if (this.orderQueue.length) { // We're leaving the formation, so stop our FormationWalk order if (this.FinishOrder()) return; } // No orders left, we're an individual now if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); }, // Override the LeaveFoundation order since we're not doing // anything more important (and we might be stuck in the WALKING // state forever and need to get out of foundations in that case) "Order.LeaveFoundation": function(msg) { // If foundation is not ally of entity, or if entity is unpacked siege, // ignore the order if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) || this.IsPacking() || this.CanPack() || this.IsTurret()) { this.FinishOrder(); return; } // Move a tile outside the building var range = 4; var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range); if (ok) { // We've started walking to the given point this.SetNextState("WALKINGTOPOINT"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, "IDLE": { "enter": function() { if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); return true; }, }, "WALKING": { "enter": function () { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpFormation && cmpVisual) { cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk")); cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run")); } this.SelectAnimation("move"); }, // Occurs when the unit has reached its destination and the controller // is done moving. The controller is notified. "MoveCompleted": function(msg) { // We can only finish this order if the move was really completed. if (!msg.data.error && this.FinishOrder()) return; var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { cmpVisual.ResetMoveAnimation("walk"); cmpVisual.ResetMoveAnimation("run"); } var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.SetInPosition(this.entity); }, }, // Special case used by Order.LeaveFoundation "WALKINGTOPOINT": { "enter": function() { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.UnsetInPosition(this.entity); this.SelectAnimation("move"); }, "MoveCompleted": function() { this.FinishOrder(); }, }, }, // States for entities not part of a formation: "INDIVIDUAL": { "enter": function() { // Sanity-checking if (this.IsAnimal()) error("Animal got moved into INDIVIDUAL.* state"); }, "Attacked": function(msg) { // Respond to attack if we always target attackers, or if we target attackers // during passive orders (e.g. gathering/repairing are never forced) if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (!this.order || !this.order.data || !this.order.data.force))) { this.RespondToTargetedEntities([msg.data.attacker]); } }, "GuardedAttacked": function(msg) { // do nothing if we have a forced order in queue before the guard order for (var i = 0; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].type == "Guard") break; if (this.orderQueue[i].data && this.orderQueue[i].data.force) return; } // if we already are targeting another unit still alive, finish with it first if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack")) if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker)) return; var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health); if (cmpIdentity && cmpIdentity.HasClass("Support") && cmpHealth && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable()) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } // if the attacker is a building and we can repair the guarded, repair it rather than attacking var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI); if (cmpBuildingAI && this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable()) { this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } // target the unit if (this.CheckTargetVisible(msg.data.attacker)) this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false }); else { var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false }); // if we already had a WalkAndFight, keep only the most recent one in case the target has moved if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight") this.orderQueue.splice(1, 1); } }, "IDLE": { "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation var animationName = "idle"; if (this.IsFormationMember()) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); } this.SelectAnimation(animationName); // If the unit is guarding/escorting, go back to its duty if (this.isGuardOf) { this.Guard(this.isGuardOf, false); return true; } // The GUI and AI want to know when a unit is idle, but we don't // want to send frequent spurious messages if the unit's only // idle for an instant and will quickly go off and do something else. // So we'll set a timer here and only report the idle event if we // remain idle this.StartTimer(1000); // If a unit can heal and attack we first want to heal wounded units, // so check if we are a healer and find whether there's anybody nearby to heal. // (If anyone approaches later it'll be handled via LosHealRangeUpdate.) // If anyone in sight gets hurt that will be handled via LosHealRangeUpdate. if (this.IsHealer() && this.FindNewHealTargets()) return true; // (abort the FSM transition since we may have already switched state) // If we entered the idle state we must have nothing better to do, // so immediately check whether there's anybody nearby to attack. // (If anyone approaches later, it'll be handled via LosRangeUpdate.) if (this.FindNewTargets()) return true; // (abort the FSM transition since we may have already switched state) // Nobody to attack - stay in idle return false; }, "leave": function() { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) rangeMan.DisableActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) rangeMan.DisableActiveQuery(this.losHealRangeQuery); this.StopTimer(); if (this.isIdle) { this.isIdle = false; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, "LosRangeUpdate": function(msg) { if (this.GetStance().targetVisibleEnemies) { // Start attacking one of the newly-seen enemy (if any) this.AttackEntitiesByPreference(msg.data.added); } }, "LosHealRangeUpdate": function(msg) { this.RespondToHealableEntities(msg.data.added); }, "Timer": function(msg) { if (!this.isIdle) { this.isIdle = true; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, }, "WALKING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.FinishOrder(); }, }, "WALKINGANDFIGHTING": { "enter": function () { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.StartTimer(0, 1000); this.SelectAnimation("move"); }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "leave": function(msg) { this.StopTimer(); }, "MoveCompleted": function() { this.FinishOrder(); }, }, "GUARD": { "RemoveGuard": function() { this.StopMoving(); this.FinishOrder(); }, "ESCORTING": { "enter": function () { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.StartTimer(0, 1000); this.SelectAnimation("move"); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, "Timer": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.isGuardOf)) { this.StopMoving(); this.FinishOrder(); return; } this.SetHeldPositionOnEntity(this.isGuardOf); }, "leave": function(msg) { this.SetMoveSpeed(this.GetWalkSpeed()); this.StopTimer(); }, "MoveStarted": function(msg) { // Adapt the speed to the one of the target if needed var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange)) { var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI); if (cmpUnitAI) { var speed = cmpUnitAI.GetWalkSpeed(); if (speed < this.GetWalkSpeed()) this.SetMoveSpeed(speed); } } }, "MoveCompleted": function() { this.SetMoveSpeed(this.GetWalkSpeed()); if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("GUARDING"); }, }, "GUARDING": { "enter": function () { this.StartTimer(1000, 1000); this.SetHeldPositionOnEntity(this.entity); this.SelectAnimation("idle"); return false; }, "LosRangeUpdate": function(msg) { // Start attacking one of the newly-seen enemy (if any) if (this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.isGuardOf)) { this.FinishOrder(); return; } // then check is the target has moved if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("ESCORTING"); else { // if nothing better to do, check if the guarded needs to be healed or repaired var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health); if (cmpHealth && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable()) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); } } }, "leave": function(msg) { this.StopTimer(); }, }, }, "FLEEING": { "enter": function() { this.PlaySound("panic"); // Run quickly var speed = this.GetRunSpeed(); this.SelectAnimation("move"); this.SetMoveSpeed(speed); }, "HealthChanged": function() { var speed = this.GetRunSpeed(); this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed this.SetMoveSpeed(this.GetWalkSpeed()); }, "MoveCompleted": function() { // When we've run far enough, stop fleeing this.FinishOrder(); }, // TODO: what if we run into more enemies while fleeing? }, "COMBAT": { "Order.LeaveFoundation": function(msg) { // Ignore the order as we're busy. return { "discardOrder": true }; }, "Attacked": function(msg) { // If we're already in combat mode, ignore anyone else // who's attacking us }, "APPROACHING": { "enter": function () { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { // Show carried resources when walking. this.SetGathererAnimationOverride(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { // If the unit needs to unpack, do so if (this.CanUnpack()) this.SetNextState("UNPACKING"); else this.SetNextState("ATTACKING"); } else { if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.SetNextState("APPROACHING"); } else { // Give up this.FinishOrder(); } } }, "Attacked": function(msg) { // If we're attacked by a close enemy, we should try to defend ourself // but only if we're not forced to target something else if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force))) { this.RespondToTargetedEntities([msg.data.attacker]); } }, }, "UNPACKING": { "enter": function() { // If we're not in range yet (maybe we stopped moving), move to target again if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) this.SetNextState("APPROACHING"); else // Give up this.FinishOrder(); return true; } // In range, unpack var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Unpack(); return false; }, "PackFinished": function(msg) { this.SetNextState("ATTACKING"); }, "leave": function() { }, "Attacked": function(msg) { // Ignore further attacks while unpacking }, }, "ATTACKING": { "enter": function() { var target = this.order.data.target; var cmpFormation = Engine.QueryInterface(target, IID_Formation); // if the target is a formation, save the attacking formation, and pick a member if (cmpFormation) { this.order.data.formationTarget = target; target = cmpFormation.GetClosestMember(this.entity); this.order.data.target = target; } // Check the target is still alive and attackable if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null) && !this.CheckTargetAttackRange(target, this.order.data.attackType)) { // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetAttackRange(target, this.order.data.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } } var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType); // If the repeat time since the last attack hasn't elapsed, // delay this attack to avoid attacking too fast. var prepare = this.attackTimers.prepare; if (this.lastAttacked) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } // add prefix + no capital first letter for attackType var animationName = "attack_" + this.order.data.attackType.toLowerCase(); if (this.IsFormationMember()) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); } this.SelectAnimation(animationName, false, 1.0, "attack"); this.SetAnimationSync(prepare, this.attackTimers.repeat); this.StartTimer(prepare, this.attackTimers.repeat); // TODO: we should probably only bother syncing projectile attacks, not melee // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false; this.FaceTowardsTarget(this.order.data.target); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { var target = this.order.data.target; var cmpFormation = Engine.QueryInterface(target, IID_Formation); // if the target is a formation, save the attacking formation, and pick a member if (cmpFormation) { var thisObject = this; var filter = function(t) { return thisObject.TargetIsAlive(t) && thisObject.CanAttack(t, thisObject.order.data.forceResponse || null); }; this.order.data.formationTarget = target; target = cmpFormation.GetClosestMember(this.entity, filter); this.order.data.target = target; } // Check the target is still alive and attackable if (this.TargetIsAlive(target) && this.CanAttack(target, this.order.data.forceResponse || null)) { // If we are hunting, first update the target position of the gather order so we know where will be the killed animal if (this.order.data.hunting && this.orderQueue[1] && this.orderQueue[1].data.lastPos) { var cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { // Store the initial position, so that we can find the rest of the herd later if (!this.orderQueue[1].data.initPos) this.orderQueue[1].data.initPos = this.orderQueue[1].data.lastPos; this.orderQueue[1].data.lastPos = cmpPosition.GetPosition(); // We still know where the animal is, so we shouldn't give up before going there this.orderQueue[1].data.secondTry = undefined; } } var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastAttacked = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); cmpAttack.PerformAttack(this.order.data.attackType, target); // Check we can still reach the target for the next attack if (this.CheckTargetAttackRange(target, this.order.data.attackType)) { if (this.resyncAnimation) { this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } } // if we're targetting a formation, find a new member of that formation var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation); // if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search if (target && cmpTargetFormation) { this.order.data.target = this.order.data.formationTarget; this.TimerHandler(msg.data, msg.lateness); return; } this.oldAttackType = this.order.data.attackType; // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up // Except if in WalkAndFight mode where we look for more ennemies around before moving again if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) this.FindWalkAndFightTargets(); return; } // See if we can switch to a new nearby enemy if (this.FindNewTargets()) { // Attempt to immediately re-enter the timer function, to avoid wasting the attack. if (this.orderQueue.length > 0 && this.orderQueue[0].data.attackType == this.oldAttackType) this.TimerHandler(msg.data, msg.lateness); return; } // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, // TODO: respond to target deaths immediately, rather than waiting // until the next Timer event "Attacked": function(msg) { if (this.order.data.target != msg.data.attacker) { // If we're attacked by a close enemy, stronger than our current target, // we choose to attack it, but only if we're not forced to target something else if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force))) { var ents = [this.order.data.target, msg.data.attacker]; SortEntitiesByPriority(ents); if (ents[0] != this.order.data.target) { this.RespondToTargetedEntities(ents); } } } }, }, "CHASING": { "enter": function () { // Show weapons rather than carried resources. this.SetGathererAnimationOverride(true); this.SelectAnimation("move"); var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsFleeing()) { // Run after a fleeing target var speed = this.GetRunSpeed(); this.SetMoveSpeed(speed); } this.StartTimer(1000, 1000); }, "HealthChanged": function() { var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsFleeing()) return; var speed = this.GetRunSpeed(); this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed in case it was changed this.SetMoveSpeed(this.GetWalkSpeed()); // Show carried resources when walking. this.SetGathererAnimationOverride(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { this.SetNextState("ATTACKING"); }, }, }, "GATHER": { "APPROACHING": { "enter": function() { this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". // check that we can gather from the resource we're supposed to gather from. var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); var cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage); if ((!cmpMirage || !cmpMirage.ResourceSupply()) && (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))) { // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = this.order.data.target; var oldTemplate = this.order.data.template; // Try the next queued order if there is any if (this.FinishOrder()) return true; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( ent != oldTarget && ((type.generic == "treasure" && oldType.generic == "treasure") || (type.specific == oldType.specific && (type.specific != "meat" || oldTemplate == template))) ); }); if (nearby) { this.PerformGather(nearby, false, false); return true; } else { // It's probably better in this case, to avoid units getting stuck around a dropsite // in a "Target is far away, full, nearby are no good resources, return to dropsite" loop // to order it to GatherNear the resource position. var cmpPosition = Engine.QueryInterface(this.gatheringTarget, IID_Position); if (cmpPosition) { var pos = cmpPosition.GetPosition(); this.GatherNearPosition(pos.x, pos.z, oldType, oldTemplate); return true; } else { // we're kind of stuck here. Return resource. var nearby = this.FindNearestDropsite(oldType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return true; } } } return true; } return false; }, "MoveCompleted": function(msg) { if (msg.data.error) { // We failed to reach the target // remove us from the list of entities gathering from Resource. var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply && cmpOwnership) cmpSupply.RemoveGatherer(this.entity, cmpOwnership.GetOwner()); else if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = this.order.data.target; var oldTemplate = this.order.data.template; // Try the next queued order if there is any if (this.FinishOrder()) return; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( ent != oldTarget && ((type.generic == "treasure" && oldType.generic == "treasure") || (type.specific == oldType.specific && (type.specific != "meat" || oldTemplate == template))) ); }); if (nearby) { this.PerformGather(nearby, false, false); return; } // Couldn't find anything else. Just try this one again, // maybe we'll succeed next time this.PerformGather(oldTarget, false, false); return; } // We reached the target - start gathering from it now this.SetNextState("GATHERING"); }, "leave": function() { // don't use ownership because this is called after a conversion/resignation // and the ownership would be invalid then. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; }, }, // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { var resourceType = this.order.data.type; var resourceTemplate = this.order.data.template; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); // If there is a nearby resource start gathering if (nearby) { this.PerformGather(nearby, false, false); return; } // Couldn't find nearby resources, so give up if (this.FinishOrder()) return; // Nothing better to do: go back to dropsite var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // No dropsites, just give up }, }, "GATHERING": { "enter": function() { this.gatheringTarget = this.order.data.target; // deleted in "leave". // Check if the resource is full. if (this.gatheringTarget) { // Check that we can gather from the resource we're supposed to gather from. // Will only be added if we're not already in. var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)) { this.gatheringTarget = INVALID_ENTITY; this.StartTimer(0); return false; } } // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) this.order.data.force = false; this.order.data.autoharvest = true; // Calculate timing based on gather rates // This allows the gather rate to control how often we gather, instead of how much. var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget); if (!rate) { // Try to find another target if the current one stopped existing if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity)) { // Let the Timer logic handle this this.StartTimer(0); return false; } // No rate, give up on gathering this.FinishOrder(); return true; } // Scale timing interval based on rate, and start timer // The offset should be at least as long as the repeat time so we use the same value for both. var offset = 1000/rate; var repeat = offset; this.StartTimer(offset, repeat); // We want to start the gather animation as soon as possible, // but only if we're actually at the target and it's still alive // (else it'll look like we're chopping empty air). // (If it's not alive, the Timer handler will deal with sending us // off to a different target.) if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename, false, 1.0, typename); } return false; }, "leave": function() { this.StopTimer(); // don't use ownership because this is called after a conversion/resignation // and the ownership would be invalid then. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; // Show the carried resource, if we've gathered anything. this.SetGathererAnimationOverride(); }, "Timer": function(msg) { var resourceTemplate = this.order.data.template; var resourceType = this.order.data.type; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity)) { // Check we can still reach and gather from the target if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget)) { // Gather the resources: var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); // Try to gather treasure if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget)) return; // If we've already got some resources but they're the wrong type, // drop them first to ensure we're only ever carrying one type if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic)) cmpResourceGatherer.DropResources(); // Collect from the target var status = cmpResourceGatherer.PerformGather(this.gatheringTarget); // If we've collected as many resources as possible, // return to the nearest dropsite if (status.filled) { var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { // (Keep this Gather order on the stack so we'll // continue gathering after returning) this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // Oh no, couldn't find any drop sites. Give up on gathering. this.FinishOrder(); return; } // We can gather more from this target, do so in the next timer if (!status.exhausted) return; } else { // Try to follow the target if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { this.SetNextState("APPROACHING"); return; } // Can't reach the target, or it doesn't exist any more // We want to carry on gathering resources in the same area as // the old one. So try to get close to the old resource's // last known position var maxRange = 8; // get close but not too close if (this.order.data.lastPos && this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, 0, maxRange)) { this.SetNextState("APPROACHING"); return; } } } // We're already in range, can't get anywhere near it or the target is exhausted. var herdPos = this.order.data.initPos; // Give up on this order and try our next queued order if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // Try to find a new resource of the same specific type near our current position: // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); if (nearby) { this.PerformGather(nearby, false, false); return; } // If hunting, try to go to the initial herd position to see if we are more lucky if (herdPos) this.GatherNearPosition(herdPos.x, herdPos.z, resourceType, resourceTemplate); // Nothing else to gather - if we're carrying anything then we should // drop it off, and if not then we might as well head to the dropsite // anyway because that's a nice enough place to congregate and idle var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // No dropsites - just give up }, }, }, "HEAL": { "Attacked": function(msg) { // If we stand ground we will rather die than flee if (!this.GetStance().respondStandGround && !this.order.data.force) this.Flee(msg.data.attacker, false); }, "APPROACHING": { "enter": function () { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { this.SetNextState("HEALING"); }, }, "HEALING": { "enter": function() { var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); this.healTimers = cmpHeal.GetTimers(); // If the repeat time since the last heal hasn't elapsed, // delay the action to avoid healing too fast. var prepare = this.healTimers.prepare; if (this.lastHealed) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } this.SelectAnimation("heal", false, 1.0, "heal"); this.SetAnimationSync(prepare, this.healTimers.repeat); this.StartTimer(prepare, this.healTimers.repeat); // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false; this.FaceTowardsTarget(this.order.data.target); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { var target = this.order.data.target; // Check the target is still alive and healable if (this.TargetIsAlive(target) && this.CanHeal(target)) { // Check if we can still reach the target if (this.CheckTargetRange(target, IID_Heal)) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastHealed = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); cmpHeal.PerformHeal(target); if (this.resyncAnimation) { this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, IID_Heal)) { this.SetNextState("HEAL.CHASING"); return; } } } // Can't reach it, healed to max hp or doesn't exist any more - give up if (this.FinishOrder()) return; // Heal another one if (this.FindNewHealTargets()) return; // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, }, "CHASING": { "enter": function () { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function () { this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function () { this.SetNextState("HEALING"); }, }, }, // Returning to dropsite "RETURNRESOURCE": { "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { // Switch back to idle animation to guarantee we won't // get stuck with the carry animation after stopping moving this.SelectAnimation("idle"); // Check the dropsite is in range and we can return our resource there // (we didn't get stopped before reaching it) if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true)) { var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); cmpResourceGatherer.CommitResources(dropsiteTypes); // Stop showing the carried resource animation. this.SetGathererAnimationOverride(); // Our next order should always be a Gather, // so just switch back to that order this.FinishOrder(); return; } } // The dropsite was destroyed, or we couldn't reach it, or ownership changed // Look for a new one. var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var genericType = cmpResourceGatherer.GetMainCarryingType(); var nearby = this.FindNearestDropsite(genericType); if (nearby) { this.FinishOrder(); this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // Oh no, couldn't find any drop sites. Give up on returning. this.FinishOrder(); }, }, }, "TRADE": { "Attacked": function(msg) { // Ignore attack // TODO: Inform player }, "APPROACHINGFIRSTMARKET": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { if (this.waypoints && this.waypoints.length) { if (!this.MoveToMarket(this.order.data.firstMarket)) this.StopTrading(); } else this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "APPROACHINGSECONDMARKET"); }, }, "APPROACHINGSECONDMARKET": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { if (this.waypoints && this.waypoints.length) { if (!this.MoveToMarket(this.order.data.secondMarket)) this.StopTrading(); } else this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "APPROACHINGFIRSTMARKET"); }, }, }, "REPAIR": { "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("REPAIRING"); }, }, "REPAIRING": { "enter": function() { // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) if (this.order.data.force) this.order.data.autoharvest = true; this.order.data.force = false; this.repairTarget = this.order.data.target; // temporary, deleted in "leave". // Check we can still reach and repair the target if (!this.CanRepair(this.repairTarget)) { // Can't reach it, no longer owned by ally, or it doesn't exist any more this.FinishOrder(); return true; } if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) { if (this.MoveToTargetRange(this.repairTarget, IID_Builder)) this.SetNextState("APPROACHING"); else this.FinishOrder(); return true; } // Check if the target is still repairable var cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints()) { // The building was already finished/fully repaired before we arrived; // let the ConstructionFinished handler handle this. this.OnGlobalConstructionFinished({"entity": this.repairTarget, "newentity": this.repairTarget}); return true; } var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation); if (cmpFoundation) cmpFoundation.AddBuilder(this.entity); this.SelectAnimation("build", false, 1.0, "build"); this.StartTimer(1000, 1000); return false; }, "leave": function() { var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation); if (cmpFoundation) cmpFoundation.RemoveBuilder(this.entity); delete this.repairTarget; this.StopTimer(); }, "Timer": function(msg) { // Check we can still reach and repair the target if (!this.CanRepair(this.repairTarget)) { // No longer owned by ally, or it doesn't exist any more this.FinishOrder(); return; } var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); cmpBuilder.PerformBuilding(this.repairTarget); // if the building is completed, the leave() function will be called // by the ConstructionFinished message // in that case, the repairTarget is deleted, and we can just return if (!this.repairTarget) return; if (this.MoveToTargetRange(this.repairTarget, IID_Builder)) this.SetNextState("APPROACHING"); else if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) this.FinishOrder(); //can't approach and isn't in reach }, }, "ConstructionFinished": function(msg) { if (msg.data.entity != this.order.data.target) return; // ignore other buildings // Save the current order's data in case we need it later var oldData = this.order.data; // Save the current state so we can continue walking if necessary // FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation. // Idle animation while moving towards finished construction looks weird (ghosty). var oldState = this.GetCurrentState(); // We finished building it. // Switch to the next order (if any) if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // If autocontinue explicitly disabled (e.g. by AI) then // do nothing automatically if (!oldData.autocontinue) return; // If this building was e.g. a farm of ours, the entities that recieved // the build command should start gathering from it if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity)) { this.PerformGather(msg.data.newentity, true, false); return; } // If this building was e.g. a farmstead of ours, entities that received // the build command should look for nearby resources to gather if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false)) { var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); var types = cmpResourceDropsite.GetTypes(); // TODO: Slightly undefined behavior here, we don't know what type of resource will be collected, // may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that! var nearby = this.FindNearbyResource(function (ent, type, template) { return (types.indexOf(type.generic) != -1); }); if (nearby) { this.PerformGather(nearby, true, false); return; } } // Look for a nearby foundation to help with var nearbyFoundation = this.FindNearbyFoundation(); if (nearbyFoundation) { this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true); return; } // Unit was approaching and there's nothing to do now, so switch to walking if (oldState === "INDIVIDUAL.REPAIR.APPROACHING") { // We're already walking to the given point, so add this as a order. this.WalkToTarget(msg.data.newentity, true); } }, }, "GARRISON": { "enter": function() { // If the garrisonholder should pickup, warn it so it can take needed action var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder); if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity)) { this.pickup = this.order.data.target; // temporary, deleted in "leave" Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity }); } }, "leave": function() { // If a pickup has been requested and not yet canceled, cancel it if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } }, "APPROACHING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function() { if(this.IsUnderAlert()) { // check that we can garrison in the building we're supposed to garrison in var cmpGarrisonHolder = Engine.QueryInterface(this.alertGarrisoningTarget, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) { // Try to find another nearby building var nearby = this.FindNearbyGarrisonHolder(); if (nearby) { this.alertGarrisoningTarget = nearby; this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); } else this.FinishOrder(); } else this.SetNextState("GARRISONED"); } else this.SetNextState("GARRISONED"); }, }, "GARRISONED": { "enter": function() { // Target is not handled the same way with Alert and direct garrisoning if(this.order.data.target) var target = this.order.data.target; else { if(!this.alertGarrisoningTarget) { // We've been unable to find a target nearby, so give up this.FinishOrder(); return true; } var target = this.alertGarrisoningTarget; } var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); // Check that we can garrison here if (this.CanGarrison(target)) { // Check that we're in range of the garrison target if (this.CheckGarrisonRange(target)) { // Check that garrisoning succeeds if (cmpGarrisonHolder.Garrison(this.entity)) { this.isGarrisoned = true; if (this.formationController) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { // disable rearrange for this removal, // but enable it again for the next // move command var rearrange = cmpFormation.rearrange; cmpFormation.SetRearrange(false); cmpFormation.RemoveMembers([this.entity]); cmpFormation.SetRearrange(rearrange); } } // Check if we are garrisoned in a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { cmpResourceGatherer.CommitResources(dropsiteTypes); this.SetGathererAnimationOverride(); } } // If a pickup has been requested, remove it if (this.pickup) { var cmpHolderPosition = Engine.QueryInterface(target, IID_Position); var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderPosition) cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition(); Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } if (this.IsTurret()) this.SetNextState("IDLE"); return false; } } else { // Unable to reach the target, try again (or follow if it is a moving target) // except if the does not exits anymore or its orders have changed if (this.pickup) { var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity)) { this.FinishOrder(); return true; } } if (this.MoveToTarget(target)) { this.SetNextState("APPROACHING"); return false; } } } // Garrisoning failed for some reason, so finish the order this.FinishOrder(); return true; }, "Order.Ungarrison": function() { if (this.FinishOrder()) return; }, "leave": function() { this.isGarrisoned = false; } }, }, "AUTOGARRISON": { "enter": function() { this.isGarrisoned = true; return false; }, "Order.Ungarrison": function() { if (this.FinishOrder()) return; }, "leave": function() { this.isGarrisoned = false; } }, "CHEERING": { "enter": function() { // Unit is invulnerable while cheering var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(true); this.SelectAnimation("promotion"); this.StartTimer(2800, 2800); return false; }, "leave": function() { this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); }, "Timer": function(msg) { this.FinishOrder(); }, }, "PACKING": { "enter": function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Pack(); }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { }, "Attacked": function(msg) { // Ignore attacks while packing }, }, "UNPACKING": { "enter": function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Unpack(); }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { }, "Attacked": function(msg) { // Ignore attacks while unpacking }, }, "PICKUP": { "APPROACHING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("LOADING"); }, "PickupCanceled": function() { this.StopMoving(); this.FinishOrder(); }, }, "LOADING": { "enter": function() { this.SelectAnimation("idle"); var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) { this.FinishOrder(); return true; } return false; }, "PickupCanceled": function() { this.FinishOrder(); }, }, }, }, "ANIMAL": { "Attacked": function(msg) { if (this.template.NaturalBehaviour == "skittish" || this.template.NaturalBehaviour == "passive") { this.Flee(msg.data.attacker, false); } else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive") { if (this.CanAttack(msg.data.attacker)) this.Attack(msg.data.attacker, false); } else if (this.template.NaturalBehaviour == "domestic") { // Never flee, stop what we were doing this.SetNextState("IDLE"); } }, "Order.LeaveFoundation": function(msg) { // Run away from the foundation this.FinishOrder(); this.PushOrderFront("Flee", { "target": msg.data.target, "force": false }); }, "IDLE": { // (We need an IDLE state so that FinishOrder works) "enter": function() { // Start feeding immediately this.SetNextState("FEEDING"); return true; }, }, "ROAMING": { "enter": function() { // Walk in a random direction this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); this.SetFacePointAfterMove(false); }, "leave": function() { this.StopTimer(); this.SetFacePointAfterMove(true); }, "LosRangeUpdate": function(msg) { if (this.template.NaturalBehaviour == "skittish") { if (msg.data.added.length > 0) { this.Flee(msg.data.added[0], false); return; } } // Start attacking one of the newly-seen enemy (if any) else if (this.IsDangerousAnimal()) { this.AttackVisibleEntity(msg.data.added); } // TODO: if two units enter our range together, we'll attack the // first and then the second won't trigger another LosRangeUpdate // so we won't notice it. Probably we should do something with // ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to // find any units that are already in range. }, "Timer": function(msg) { this.SetNextState("FEEDING"); }, "MoveCompleted": function() { this.MoveRandomly(+this.template.RoamDistance); }, }, "FEEDING": { "enter": function() { // Stop and eat for a while this.SelectAnimation("feeding"); this.StopMoving(); this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax)); }, "leave": function() { this.StopTimer(); }, "LosRangeUpdate": function(msg) { if (this.template.NaturalBehaviour == "skittish") { if (msg.data.added.length > 0) { this.Flee(msg.data.added[0], false); return; } } // Start attacking one of the newly-seen enemy (if any) else if (this.template.NaturalBehaviour == "violent") { this.AttackVisibleEntity(msg.data.added); } }, "MoveCompleted": function() { }, "Timer": function(msg) { this.SetNextState("ROAMING"); }, }, "FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals "COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals "WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals // only used for domestic animals }, }; UnitAI.prototype.Init = function() { this.orderQueue = []; // current order is at the front of the list this.order = undefined; // always == this.orderQueue[0] this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to this.isGarrisoned = false; this.isIdle = false; this.lastFormationTemplate = ""; this.finishedOrder = false; // used to find if all formation members finished the order this.heldPosition = undefined; // Queue of remembered works this.workOrders = []; this.isGuardOf = undefined; // "Town Bell" behaviour this.alertRaiser = undefined; this.alertGarrisoningTarget = undefined; // For preventing increased action rate due to Stop orders or target death. this.lastAttacked = undefined; this.lastHealed = undefined; this.SetStance(this.template.DefaultStance); }; UnitAI.prototype.IsTurret = function() { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY; }; UnitAI.prototype.ReactsToAlert = function(level) { return this.template.AlertReactiveLevel <= level; }; UnitAI.prototype.IsUnderAlert = function() { return this.alertRaiser != undefined; }; UnitAI.prototype.ResetAlert = function() { this.alertGarrisoningTarget = undefined; this.alertRaiser = undefined; }; UnitAI.prototype.GetAlertRaiser = function() { return this.alertRaiser; }; UnitAI.prototype.IsFormationController = function() { return (this.template.FormationController == "true"); }; UnitAI.prototype.IsFormationMember = function() { return (this.formationController != INVALID_ENTITY); }; UnitAI.prototype.HasFinishedOrder = function() { return this.finishedOrder; }; UnitAI.prototype.ResetFinishOrder = function() { this.finishedOrder = false; }; UnitAI.prototype.IsAnimal = function() { return (this.template.NaturalBehaviour ? true : false); }; UnitAI.prototype.IsDangerousAnimal = function() { return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" || this.template.NaturalBehaviour == "aggressive")); }; UnitAI.prototype.IsDomestic = function() { var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (!cmpIdentity) return false; return cmpIdentity.HasClass("Domestic"); }; UnitAI.prototype.IsHealer = function() { return Engine.QueryInterface(this.entity, IID_Heal); }; UnitAI.prototype.IsIdle = function() { return this.isIdle; }; UnitAI.prototype.IsGarrisoned = function() { return this.isGarrisoned || this.IsTurret(); }; UnitAI.prototype.IsFleeing = function() { var state = this.GetCurrentState().split(".").pop(); return (state == "FLEEING"); }; UnitAI.prototype.IsWalking = function() { var state = this.GetCurrentState().split(".").pop(); return (state == "WALKING"); }; /** * return true if in WalkAndFight looking for new targets */ UnitAI.prototype.IsWalkingAndFighting = function() { if (this.IsFormationMember()) { var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); return (cmpUnitAI && cmpUnitAI.IsWalkingAndFighting()); } return (this.orderQueue.length > 0 && this.orderQueue[0].type == "WalkAndFight"); }; UnitAI.prototype.OnCreate = function() { if (this.IsAnimal()) this.UnitFsm.Init(this, "ANIMAL.FEEDING"); else if (this.IsFormationController()) this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); else this.UnitFsm.Init(this, "INDIVIDUAL.IDLE"); }; UnitAI.prototype.OnDiplomacyChanged = function(msg) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) this.SetupRangeQuery(); }; UnitAI.prototype.OnOwnershipChanged = function(msg) { this.SetupRangeQueries(); // If the unit isn't being created or dying, reset stance and clear orders (if not garrisoned). if (msg.to != -1 && msg.from != -1) { // Switch to a virgin state to let states execute their leave handlers. var index = this.GetCurrentState().indexOf("."); if (index != -1) this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index)); this.SetStance(this.template.DefaultStance); if(!this.isGarrisoned) this.Stop(false); } }; UnitAI.prototype.OnDestroy = function() { // Switch to an empty state to let states execute their leave handlers. this.UnitFsm.SwitchToNextState(this, ""); // Clean up range queries var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) rangeMan.DestroyActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) rangeMan.DestroyActiveQuery(this.losHealRangeQuery); }; UnitAI.prototype.OnVisionRangeChanged = function(msg) { // Update range queries if (this.entity == msg.entity) this.SetupRangeQueries(); }; UnitAI.prototype.HasPickupOrder = function(entity) { for each (var order in this.orderQueue) if (order.type == "PickupUnit" && order.data.target == entity) return true; return false; }; UnitAI.prototype.OnPickupRequested = function(msg) { // First check if we already have such a request if (this.HasPickupOrder(msg.entity)) return; // Otherwise, insert the PickUp order after the last forced order this.PushOrderAfterForced("PickupUnit", { "target": msg.entity }); }; UnitAI.prototype.OnPickupCanceled = function(msg) { var cmpUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI); for (var i = 0; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].type == "PickupUnit" && this.orderQueue[i].data.target == msg.entity) { if (i == 0) this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg}); else this.orderQueue.splice(i, 1); break; } } }; // Wrapper function that sets up the normal and healer range queries. UnitAI.prototype.SetupRangeQueries = function() { this.SetupRangeQuery(); if (this.IsHealer()) this.SetupHealRangeQuery(); } // Set up a range query for all enemy and gaia units within LOS range // which can be attacked. // This should be called whenever our ownership changes. UnitAI.prototype.SetupRangeQuery = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.losRangeQuery) { rangeMan.DestroyActiveQuery(this.losRangeQuery); this.losRangeQuery = undefined; } var players = []; if (owner != -1) { // If unit not just killed, get enemy players via diplomacy var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); var numPlayers = playerMan.GetNumPlayers(); for (var i = 0; i < numPlayers; ++i) { // Exclude allies, and self // TODO: How to handle neutral players - Special query to attack military only? if (cmpPlayer.IsEnemy(i)) players.push(i); } } var range = this.GetQueryRange(IID_Attack); this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, rangeMan.GetEntityFlagMask("normal")); rangeMan.EnableActiveQuery(this.losRangeQuery); }; // Set up a range query for all own or ally units within LOS range // which can be healed. // This should be called whenever our ownership changes. UnitAI.prototype.SetupHealRangeQuery = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (this.losHealRangeQuery) rangeMan.DestroyActiveQuery(this.losHealRangeQuery); var players = [owner]; if (owner != -1) { // If unit not just killed, get ally players via diplomacy var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); var numPlayers = playerMan.GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) { // Exclude gaia and enemies if (cmpPlayer.IsAlly(i)) players.push(i); } } var range = this.GetQueryRange(IID_Heal); this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, rangeMan.GetEntityFlagMask("injured")); rangeMan.EnableActiveQuery(this.losHealRangeQuery); }; //// FSM linkage functions //// UnitAI.prototype.SetNextState = function(state) { this.UnitFsm.SetNextState(this, state); }; // This will make sure that the state is always entered even if this means leaving it and reentering it // This is so that a state can be reinitialized with new order data without having to switch to an intermediate state UnitAI.prototype.SetNextStateAlwaysEntering = function(state) { this.UnitFsm.SetNextStateAlwaysEntering(this, state); }; UnitAI.prototype.DeferMessage = function(msg) { this.UnitFsm.DeferMessage(this, msg); }; UnitAI.prototype.GetCurrentState = function() { return this.UnitFsm.GetCurrentState(this); }; UnitAI.prototype.FsmStateNameChanged = function(state) { Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state }); }; /** * Call when the current order has been completed (or failed). * Removes the current order from the queue, and processes the * next one (if any). Returns false and defaults to IDLE * if there are no remaining orders. */ UnitAI.prototype.FinishOrder = function() { if (!this.orderQueue.length) { var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(this.entity); error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack); } this.orderQueue.shift(); this.order = this.orderQueue[0]; if (this.orderQueue.length) { var ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // If the order was rejected then immediately take it off // and process the remaining queue if (ret && ret.discardOrder) { return this.FinishOrder(); } // Otherwise we've successfully processed a new order return true; } else { this.SetNextState("IDLE"); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // Check if there are queued formation orders if (this.IsFormationMember()) { var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { // Inform the formation controller that we finished this task this.finishedOrder = true; // We don't want to carry out the default order // if there are still queued formation orders left if (cmpUnitAI.GetOrders().length > 1) return true; } } return false; } }; /** * Add an order onto the back of the queue, * and execute it if we didn't already have an order. */ UnitAI.prototype.PushOrder = function(type, data) { var order = { "type": type, "data": data }; this.orderQueue.push(order); // If we didn't already have an order, then process this new one if (this.orderQueue.length == 1) { this.order = order; var ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // If the order was rejected then immediately take it off // and process the remaining queue if (ret && ret.discardOrder) this.FinishOrder(); } }; /** * Add an order onto the front of the queue, * and execute it immediately. */ UnitAI.prototype.PushOrderFront = function(type, data) { var order = { "type": type, "data": data }; // If current order is cheering then add new order after it // same thing if current order if packing/unpacking if (this.order && this.order.type == "Cheering") { var cheeringOrder = this.orderQueue.shift(); this.orderQueue.unshift(cheeringOrder, order); } else if (this.order && this.IsPacking()) { var packingOrder = this.orderQueue.shift(); this.orderQueue.unshift(packingOrder, order); } else { this.orderQueue.unshift(order); this.order = order; var ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // If the order was rejected then immediately take it off again; // assume the previous active order is still valid (the short-lived // new order hasn't changed state or anything) so we can carry on // as if nothing had happened if (ret && ret.discardOrder) { this.orderQueue.shift(); this.order = this.orderQueue[0]; } } }; /** * Insert an order after the last forced order onto the queue * and after the other orders of the same type */ UnitAI.prototype.PushOrderAfterForced = function(type, data) { if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type)) { this.PushOrderFront(type, data); } else { for (var i = 1; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].data && this.orderQueue[i].data.force) continue; if (this.orderQueue[i].type == type) continue; this.orderQueue.splice(i, 0, {"type": type, "data": data}); return; } this.PushOrder(type, data); } }; UnitAI.prototype.ReplaceOrder = function(type, data) { // Remember the previous work orders to be able to go back to them later if required if (data && data.force) { if (this.IsFormationController()) this.CallMemberFunction("UpdateWorkOrders", [type]); else this.UpdateWorkOrders(type); } // Special cases of orders that shouldn't be replaced: // 1. Cheering - we're invulnerable, add order after we finish // 2. Packing/unpacking - we're immobile, add order after we finish (unless it's cancel) // TODO: maybe a better way of doing this would be to use priority levels if (this.order && this.order.type == "Cheering") { var order = { "type": type, "data": data }; var cheeringOrder = this.orderQueue.shift(); this.orderQueue = [cheeringOrder, order]; } else if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack") { var order = { "type": type, "data": data }; var packingOrder = this.orderQueue.shift(); this.orderQueue = [packingOrder, order]; } else { this.orderQueue = []; this.PushOrder(type, data); } }; UnitAI.prototype.GetOrders = function() { return this.orderQueue.slice(); }; UnitAI.prototype.AddOrders = function(orders) { for each (var order in orders) { this.PushOrder(order.type, order.data); } }; UnitAI.prototype.GetOrderData = function() { var orders = []; for (var i in this.orderQueue) { if (this.orderQueue[i].data) orders.push(deepcopy(this.orderQueue[i].data)); } return orders; }; UnitAI.prototype.UpdateWorkOrders = function(type) { // Under alert, remembered work orders won't be forgotten if (this.IsUnderAlert()) return; var isWorkType = function(type){ return (type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource"); }; // If we are being re-affected to a work order, forget the previous ones if (isWorkType(type)) { this.workOrders = []; return; } // Then if we already have work orders, keep them if (this.workOrders.length) return; // First if the unit is in a formation, get its workOrders from it if (this.IsFormationMember()) { var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i) { if (isWorkType(cmpUnitAI.orderQueue[i].type)) { this.workOrders = cmpUnitAI.orderQueue.slice(i); return; } } } } // If nothing found, take the unit orders for (var i = 0; i < this.orderQueue.length; ++i) { if (isWorkType(this.orderQueue[i].type)) { this.workOrders = this.orderQueue.slice(i); return; } } }; UnitAI.prototype.BackToWork = function() { if (this.workOrders.length == 0) return false; // Clear the order queue considering special orders not to avoid if (this.order && this.order.type == "Cheering") { var cheeringOrder = this.orderQueue.shift(); this.orderQueue = [cheeringOrder]; } else this.orderQueue = []; this.AddOrders(this.workOrders); // And if the unit is in a formation, remove it from the formation if (this.IsFormationMember()) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers([this.entity]); } this.workOrders = []; return true; }; UnitAI.prototype.HasWorkOrders = function() { return this.workOrders.length > 0; }; UnitAI.prototype.GetWorkOrders = function() { return this.workOrders; }; UnitAI.prototype.SetWorkOrders = function(orders) { this.workOrders = orders; }; UnitAI.prototype.TimerHandler = function(data, lateness) { // Reset the timer if (data.timerRepeat === undefined) this.timer = undefined; this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness}); }; /** * Set up the UnitAI timer to run after 'offset' msecs, and then * every 'repeat' msecs until StopTimer is called. A "Timer" message * will be sent each time the timer runs. */ UnitAI.prototype.StartTimer = function(offset, repeat) { if (this.timer) error("Called StartTimer when there's already an active timer"); var data = { "timerRepeat": repeat }; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (repeat === undefined) this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data); else this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data); }; /** * Stop the current UnitAI timer. */ UnitAI.prototype.StopTimer = function() { if (!this.timer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; //// Message handlers ///// UnitAI.prototype.OnMotionChanged = function(msg) { if (msg.starting && !msg.error) this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); else if (!msg.starting || msg.error) this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) { // TODO: This is a bit inefficient since every unit listens to every // construction message - ideally we could scope it to only the one we're building this.UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg}); }; UnitAI.prototype.OnGlobalEntityRenamed = function(msg) { for each (var order in this.orderQueue) { if (order.data && order.data.target && order.data.target == msg.entity) order.data.target = msg.newentity; if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity) order.data.formationTarget = msg.newentity; } if (this.isGuardOf && this.isGuardOf == msg.entity) this.isGuardOf = msg.newentity; }; UnitAI.prototype.OnAttacked = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; UnitAI.prototype.OnGuardedAttacked = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data}); }; UnitAI.prototype.OnHealthChanged = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to}); }; UnitAI.prototype.OnRangeUpdate = function(msg) { if (msg.tag == this.losRangeQuery) this.UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg}); else if (msg.tag == this.losHealRangeQuery) this.UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg}); }; UnitAI.prototype.OnPackFinished = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed}); }; //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.GetWalkSpeed(); }; UnitAI.prototype.GetRunSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); var runSpeed = cmpUnitMotion.GetRunSpeed(); var walkSpeed = cmpUnitMotion.GetWalkSpeed(); if (runSpeed <= walkSpeed) return runSpeed; var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); var health = cmpHealth.GetHitpoints()/cmpHealth.GetMaxHitpoints(); return (health*runSpeed + (1-health)*walkSpeed); }; /** * Returns true if the target exists and has non-zero hitpoints. */ UnitAI.prototype.TargetIsAlive = function(ent) { var cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) return true; var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) return true; var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) return false; return (cmpHealth.GetHitpoints() != 0); }; /** * Returns true if the target exists and needs to be killed before * beginning to gather resources from it. */ UnitAI.prototype.MustKillGatherTarget = function(ent) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (!cmpResourceSupply) return false; if (!cmpResourceSupply.GetKillBeforeGather()) return false; return this.TargetIsAlive(ent); }; /** * Returns the entity ID of the nearest resource supply where the given * filter returns true, or undefined if none can be found. * TODO: extend this to exclude resources that already have lots of * gatherers. */ UnitAI.prototype.FindNearbyResource = function(filter) { var range = 64; // TODO: what's a sensible number? var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); // We accept resources owned by Gaia or any player var players = [0]; for (var i = 1; i < playerMan.GetNumPlayers(); ++i) players.push(i); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply); for each (var ent in nearby) { if (!this.CanGather(ent)) continue; var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); var type = cmpResourceSupply.GetType(); var amount = cmpResourceSupply.GetCurrentAmount(); var template = cmpTemplateManager.GetCurrentTemplateName(ent); // Remove "resource|" prefix from template names, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); if (amount > 0 && cmpResourceSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity) && filter(ent, type, template)) return ent; } return undefined; }; /** * Returns the entity ID of the nearest resource dropsite that accepts * the given type, or undefined if none can be found. */ UnitAI.prototype.FindNearestDropsite = function(genericType) { // Find dropsites owned by this unit's player var players = []; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) players = [cmpOwnership.GetOwner()]; // Ships are unable to reach land dropsites and shouldn't attempt to do so. var excludeLand = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship"); var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite); if (excludeLand) { nearby = nearby.filter( function(e) { return Engine.QueryInterface(e, IID_Identity).HasClass("Naval"); }); } for each (var ent in nearby) { var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (!cmpDropsite.AcceptsType(genericType)) continue; return ent; } return undefined; }; /** * Returns the entity ID of the nearest building that needs to be constructed, * or undefined if none can be found close enough. */ UnitAI.prototype.FindNearbyFoundation = function() { var range = 64; // TODO: what's a sensible number? // Find buildings owned by this unit's player var players = []; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) players = [cmpOwnership.GetOwner()]; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_Foundation); for each (var ent in nearby) { // Skip foundations that are already complete. (This matters since // we process the ConstructionFinished message before the foundation // we're working on has been deleted.) var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation.IsFinished()) continue; return ent; } return undefined; }; /** * Returns the entity ID of the nearest building in which the unit can garrison, * or undefined if none can be found close enough. */ UnitAI.prototype.FindNearbyGarrisonHolder = function() { var range = 128; // TODO: what's a sensible number? // Find buildings owned by this unit's player var players = []; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) players = [cmpOwnership.GetOwner()]; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_GarrisonHolder); for each (var ent in nearby) { var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); // We only want to garrison in buildings, not in moving units like ships,... var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI && cmpGarrisonHolder.AllowedToGarrison(this.entity) && !cmpGarrisonHolder.IsFull()) return ent; } return undefined; }; /** * Play a sound appropriate to the current entity. */ UnitAI.prototype.PlaySound = function(name) { // If we're a formation controller, use the sounds from our first member if (this.IsFormationController()) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); var member = cmpFormation.GetPrimaryMember(); if (member) PlaySound(name, member); } else { // Otherwise use our own sounds PlaySound(name, this.entity); } }; UnitAI.prototype.SetGathererAnimationOverride = function(disable) { var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return; var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; // Remove the animation override, so that weapons are shown again. if (disable) { cmpVisual.ResetMoveAnimation("walk"); return; } // Work out what we're carrying, in order to select an appropriate animation var type = cmpResourceGatherer.GetLastCarriedType(); if (type) { var typename = "carry_" + type.generic; // Special case for meat if (type.specific == "meat") typename = "carry_" + type.specific; cmpVisual.ReplaceMoveAnimation("walk", typename); } else cmpVisual.ResetMoveAnimation("walk"); } UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; // Special case: the "move" animation gets turned into a special // movement mode that deals with speeds and walk/run automatically if (name == "move") { // Speed to switch from walking to running animations var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2; cmpVisual.SelectMovementAnimation(runThreshold); return; } var soundgroup; if (sound) { var cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (cmpSound) soundgroup = cmpSound.GetSoundGroup(sound); } // Set default values if unspecified if (once === undefined) once = false; if (speed === undefined) speed = 1.0; if (soundgroup === undefined) soundgroup = ""; cmpVisual.SelectAnimation(name, once, speed, soundgroup); }; UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetAnimationSyncRepeat(repeattime); cmpVisual.SetAnimationSyncOffset(actiontime); }; UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.StopMoving(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; UnitAI.prototype.MoveToTarget = function(target) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, 0, 0); }; UnitAI.prototype.MoveToTargetRange = function(target, iid, type) { if (!this.CheckTargetVisible(target) || this.IsTurret()) return false; var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return false; var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; /** * Move unit so we hope the target is in the attack range * for melee attacks, this goes straight to the default range checks * for ranged attacks, the parabolic range is used */ UnitAI.prototype.MoveToTargetAttackRange = function(target, type) { // for formation members, the formation will take care of the range check if (this.IsFormationMember()) { var cmpFormationAttack = Engine.QueryInterface(this.formationController, IID_Attack); var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationAttack && cmpFormationAttack.CanAttackAsFormation() && cmpFormationUnitAI && cmpFormationUnitAI.GetCurrentState() == "FORMATIONCONTROLLER.ATTACKING") return false; } var cmpFormation = Engine.QueryInterface(target, IID_Formation) if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); if(type!= "Ranged") return this.MoveToTargetRange(target, IID_Attack, type); if (!this.CheckTargetVisible(target)) return false; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); var range = cmpAttack.GetRange(type); var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!thisCmpPosition.IsInWorld()) return false; var s = thisCmpPosition.GetPosition(); var targetCmpPosition = Engine.QueryInterface(target, IID_Position); if(!targetCmpPosition.IsInWorld()) return false; var t = targetCmpPosition.GetPosition(); // h is positive when I'm higher than the target var h = s.y-t.y+range.elevationBonus; // No negative roots please if(h>-range.max/2) var parabolicMaxRange = Math.sqrt(range.max*range.max+2*range.max*h); else // return false? Or hope you come close enough? var parabolicMaxRange = 0; //return false; // the parabole changes while walking, take something in the middle var guessedMaxRange = (range.max + parabolicMaxRange)/2; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange)) return true; // if that failed, try closer return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange)); }; UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, min, max); }; UnitAI.prototype.MoveToGarrisonRange = function(target) { if (!this.CheckTargetVisible(target)) return false; var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInPointRange(x, z, min, max); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return false; var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); }; /** * Check if the target is inside the attack range * For melee attacks, this goes straigt to the regular range calculation * For ranged attacks, the parabolic formula is used to accout for bigger ranges * when the target is lower, and smaller ranges when the target is higher */ UnitAI.prototype.CheckTargetAttackRange = function(target, type) { // for formation members, the formation will take care of the range check if (this.IsFormationMember()) { var cmpFormationAttack = Engine.QueryInterface(this.formationController, IID_Attack); var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationAttack && cmpFormationAttack.CanAttackAsFormation() && cmpFormationUnitAI && cmpFormationUnitAI.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING" && cmpFormationUnitAI.order.data.target == target) return true; } var cmpFormation = Engine.QueryInterface(target, IID_Formation) if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); if (type != "Ranged") return this.CheckTargetRange(target, IID_Attack, type); var targetCmpPosition = Engine.QueryInterface(target, IID_Position); if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) return false; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); var range = cmpAttack.GetRange(type); var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!thisCmpPosition.IsInWorld()) return false; var s = thisCmpPosition.GetPosition(); var t = targetCmpPosition.GetPosition(); var h = s.y-t.y+range.elevationBonus; var maxRangeSq = 2*range.max*(h + range.max/2); if (maxRangeSq < 0) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq)); }; UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, min, max); }; UnitAI.prototype.CheckGarrisonRange = function(target) { var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; var range = cmpGarrisonHolder.GetLoadingRange(); var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2) var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); }; /** * Returns true if the target entity is visible through the FoW/SoD. */ UnitAI.prototype.CheckTargetVisible = function(target) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return false; // Entities that are hidden and miraged are considered visible var cmpFogging = Engine.QueryInterface(target, IID_Fogging); if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner())) return true; if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden") return false; // Either visible directly, or visible in fog return true; }; UnitAI.prototype.FaceTowardsTarget = function(target) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); var targetpos = cmpTargetPosition.GetPosition(); var angle = Math.atan2(targetpos.x - pos.x, targetpos.z - pos.z); var rot = cmpPosition.GetRotation(); var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI; if (Math.abs(delta) > 0.2) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.z); } }; UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type); var cmpPosition = Engine.QueryInterface(target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; var halfvision = cmpVision.GetRange() / 2; var pos = cmpPosition.GetPosition(); var heldPosition = this.heldPosition; if (heldPosition === undefined) heldPosition = {"x": pos.x, "z": pos.z}; var dx = heldPosition.x - pos.x; var dz = heldPosition.z - pos.z; var dist = Math.sqrt(dx*dx + dz*dz); return dist < halfvision + range.max; }; UnitAI.prototype.CheckTargetIsInVisionRange = function(target) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; var range = cmpVision.GetRange(); var distance = DistanceBetweenEntities(this.entity,target); return distance < range; }; UnitAI.prototype.GetBestAttack = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttack(); }; UnitAI.prototype.GetBestAttackAgainst = function(target) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttackAgainst(target); }; UnitAI.prototype.GetAttackBonus = function(type, target) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return 1; return cmpAttack.GetAttackBonus(type, target); }; /** * Try to find one of the given entities which can be attacked, * and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackVisibleEntity = function(ents, forceResponse) { for each (var target in ents) { if (this.CanAttack(target, forceResponse)) { this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse }); return true; } } return false; }; /** * Try to find one of the given entities which can be attacked * and which is close to the hold position, and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackEntityInZone = function(ents, forceResponse) { for each (var target in ents) { var type = this.GetBestAttackAgainst(target); if (this.CanAttack(target, forceResponse) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, type) && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))) { this.PushOrderFront("Attack", { "target": target, "force": false, "forceResponse": forceResponse }); return true; } } return false; }; /** * Try to respond appropriately given our current stance, * given a list of entities that match our stance's target criteria. * Returns true if it responded. */ UnitAI.prototype.RespondToTargetedEntities = function(ents) { if (!ents.length) return false; if (this.GetStance().respondChase) return this.AttackVisibleEntity(ents, true); if (this.GetStance().respondStandGround) return this.AttackVisibleEntity(ents, true); if (this.GetStance().respondHoldGround) return this.AttackEntityInZone(ents, true); if (this.GetStance().respondFlee) { this.PushOrderFront("Flee", { "target": ents[0], "force": false }); return true; } return false; }; /** * Try to respond to healable entities. * Returns true if it responded. */ UnitAI.prototype.RespondToHealableEntities = function(ents) { if (!ents.length) return false; for each (var ent in ents) { if (this.CanHeal(ent)) { this.PushOrderFront("Heal", { "target": ent, "force": false }); return true; } } return false; }; /** * Returns true if we should stop following the target entity. */ UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type) { // Forced orders shouldn't be interrupted. if (force) return false; // If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker if (this.isGuardOf) { var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); var cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack) { for each (var targetType in cmpAttack.GetAttackTypes()) if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, targetType)) return false; } } // Stop if we're in hold-ground mode and it's too far from the holding point if (this.GetStance().respondHoldGround) { if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type)) return true; } // Stop if it's left our vision range, unless we're especially persistent if (!this.GetStance().respondChaseBeyondVision) { if (!this.CheckTargetIsInVisionRange(target)) return true; } // (Note that CCmpUnitMotion will detect if the target is lost in FoW, // and will continue moving to its last seen position and then stop) return false; }; /* * Returns whether we should chase the targeted entity, * given our current stance. */ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force) { if (this.IsTurret()) return false; // TODO: use special stances instead? var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) return false; if (this.GetStance().respondChase) return true; // If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker if (this.isGuardOf) { var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); var cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack) { for each (var type in cmpAttack.GetAttackTypes()) if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)) return true; } } if (force) return true; return false; }; //// External interface functions //// UnitAI.prototype.SetFormationController = function(ent) { this.formationController = ent; // Set obstruction group, so we can walk through members // of our own formation (or ourself if not in formation) var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) { if (ent == INVALID_ENTITY) cmpObstruction.SetControlGroup(this.entity); else cmpObstruction.SetControlGroup(ent); } // If we were removed from a formation, let the FSM switch back to INDIVIDUAL if (ent == INVALID_ENTITY) this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" }); }; UnitAI.prototype.GetFormationController = function() { return this.formationController; }; UnitAI.prototype.SetLastFormationTemplate = function(template) { this.lastFormationTemplate = template; }; UnitAI.prototype.GetLastFormationTemplate = function() { return this.lastFormationTemplate; }; UnitAI.prototype.MoveIntoFormation = function(cmd) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); // Add new order to move into formation at the current position this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true }); }; UnitAI.prototype.GetTargetPositions = function() { var targetPositions = []; for (var i = 0; i < this.orderQueue.length; ++i) { var order = this.orderQueue[i]; switch (order.type) { case "Walk": case "WalkAndFight": case "WalkToPointRange": case "MoveIntoFormation": case "GatherNearPosition": targetPositions.push(new Vector2D(order.data.x, order.data.z)); break; // and continue the loop case "WalkToTarget": case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will. case "Guard": case "Flee": case "LeaveFoundation": case "Attack": case "Heal": case "Gather": case "ReturnResource": case "Repair": case "Garrison": // Find the target unit's position var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return targetPositions; targetPositions.push(cmpTargetPosition.GetPosition2D()); return targetPositions; case "Stop": return []; default: error("GetTargetPositions: Unrecognised order type '"+order.type+"'"); return []; } } return targetPositions; }; /** * Returns the estimated distance that this unit will travel before either * finishing all of its orders, or reaching a non-walk target (attack, gather, etc). * Intended for Formation to switch to column layout on long walks. */ UnitAI.prototype.ComputeWalkingDistance = function() { var distance = 0; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return 0; // Keep track of the position at the start of each order var pos = cmpPosition.GetPosition2D(); var targetPositions = this.GetTargetPositions(); for (var i = 0; i < targetPositions.length; i++) { distance += pos.distanceTo(targetPositions[i]); // Remember this as the start position for the next order pos = targetPositions[i]; } // Return the total distance to the end of the order queue return distance; }; UnitAI.prototype.AddOrder = function(type, data, queued) { if (this.expectedRoute) this.expectedRoute = undefined; if (queued) this.PushOrder(type, data); else this.ReplaceOrder(type, data); }; /** * Adds guard/escort order to the queue, forced by the player. */ UnitAI.prototype.Guard = function(target, queued) { if (!this.CanGuard()) { this.WalkToTarget(target, queued); return; } // if we already had an old guard order, do nothing if the target is the same // and the order is running, otherwise remove the previous order if (this.isGuardOf) { if (this.isGuardOf == target && this.order && this.order.type == "Guard") return; else this.RemoveGuard(); } this.AddOrder("Guard", { "target": target, "force": false }, queued); }; UnitAI.prototype.AddGuard = function(target) { if (!this.CanGuard()) return false; var cmpGuard = Engine.QueryInterface(target, IID_Guard); if (!cmpGuard) return false; // Do not allow to guard a unit already guarding var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsGuardOf()) return false; this.isGuardOf = target; this.guardRange = cmpGuard.GetRange(this.entity); cmpGuard.AddGuard(this.entity); return true; }; UnitAI.prototype.RemoveGuard = function() { if (this.isGuardOf) { var cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard); if (cmpGuard) cmpGuard.RemoveGuard(this.entity); this.guardRange = undefined; this.isGuardOf = undefined; } if (!this.order) return; if (this.order.type == "Guard") this.UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"}); else for (var i = 1; i < this.orderQueue.length; ++i) if (this.orderQueue[i].type == "Guard") this.orderQueue.splice(i, 1); }; UnitAI.prototype.IsGuardOf = function() { return this.isGuardOf; }; UnitAI.prototype.SetGuardOf = function(entity) { // entity may be undefined this.isGuardOf = entity; }; UnitAI.prototype.CanGuard = function() { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Do not let a unit already guarded to guard. This would work in principle, // but would clutter the gui with too much buttons to take all cases into account var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard); if (cmpGuard && cmpGuard.GetEntities().length) return false; return (this.template.CanGuard == "true"); }; /** * Adds walk order to queue, forced by the player. */ UnitAI.prototype.Walk = function(x, z, queued) { if (this.expectedRoute && queued) this.expectedRoute.push({ "x": x, "z": z }); else this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued); }; /** * Adds walk to point range order to queue, forced by the player. */ UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued) { this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued); }; /** * Adds stop order to queue, forced by the player. */ UnitAI.prototype.Stop = function(queued) { this.AddOrder("Stop", undefined, queued); }; /** * Adds walk-to-target order to queue, this only occurs in response * to a player order, and so is forced. */ UnitAI.prototype.WalkToTarget = function(target, queued) { this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued); }; /** * Adds walk-and-fight order to queue, this only occurs in response * to a player order, and so is forced. * If targetClasses is given, only entities matching the targetClasses can be attacked. */ UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, queued) { this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "force": true }, queued); }; /** * Adds leave foundation order to queue, treated as forced. */ UnitAI.prototype.LeaveFoundation = function(target) { // If we're already being told to leave a foundation, then // ignore this new request so we don't end up being too indecisive // to ever actually move anywhere // Ignore also the request if we are packing if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target) || this.IsPacking())) return; this.PushOrderFront("LeaveFoundation", { "target": target, "force": true }); }; /** * Adds attack order to the queue, forced by the player. */ UnitAI.prototype.Attack = function(target, queued) { if (!this.CanAttack(target)) { // We don't want to let healers walk to the target unit so they can be easily killed. // Instead we just let them get into healing range. if (this.IsHealer()) this.MoveToTargetRange(target, IID_Heal); else this.WalkToTarget(target, queued); return; } this.AddOrder("Attack", { "target": target, "force": true }, queued); }; /** * Adds garrison order to the queue, forced by the player. */ UnitAI.prototype.Garrison = function(target, queued) { if (target == this.entity) return; if (!this.CanGarrison(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", { "target": target, "force": true }, queued); }; /** * Adds ungarrison order to the queue. */ UnitAI.prototype.Ungarrison = function() { if (this.IsGarrisoned()) this.AddOrder("Ungarrison", null, false); }; /** * Adds autogarrison order to the queue (only used by ProductionQueue for auto-garrisoning * and Promotion when promoting already garrisoned entities). */ UnitAI.prototype.Autogarrison = function(target) { this.AddOrder("Autogarrison", { "target": target }, false); }; /** * Adds gather order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Gather = function(target, queued) { this.PerformGather(target, queued, true); }; /** * Internal function to abstract the force parameter. */ UnitAI.prototype.PerformGather = function(target, queued, force) { if (!this.CanGather(target)) { this.WalkToTarget(target, queued); return; } // Save the resource type now, so if the resource gets destroyed // before we process the order then we still know what resource // type to look for more of var type; var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); var cmpMirage = Engine.QueryInterface(target, IID_Mirage); if (cmpResourceSupply) type = cmpResourceSupply.GetType(); else if (cmpMirage && cmpMirage.ResourceSupply()) type = cmpMirage.GetType(); else error("CanGather allowed gathering from invalid entity"); // Also save the target entity's template, so that if it's an animal, // we won't go from hunting slow safe animals to dangerous fast ones var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(target); // Remove "resource|" prefix from template name, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); // Remember the position of our target, if any, in case it disappears // later and we want to head to its last known position var lastPos = undefined; var cmpPosition = Engine.QueryInterface(target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) lastPos = cmpPosition.GetPosition(); this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued); }; /** * Adds gather-near-position order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued) { // Remove "resource|" prefix from template name, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer)) this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued); else this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued); }; /** * Adds heal order to the queue, forced by the player. */ UnitAI.prototype.Heal = function(target, queued) { if (!this.CanHeal(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Heal", { "target": target, "force": true }, queued); }; /** * Adds return resource order to the queue, forced by the player. */ UnitAI.prototype.ReturnResource = function(target, queued) { if (!this.CanReturnResource(target, true)) { this.WalkToTarget(target, queued); return; } this.AddOrder("ReturnResource", { "target": target, "force": true }, queued); }; /** * Adds trade order to the queue. Either walk to the first market, or * start a new route. Not forced, so it can be interrupted by attacks. * The possible route may be given directly as a SetupTradeRoute argument * if coming from a RallyPoint, or through this.expectedRoute if a user command. */ UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued) { if (!this.CanTrade(target)) { this.WalkToTarget(target, queued); return; } var marketsChanged = this.SetTargetMarket(target, source); if (marketsChanged) { var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets()) { var data = { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket(), "route": route, "force": false }; if (this.expectedRoute) { if (!route && this.expectedRoute.length) data.route = this.expectedRoute.slice(); this.expectedRoute = undefined; } if (this.IsFormationController()) { this.CallMemberFunction("AddOrder", ["Trade", data, queued]); var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.Disband(); } else this.AddOrder("Trade", data, queued); } else { if (this.IsFormationController()) this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]); else this.WalkToTarget(cmpTrader.GetFirstMarket(), queued); this.expectedRoute = []; } } }; UnitAI.prototype.SetTargetMarket = function(target, source) { var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader) return false; var marketsChanged = cmpTrader.SetTargetMarket(target, source); if (this.IsFormationController()) this.CallMemberFunction("SetTargetMarket", [target, source]); return marketsChanged; }; UnitAI.prototype.MoveToMarket = function(targetMarket) { if (this.waypoints && this.waypoints.length > 1) { var point = this.waypoints.pop(); var ok = this.MoveToPoint(point.x, point.z); if (!ok) ok = this.MoveToMarket(targetMarket); } else { this.waypoints = undefined; var ok = this.MoveToTarget(targetMarket); } return ok; }; UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket, nextMarket, nextFsmStateName) { if (!this.CanTrade(currentMarket)) { this.StopTrading(); return; } if (this.CheckTargetRange(currentMarket, IID_Trader)) { var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); cmpTrader.PerformTrade(currentMarket); if (!cmpTrader.GetGain().traderGain) { this.StopTrading(); return; } if (this.order.data.route && this.order.data.route.length) { this.waypoints = this.order.data.route.slice(); if (nextFsmStateName == "APPROACHINGSECONDMARKET") this.waypoints.reverse(); this.waypoints.unshift(null); // additionnal dummy point for the market } if (this.MoveToMarket(nextMarket)) // We've started walking to the next market this.SetNextState(nextFsmStateName); else this.StopTrading(); } else { if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again this.StopTrading(); } }; UnitAI.prototype.StopTrading = function() { this.StopMoving(); this.FinishOrder(); var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); cmpTrader.StopTrading(); }; /** * Adds repair/build order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Repair = function(target, autocontinue, queued) { if (!this.CanRepair(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued); }; /** * Adds flee order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.Flee = function(target, queued) { this.AddOrder("Flee", { "target": target, "force": false }, queued); }; /** * Adds cheer order to the queue. Forced so it won't be interrupted by attacks. */ UnitAI.prototype.Cheer = function() { this.AddOrder("Cheering", { "force": true }, false); }; UnitAI.prototype.Pack = function(queued) { // Check that we can pack if (this.CanPack()) this.AddOrder("Pack", { "force": true }, queued); }; UnitAI.prototype.Unpack = function(queued) { // Check that we can unpack if (this.CanUnpack()) this.AddOrder("Unpack", { "force": true }, queued); }; UnitAI.prototype.CancelPack = function(queued) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) this.AddOrder("CancelPack", { "force": true }, queued); }; UnitAI.prototype.CancelUnpack = function(queued) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) this.AddOrder("CancelUnpack", { "force": true }, queued); }; UnitAI.prototype.SetStance = function(stance) { if (g_Stances[stance]) this.stance = stance; else error("UnitAI: Setting to invalid stance '"+stance+"'"); }; UnitAI.prototype.SwitchToStance = function(stance) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.x, pos.z); this.SetStance(stance); // Stop moving if switching to stand ground // TODO: Also stop existing orders in a sensible way if (stance == "standground") this.StopMoving(); // Reset the range queries, since the range depends on stance. this.SetupRangeQueries(); }; UnitAI.prototype.SetTurretStance = function() { this.previousStance = undefined; if (this.GetStance().respondStandGround) return; for (let stance in g_Stances) { if (!g_Stances[stance].respondStandGround) continue; this.previousStance = this.GetStanceName(); this.SwitchToStance(stance); return; } }; UnitAI.prototype.ResetTurretStance = function() { if (!this.previousStance) return; this.SwitchToStance(this.previousStance); this.previousStance = undefined; }; /** * Resets losRangeQuery, and if there are some targets in range that we can * attack then we start attacking and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewTargets = function() { if (!this.losRangeQuery) return false; if (!this.GetStance().targetVisibleEnemies) return false; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.AttackEntitiesByPreference( rangeMan.ResetActiveQuery(this.losRangeQuery) )) return true; return false; }; UnitAI.prototype.FindWalkAndFightTargets = function() { if (this.IsFormationController()) { var cmpUnitAI; var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); for each (var ent in cmpFormation.members) { if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI))) continue; var targets = cmpUnitAI.GetTargetsFromUnit(); for (var targ of targets) { if (!cmpUnitAI.CanAttack(targ)) continue; if (this.order.data.targetClasses) { var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); var targetClasses = this.order.data.targetClasses; if (targetClasses.attack && cmpIdentity && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) continue; if (targetClasses.avoid && cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) continue; // Only used by the AIs to prevent some choices of targets if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) continue; } this.PushOrderFront("Attack", { "target": targ, "force": true }); return true; } } return false; } var targets = this.GetTargetsFromUnit(); for (var targ of targets) { if (!this.CanAttack(targ)) continue; if (this.order.data.targetClasses) { var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); var targetClasses = this.order.data.targetClasses; if (cmpIdentity && targetClasses.attack && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) continue; if (cmpIdentity && targetClasses.avoid && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) continue; // Only used by the AIs to prevent some choices of targets if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) continue; } this.PushOrderFront("Attack", { "target": targ, "force": true }); return true; } return false; }; UnitAI.prototype.GetTargetsFromUnit = function() { if (!this.losRangeQuery) return []; if (!this.GetStance().targetVisibleEnemies) return []; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return []; const attackfilter = function(e) { var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var entities = rangeMan.ResetActiveQuery(this.losRangeQuery); var targets = entities.filter(function (v) { return cmpAttack.CanAttack(v) && attackfilter(v); }) .sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }); return targets; }; /** * Resets losHealRangeQuery, and if there are some targets in range that we can heal * then we start healing and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewHealTargets = function() { if (!this.losHealRangeQuery) return false; var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery); for each (var ent in ents) { if (this.CanHeal(ent)) { this.PushOrderFront("Heal", { "target": ent, "force": false }); return true; } } // We haven't found any target to heal return false; }; UnitAI.prototype.GetQueryRange = function(iid) { var ret = { "min": 0, "max": 0 }; if (this.GetStance().respondStandGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return ret; var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack()); ret.min = range.min; ret.max = range.max; } else if (this.GetStance().respondChase) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var range = cmpVision.GetRange(); ret.max = range; } else if (this.GetStance().respondHoldGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return ret; var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack()); var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var halfvision = cmpVision.GetRange() / 2; ret.max = range.max + halfvision; } // We probably have stance 'passive' and we wouldn't have a range, // but as it is the default for healers we need to set it to something sane. else if (iid === IID_Heal) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var range = cmpVision.GetRange(); ret.max = range; } return ret; }; UnitAI.prototype.GetStance = function() { return g_Stances[this.stance]; }; UnitAI.prototype.GetPossibleStances = function() { if (this.IsTurret()) return []; return Object.keys(g_Stances); }; UnitAI.prototype.GetStanceName = function() { return this.stance; }; UnitAI.prototype.SetMoveSpeed = function(speed) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.SetSpeed(speed); }; UnitAI.prototype.SetHeldPosition = function(x, z) { this.heldPosition = {"x": x, "z": z}; }; UnitAI.prototype.SetHeldPositionOnEntity = function(entity) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.x, pos.z); }; UnitAI.prototype.GetHeldPosition = function() { return this.heldPosition; }; UnitAI.prototype.WalkToHeldPosition = function() { if (this.heldPosition) { this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false); return true; } return false; }; //// Helper functions //// UnitAI.prototype.CanAttack = function(target, forceResponse) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Attack commands var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; if (!cmpAttack.CanAttack(target)) return false; // Verify that the target is alive if (!this.TargetIsAlive(target)) return false; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership) + if (!cmpOwnership || cmpOwnership.GetOwner() < 0) return false; + var owner = cmpOwnership.GetOwner(); // Verify that the target is an attackable resource supply like a domestic animal // or that it isn't owned by an ally of this entity's player or is responding to // an attack. - var owner = cmpOwnership.GetOwner(); - if (!this.MustKillGatherTarget(target) - && !(IsOwnedByEnemyOfPlayer(owner, target) - || IsOwnedByNeutralOfPlayer(owner, target) - || (forceResponse && !IsOwnedByPlayer(owner, target)))) - return false; + if (this.MustKillGatherTarget(target)) + return true; - return true; + var cmpCapturable = Engine.QueryInterface(target, IID_Capturable); + if (cmpCapturable && cmpCapturable.CanCapture(owner) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1) + return true; + + if (IsOwnedByEnemyOfPlayer(owner, target) || IsOwnedByNeutralOfPlayer(owner, target)) + return true; + if (forceResponse && !IsOwnedByPlayer(owner, target)) + return true; + return false; }; UnitAI.prototype.CanGarrison = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; // Verify that the target is owned by this entity's player or a mutual ally of this player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target))) return false; // Don't let animals garrison for now // (If we want to support that, we'll need to change Order.Garrison so it // doesn't move the animal into an INVIDIDUAL.* state) if (this.IsAnimal()) return false; return true; }; UnitAI.prototype.CanGather = function(target) { if (this.IsTurret()) return false; // The target must be a valid resource supply, or the mirage of one. var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); var cmpMirage = Engine.QueryInterface(target, IID_Mirage); if (!cmpResourceSupply && !(cmpMirage && cmpMirage.ResourceSupply())) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Gather commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that we can gather from this target if (!cmpResourceGatherer.GetTargetGatherRate(target)) return false; // No need to verify ownership as we should be able to gather from // a target regardless of ownership. // No need to call "cmpResourceSupply.IsAvailable()" either because that // would cause units to walk to full entities instead of choosing another one // nearby to gather from, which is undesirable. return true; }; UnitAI.prototype.CanHeal = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Heal commands var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); if (!cmpHeal) return false; // Verify that the target is alive if (!this.TargetIsAlive(target)) return false; // Verify that the target is owned by the same player as the entity or of an ally var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))) return false; // Verify that the target is not unhealable (or at max health) var cmpHealth = Engine.QueryInterface(target, IID_Health); if (!cmpHealth || cmpHealth.IsUnhealable()) return false; // Verify that the target has no unhealable class var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return false; if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetUnhealableClasses())) return false; // Verify that the target is a healable class if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetHealableClasses())) return true; return false; }; UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource) { if (this.IsTurret()) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to ReturnResource commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that the target is a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return false; if (checkCarriedResource) { // Verify that we are carrying some resources, // and can return our current resource to this target var type = cmpResourceGatherer.GetMainCarryingType(); if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; } // Verify that the dropsite is owned by this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) return false; return true; }; UnitAI.prototype.CanTrade = function(target) { if (this.IsTurret()) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Trade commands var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader || !cmpTrader.CanTrade(target)) return false; return true; }; UnitAI.prototype.CanRepair = function(target) { if (this.IsTurret()) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Repair (Builder) commands var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); if (!cmpBuilder) return false; // Verify that the target is owned by an ally of this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)) return false; return true; }; UnitAI.prototype.CanPack = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked()); }; UnitAI.prototype.CanUnpack = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked()); }; UnitAI.prototype.IsPacking = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && cmpPack.IsPacking()); }; //// Animal specific functions //// UnitAI.prototype.MoveRandomly = function(distance) { // We want to walk in a random direction, but avoid getting stuck // in obstacles or narrow spaces. // So pick a circular range from approximately our current position, // and move outwards to the nearest point on that circle, which will // lead to us avoiding obstacles and moving towards free space. // TODO: we probably ought to have a 'home' point, and drift towards // that, so we don't spread out all across the whole map var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition) return; if (!cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); var jitter = 0.5; // Randomly adjust the range's center a bit, so we tend to prefer // moving in random directions (if there's nothing in the way) var tx = pos.x + (2*Math.random()-1)*jitter; var tz = pos.z + (2*Math.random()-1)*jitter; var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.MoveToPointRange(tx, tz, distance, distance); }; UnitAI.prototype.SetFacePointAfterMove = function(val) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpMotion) cmpMotion.SetFacePointAfterMove(val); }; UnitAI.prototype.AttackEntitiesByPreference = function(ents) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; const attackfilter = function(e) { var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; return this.RespondToTargetedEntities( ents.filter(function (v) { return cmpAttack.CanAttack(v) && attackfilter(v); }) .sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }) ); }; /** * Call obj.funcname(args) on UnitAI components of all formation members. */ UnitAI.prototype.CallMemberFunction = function(funcname, args) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; var members = cmpFormation.GetMembers(); for each (var ent in members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI[funcname].apply(cmpUnitAI, args); } }; /** * Call obj.functname(args) on UnitAI components of all formation members, * and return true if all calls return true. */ UnitAI.prototype.TestAllMemberFunction = function(funcname, args) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return false; var members = cmpFormation.GetMembers(); for each (var ent in members) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI[funcname].apply(cmpUnitAI, args)) return false; } return true; }; UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec); Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Capturable.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Capturable.js (revision 16550) @@ -0,0 +1,5 @@ +Engine.RegisterInterface("Capturable"); + +// Message in the form of {"capturePoints": [gaia, p1, p2, ...]} +Engine.RegisterMessageType("CapturePointsChanged"); + Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/decay_outpost.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/technologies/decay_outpost.json (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/decay_outpost.json (revision 16550) @@ -1,13 +1,13 @@ { "genericName": "Stone Foundations", "description": "Outposts survive twice as long in neutral territory.", "cost": {"food": 0, "wood": 0, "stone": 100, "metal": 0}, "requirements": {"tech": "phase_village"}, "requirementsTooltip": "Unlocked in Town Phase.", "icon": "blocks_three.png", "researchTime": 40, "tooltip": "Territory decay -50% for Outposts.", - "modifications": [{"value": "TerritoryDecay/HealthDecayRate", "multiply": 0.5}], + "modifications": [{"value": "TerritoryDecay/DecayRate", "multiply": 0.5}], "affects": ["Outpost"], "soundComplete": "interface/alarm/alarm_upgradearmory.xml" } Index: ps/trunk/binaries/data/mods/public/simulation/data/technologies/romans/decay_logistics.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/technologies/romans/decay_logistics.json (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/data/technologies/romans/decay_logistics.json (revision 16550) @@ -1,13 +1,13 @@ { "genericName": "Roman Logistics", "description": "The Romans were masters of the logistics of warfare.", "cost": {"food": 0, "wood": 100, "stone": 100, "metal": 0}, "requirements": {"all": [{"tech": "phase_city"}, {"civ": "rome"}]}, "requirementsTooltip": "Unlocked in City Phase.", "icon": "handcart_empty.png", "researchTime": 40, "tooltip": "Entrenched Camps and Siege Walls decay 50% slower.", - "modifications": [{"value": "TerritoryDecay/HealthDecayRate", "multiply": 0.5}], + "modifications": [{"value": "TerritoryDecay/DecayRate", "multiply": 0.5}], "affects": ["ArmyCamp", "SiegeWall"], "soundComplete": "interface/alarm/alarm_upgradearmory.xml" } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 16550) @@ -1,1619 +1,1629 @@ // Setting this to true will display some warnings when commands // are likely to fail, which may be useful for debugging AIs var g_DebugCommands = false; function ProcessCommand(player, cmd) { // Do some basic checks here that commanding player is valid var data = {}; data.cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!data.cmpPlayerMan || player < 0) return; data.playerEnt = data.cmpPlayerMan.GetPlayerByID(player); if (data.playerEnt == INVALID_ENTITY) return; data.cmpPlayer = Engine.QueryInterface(data.playerEnt, IID_Player); if (!data.cmpPlayer) return; data.controlAllUnits = data.cmpPlayer.CanControlAllUnits(); if (cmd.entities) data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits); // Note: checks of UnitAI targets are not robust enough here, as ownership // can change after the order is issued, they should be checked by UnitAI // when the specific behavior (e.g. attack, garrison) is performed. // (Also it's not ideal if a command silently fails, it's nicer if UnitAI // moves the entities closer to the target before giving up.) // Now handle various commands if (commands[cmd.type]) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("PlayerCommand", {"player": player, "cmd": cmd}); commands[cmd.type](player, cmd, data); } else error("Invalid command: unknown command type: "+uneval(cmd)); } var commands = { "debug-print": function(player, cmd, data) { print(cmd.message); }, "chat": function(player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": cmd.type, "players": [player], "message": cmd.message}); }, "aichat": function(player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); var notification = { "players": [player] }; for (var key in cmd) notification[key] = cmd[key]; cmpGuiInterface.PushNotification(notification); }, "cheat": function(player, cmd, data) { Cheat(cmd); }, "quit": function(player, cmd, data) { // Let the AI exit the game for testing purposes var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "quit", "players": [player]}); }, "diplomacy": function(player, cmd, data) { switch(cmd.to) { case "ally": data.cmpPlayer.SetAlly(cmd.player); break; case "neutral": data.cmpPlayer.SetNeutral(cmd.player); break; case "enemy": data.cmpPlayer.SetEnemy(cmd.player); break; default: warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "diplomacy", "players": [player], "player1": cmd.player, "status": cmd.to}); }, "tribute": function(player, cmd, data) { data.cmpPlayer.TributeResource(cmd.player, cmd.amounts); }, "control-all": function(player, cmd, data) { data.cmpPlayer.SetControlAllUnits(cmd.flag); }, "reveal-map": function(player, cmd, data) { // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, cmd.enable); }, "walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); }); }, "walk-to-range": function(player, cmd, data) { // Only used by the AI for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(cmpUnitAI) cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued); } }, "attack-walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.queued); }); }, "attack": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) { // This check is for debugging only! warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); } // See UnitAI.CanAttack for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Attack(cmd.target, cmd.queued); }); }, "heal": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) { // This check is for debugging only! warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); } // See UnitAI.CanHeal for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Heal(cmd.target, cmd.queued); }); }, "repair": function(player, cmd, data) { // This covers both repairing damaged buildings, and constructing unfinished foundations if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) { // This check is for debugging only! warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); } // See UnitAI.CanRepair for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); }); }, "gather": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) { // This check is for debugging only! warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); } // See UnitAI.CanGather for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Gather(cmd.target, cmd.queued); }); }, "gather-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued); }); }, "returnresource": function(player, cmd, data) { // Check dropsite is owned by player if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) { // This check is for debugging only! warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); } // See UnitAI.CanReturnResource for target checks GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.ReturnResource(cmd.target, cmd.queued); }); }, "back-to-work": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(!cmpUnitAI || !cmpUnitAI.BackToWork()) notifyBackToWorkFailure(player); } }, "remove-guard": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(cmpUnitAI) cmpUnitAI.RemoveGuard(); } }, "train": function(player, cmd, data) { // Check entity limits var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(cmd.template); var unitCategory = null; if (template.TrainingRestrictions) unitCategory = template.TrainingRestrictions.Category; // Verify that the building(s) can be controlled by the player if (data.entities.length <= 0) { if (g_DebugCommands) warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd)); return; } for each (var ent in data.entities) { if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd)); continue; } } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); if (!cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: training requires unresearched technology: " + uneval(cmd)); continue; } var queue = Engine.QueryInterface(ent, IID_ProductionQueue); // Check if the building can train the unit // TODO: the AI API does not take promotion technologies into account for the list // of trainable units (taken directly from the unit template). Here is a temporary fix. if (queue && data.cmpPlayer.IsAI()) { var list = queue.GetEntitiesList(); if (list.indexOf(cmd.template) === -1 && cmd.promoted) { for (var promoted of cmd.promoted) { if (list.indexOf(promoted) === -1) continue; cmd.template = promoted; break; } } } if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1) if ("metadata" in cmd) queue.AddBatch(cmd.template, "unit", +cmd.count, cmd.metadata); else queue.AddBatch(cmd.template, "unit", +cmd.count); } }, "research": function(player, cmd, data) { // Verify that the building can be controlled by the player if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd)); return; } var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); if (!cmpTechnologyManager.CanResearch(cmd.template)) { if (g_DebugCommands) warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd)); return; } var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.AddBatch(cmd.template, "technology"); }, "stop-production": function(player, cmd, data) { // Verify that the building can be controlled by the player if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd)); return; } var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.RemoveBatch(cmd.id); }, "construct": function(player, cmd, data) { TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "construct-wall": function(player, cmd, data) { TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "delete-entities": function(player, cmd, data) { - for each (var ent in data.entities) + for (let ent of data.entities) { + // don't allow to delete entities who are half-captured + var cmpCapturable = Engine.QueryInterface(ent, IID_Capturable); + if (cmpCapturable) + { + var capturePoints = cmpCapturable.GetCapturePoints(); + var maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); + if (capturePoints[player] < maxCapturePoints / 2) + return; + } + // either kill or delete the entity var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (!cmpResourceSupply || !cmpResourceSupply.GetKillBeforeGather()) cmpHealth.Kill(); } else Engine.DestroyEntity(ent); } }, "set-rallypoint": function(player, cmd, data) { for each (var ent in data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { if (!cmd.queued) cmpRallyPoint.Unset(); cmpRallyPoint.AddPosition(cmd.x, cmd.z); cmpRallyPoint.AddData(cmd.data); } } }, "unset-rallypoint": function(player, cmd, data) { for each (var ent in data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Reset(); } }, "defeat-player": function(player, cmd, data) { // Send "OnPlayerDefeated" message to player Engine.PostMessage(data.playerEnt, MT_PlayerDefeated, { "playerId": player } ); }, "garrison": function(player, cmd, data) { // Verify that the building can be controlled by the player or is mutualAlly if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Garrison(cmd.target, cmd.queued); }); }, "guard": function(player, cmd, data) { // Verify that the target can be controlled by the player or is mutualAlly if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Guard(cmd.target, cmd.queued); }); }, "stop": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.Stop(cmd.queued); }); }, "unload": function(player, cmd, data) { // Verify that the building can be controlled by the player or is mutualAlly if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); var notUngarrisoned = 0; // The owner can ungarrison every garrisoned unit if (IsOwnedByPlayer(player, cmd.garrisonHolder)) data.entities = cmd.entities; for each (var ent in data.entities) if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent)) notUngarrisoned++; if (notUngarrisoned != 0) notifyUnloadFailure(player, cmd.garrisonHolder) }, "unload-template": function(player, cmd, data) { var index = cmd.template.indexOf("&"); // Templates for garrisoned units are extended if (index == -1) return; var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for each (var garrisonHolder in entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Only the owner of the garrisonHolder may unload entities from any owners if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits && player != +cmd.template.slice(1,index)) continue; if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.all)) notifyUnloadFailure(player, garrisonHolder); } } }, "unload-all-own": function(player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); for each (var garrisonHolder in entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllOwn()) notifyUnloadFailure(player, garrisonHolder) } }, "unload-all": function(player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); for each (var garrisonHolder in entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) notifyUnloadFailure(player, garrisonHolder) } }, "increase-alert-level": function(player, cmd, data) { for each (var ent in data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (!cmpAlertRaiser || !cmpAlertRaiser.IncreaseAlertLevel()) notifyAlertFailure(player); } }, "alert-end": function(player, cmd, data) { for each (var ent in data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.EndOfAlert(); } }, "formation": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd.name).forEach(function(cmpUnitAI) { cmpUnitAI.MoveIntoFormation(cmd); }); }, "promote": function(player, cmd, data) { // No need to do checks here since this is a cheat anyway var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "chat", "players": [player], "message": "(Cheat - promoted units)"}); for each (var ent in cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } }, "stance": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && !cmpUnitAI.IsTurret()) cmpUnitAI.SwitchToStance(cmd.name); } }, "wall-to-gate": function(player, cmd, data) { for each (var ent in data.entities) { TryTransformWallToGate(ent, data.cmpPlayer, cmd.template); } }, "lock-gate": function(player, cmd, data) { for each (var ent in data.entities) { var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) { if (cmd.lock) cmpGate.LockGate(); else cmpGate.UnlockGate(); } } }, "setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(function(cmpUnitAI) { cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); }, "select-required-goods": function(player, cmd, data) { for each (var ent in data.entities) { var cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) cmpTrader.SetRequiredGoods(cmd.requiredGoods); } }, "set-trading-goods": function(player, cmd, data) { data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); }, "barter": function(player, cmd, data) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); cmpBarter.ExchangeResources(data.playerEnt, cmd.sell, cmd.buy, cmd.amount); }, "set-shading-color": function(player, cmd, data) { // Debug command to make an entity brightly colored for each (var ent in cmd.entities) { var cmpVisual = Engine.QueryInterface(ent, IID_Visual) if (cmpVisual) cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0) // alpha isn't used so just send 0 } }, "pack": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmd.pack) cmpUnitAI.Pack(cmd.queued); else cmpUnitAI.Unpack(cmd.queued); } } }, "cancel-pack": function(player, cmd, data) { for each (var ent in data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmd.pack) cmpUnitAI.CancelPack(cmd.queued); else cmpUnitAI.CancelUnpack(cmd.queued); } } }, "attack-request": function(player, cmd, data) { // Send a chat message to human players var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGuiInterface) { var notification = { "type": "aichat", "players": [player], "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."), "translateParameters": ["_player_"], "parameters": {"_player_": cmd.target} }; cmpGuiInterface.PushNotification(notification); } // And send an attackRequest event to the AIs let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("AttackRequest", cmd); }, "dialog-answer": function(player, cmd, data) { // Currently nothing. Triggers can read it anyway, and send this // message to any component you like. }, }; /** * Sends a GUI notification about unit(s) that failed to ungarrison. */ function notifyUnloadFailure(player, garrisonHolder) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Unable to ungarrison unit(s)" }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } /** * Sends a GUI notification about worker(s) that failed to go back to work. */ function notifyBackToWorkFailure(player) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "Some unit(s) can't go back to work" }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } /** * Sends a GUI notification about Alerts that failed to be raised */ function notifyAlertFailure(player) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var notification = {"players": [cmpPlayer.GetPlayerID()], "message": "You can't raise the alert to a higher level !" }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } /** * Get some information about the formations used by entities. * The entities must have a UnitAI component. */ function ExtractFormations(ents) { var entities = []; // subset of ents that have UnitAI var members = {}; // { formationentity: [ent, ent, ...], ... } for each (var ent in ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var fid = cmpUnitAI.GetFormationController(); if (fid != INVALID_ENTITY) { if (!members[fid]) members[fid] = []; members[fid].push(ent); } entities.push(ent); } var ids = [ id for (id in members) ]; return { "entities": entities, "members": members, "ids": ids }; } /** * Tries to find the best angle to put a dock at a given position * Taken from GuiInterface.js */ function GetDockAngle(template, x, z) { var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) return undefined; // Get footprint size var halfSize = 0; if (template.Footprint.Square) halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; else if (template.Footprint.Circle) halfSize = template.Footprint.Circle["@radius"]; /* Find direction of most open water, algorithm: * 1. Pick points in a circle around dock * 2. If point is in water, add to array * 3. Scan array looking for consecutive points * 4. Find longest sequence of consecutive points * 5. If sequence equals all points, no direction can be determined, * expand search outward and try (1) again * 6. Calculate angle using average of sequence */ const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { var angle = (i/numPoints)*2*Math.PI; var d = halfSize*(dist+1); var nx = x - d*Math.sin(angle); var nz = z + d*Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) waterPoints.push(i); } var consec = []; var length = waterPoints.length; if (!length) continue; for (var i = 0; i < length; ++i) { var count = 0; for (var j = 0; j < (length-1); ++j) { if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } var start = 0; var count = 0; for (var c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI); } return undefined; } /** * Attempts to construct a building using the specified parameters. * Returns true on success, false on failure. */ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) { // Message structure: // { // "type": "construct", // "entities": [...], // entities that will be ordered to construct the building (if applicable) // "template": "...", // template name of the entity being constructed // "x": ..., // "z": ..., // "angle": ..., // "metadata": "...", // AI metadata of the building // "actorSeed": ..., // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable) // "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. If specified, must be a valid control group ID (> 0). // "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. May be INVALID_ENTITY. // } /* * Construction process: * . Take resources away immediately. * . Create a foundation entity with 1hp, 0% build progress. * . Increase hp and build progress up to 100% when people work on it. * . If it's destroyed, an appropriate fraction of the resource cost is refunded. * . If it's completed, it gets replaced with the real building. */ // Check whether we can control these units var entities = FilterEntityList(cmd.entities, player, controlAllUnits); if (!entities.length) return false; var foundationTemplate = "foundation|" + cmd.template; // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity(foundationTemplate); if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) error("Error creating foundation entity for '" + cmd.template + "'"); return false; } // If it's a dock, get the right angle. var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateMgr.GetTemplate(cmd.template); if (template.BuildRestrictions.Category === "Dock") { var angle = GetDockAngle(template, cmd.x, cmd.z); if (angle !== undefined) cmd.angle = angle; } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(cmd.angle); // Set the obstruction control group if needed if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2) { var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); // primary control group must always be valid if (cmd.obstructionControlGroup) { if (cmd.obstructionControlGroup <= 0) warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0"); cmpObstruction.SetControlGroup(cmd.obstructionControlGroup); } if (cmd.obstructionControlGroup2) cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2); } // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (cmpBuildRestrictions) { var ret = cmpBuildRestrictions.CheckPlacement(); if (!ret.success) { if (g_DebugCommands) { warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd)); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); ret.players = [player]; cmpGuiInterface.PushNotification(ret); // Remove the foundation because the construction was aborted // move it out of world because it's not destroyed immediately. cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } } else error("cmpBuildRestrictions not defined"); // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (!cmpEntityLimits || !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) { if (g_DebugCommands) { warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); } // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) { warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd)); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "players": [player], "message": "Building's technology requirements are not met." }); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); } // We need the cost after tech modifications // To calculate this with an entity requires ownership, so use the template instead var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(foundationTemplate); var costs = {}; for (var r in template.Cost.Resources) { costs[r] = +template.Cost.Resources[r]; if (cmpTechnologyManager) costs[r] = cmpTechnologyManager.ApplyModificationsTemplate("Cost/Resources/"+r, costs[r], template); } if (!cmpPlayer.TrySubtractResources(costs)) { if (g_DebugCommands) warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); Engine.DestroyEntity(ent); cmpPosition.MoveOutOfWorld(); return false; } var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual && cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); cmpFoundation.InitialiseConstruction(player, cmd.template); // send Metadata info if any if (cmd.metadata) Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } ); // Tell the units to start building this new entity if (cmd.autorepair) { ProcessCommand(player, { "type": "repair", "entities": entities, "target": ent, "autocontinue": cmd.autocontinue, "queued": cmd.queued }); } return ent; } function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd) { // 'cmd' message structure: // { // "type": "construct-wall", // "entities": [...], // entities that will be ordered to construct the wall (if applicable) // "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...) // { // "template": "...", // one of the templates from the wallset // "x": ..., // "z": ..., // "angle": ..., // }, // ... // ], // "wallSet": { // "templates": { // "tower": // tower template name // "long": // long wall segment template name // ... // etc. // }, // "maxTowerOverlap": ..., // "minTowerOverlap": ..., // }, // "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall // "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable) // } if (cmd.pieces.length <= 0) return; if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side"); return; } if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side"); return; } // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing // towers in the case of snapping). The towers themselves all keep their default unique control groups. // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of // the first tower encountered towards the ending side of the wall (if any). // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes: // // FIRST PASS: // - Go from start to end and construct wall piece foundations as far as we can without running into a piece that // cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID // as the primary control group, thus allowing it to be built overlapping the previous piece. // - If we encounter a new tower along the way (which will gain its own control group), do the following: // o First build it using temporarily the same control group of the previous (non-tower) piece // o Set the previous piece's secondary control group to the tower's entity ID // o Restore the primary control group of the constructed tower back its original (unique) value. // The temporary control group is necessary to allow the newer tower with its unique control group ID to be able // to be placed while overlapping the previous piece. // // SECOND PASS: // - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this // time registering the right neighbouring tower in each non-tower piece. // first pass; L -> R var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that // the first wall piece can be built while overlapping it. if (cmd.startSnappedEntity) { var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction); if (!cmpSnappedStartObstruction) { error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup(); //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup); } var i = 0; for (; i < cmd.pieces.length; ++i) { var piece = cmd.pieces[i]; // All wall pieces after the first must be queued. if (i > 0 && !cmd.queued) cmd.queued = true; // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do // start position snapping (implying that the first entity we build must be a tower) if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY) { if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity)) { error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")"); break; } } var constructPieceCmd = { "type": "construct", "entities": cmd.entities, "template": piece.template, "x": piece.x, "z": piece.z, "angle": piece.angle, "autorepair": cmd.autorepair, "autocontinue": cmd.autocontinue, "queued": cmd.queued, // Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed // using the control group of the last tower (see comments above). "obstructionControlGroup": lastTowerControlGroup, }; // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's // control group directly at construction time (instead of setting it in the second pass) to allow it to be built // while overlapping the snapped entity. if (i == cmd.pieces.length - 1 && cmd.endSnappedEntity) { var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (cmpEndSnappedObstruction) constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup(); } var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd); if (pieceEntityId) { // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later piece.ent = pieceEntityId; // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex if (piece.template == cmd.wallSet.templates.tower) { var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction); var newTowerControlGroup = pieceEntityId; if (i > 0) { //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup); var cmpPreviousObstruction = Engine.QueryInterface(cmd.pieces[i-1].ent, IID_Obstruction); // TODO: ensure that cmpPreviousObstruction exists // TODO: ensure that the previous obstruction does not yet have a secondary control group set cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); } // TODO: ensure that cmpTowerObstruction exists cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group lastTowerIndex = i; lastTowerControlGroup = newTowerControlGroup; } } else { // failed to build wall piece, abort i = j + 1; // compensate for the -1 subtracted by lastBuiltPieceIndex below break; } } var lastBuiltPieceIndex = i - 1; var wallComplete = (lastBuiltPieceIndex == cmd.pieces.length - 1); // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower). // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any) // as their secondary control groups. lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // only start off with the ending side's snapped tower's control group if we were able to build the entire wall if (cmd.endSnappedEntity && wallComplete) { var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (!cmpSnappedEndObstruction) { error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup(); } for (var j = lastBuiltPieceIndex; j >= 0; --j) { var piece = cmd.pieces[j]; if (!piece.ent) { error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'"); continue; } var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction); if (!cmpPieceObstruction) { error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component"); continue; } if (piece.template == cmd.wallSet.templates.tower) { // encountered a tower entity, update the last tower control group lastTowerControlGroup = cmpPieceObstruction.GetControlGroup(); } else { // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'. // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'. var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2(); if (existingSecondaryControlGroup == INVALID_ENTITY) { if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY) { cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup); } } else if (existingSecondaryControlGroup != lastTowerControlGroup) { error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); break; } } } } /** * Remove the given list of entities from their current formations. */ function RemoveFromFormation(ents) { var formation = ExtractFormations(ents); for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } } /** * Returns a list of UnitAI components, each belonging either to a * selected unit or to a formation entity for groups of the selected units. */ function GetFormationUnitAIs(ents, player, formationTemplate) { // If an individual was selected, remove it from any formation // and command it individually if (ents.length == 1) { // Skip unit if it has no UnitAI var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) return []; RemoveFromFormation(ents); return [ cmpUnitAI ]; } // Separate out the units that don't support the chosen formation var formedEnts = []; var nonformedUnitAIs = []; for each (var ent in ents) { // Skip units with no UnitAI or no position var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) continue; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: We only check if the formation is usable by some units // if we move them to it. We should check if we can use formations // for the other cases. var nullFormation = (formationTemplate || cmpUnitAI.GetLastFormationTemplate()) == "formations/null"; if (!nullFormation && cmpIdentity && cmpIdentity.CanUseFormation(formationTemplate || "formations/line_closed")) formedEnts.push(ent); else { if (nullFormation) cmpUnitAI.SetLastFormationTemplate("formations/null"); nonformedUnitAIs.push(cmpUnitAI); } } if (formedEnts.length == 0) { // No units support the foundation - return all the others return nonformedUnitAIs; } // Find what formations the formationable selected entities are currently in var formation = ExtractFormations(formedEnts); var formationUnitAIs = []; if (formation.ids.length == 1) { // Selected units either belong to this formation or have no formation // Check that all its members are selected var fid = formation.ids[0]; var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length && cmpFormation.GetMemberCount() == formation.entities.length) { cmpFormation.DeleteTwinFormations(); // The whole formation was selected, so reuse its controller for this command formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) cmpFormation.LoadFormation(formationTemplate); } } if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller // Remove selected units from their current formation for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } // TODO replace the fixed 60 with something sensible, based on vision range f.e. var formationSeparation = 60; var clusters = ClusterEntities(formation.entities, formationSeparation); var formationEnts = []; for each (var cluster in clusters) { if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) { // get the most recently used formation, or default to line closed var lastFormationTemplate = undefined; for each (var ent in cluster) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { var template = cmpUnitAI.GetLastFormationTemplate(); if (lastFormationTemplate === undefined) { lastFormationTemplate = template; } else if (lastFormationTemplate != template) { lastFormationTemplate = undefined; break; } } } if (lastFormationTemplate && CanMoveEntsIntoFormation(cluster, lastFormationTemplate)) formationTemplate = lastFormationTemplate; else formationTemplate = "formations/line_closed"; } // Create the new controller var formationEnt = Engine.AddEntity(formationTemplate); var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); cmpFormation.SetFormationSeparation(formationSeparation); cmpFormation.SetMembers(cluster); for each (var ent in formationEnts) cmpFormation.RegisterTwinFormation(ent); formationEnts.push(formationEnt); var cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); cmpOwnership.SetOwner(player); } } return nonformedUnitAIs.concat(formationUnitAIs); } /** * Group a list of entities in clusters via single-links */ function ClusterEntities(ents, separationDistance) { var clusters = []; if (!ents.length) return clusters; var distSq = separationDistance * separationDistance; var positions = []; // triangular matrix with the (squared) distances between the different clusters // the other half is not initialised var matrix = []; for (var i = 0; i < ents.length; i++) { matrix[i] = []; clusters.push([ents[i]]); var cmpPosition = Engine.QueryInterface(ents[i], IID_Position); positions.push(cmpPosition.GetPosition2D()); for (var j = 0; j < i; j++) matrix[i][j] = positions[i].distanceToSquared(positions[j]); } while (clusters.length > 1) { // search two clusters that are closer than the required distance var smallDist = Infinity; var closeClusters = undefined; for (var i = matrix.length - 1; i >= 0 && !closeClusters; --i) for (var j = i - 1; j >= 0 && !closeClusters; --j) if (matrix[i][j] < distSq) closeClusters = [i,j]; // if no more close clusters found, just return all found clusters so far if (!closeClusters) return clusters; // make a new cluster with the entities from the two found clusters var newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); // calculate the minimum distance between the new cluster and all other remaining // clusters by taking the minimum of the two distances. var distances = []; for (var i = 0; i < clusters.length; i++) { if (i == closeClusters[1] || i == closeClusters[0]) continue; var dist1 = matrix[closeClusters[1]][i] || matrix[i][closeClusters[1]]; var dist2 = matrix[closeClusters[0]][i] || matrix[i][closeClusters[0]]; distances.push(Math.min(dist1, dist2)); } // remove the rows and columns in the matrix for the merged clusters, // and the clusters themselves from the cluster list clusters.splice(closeClusters[0],1); clusters.splice(closeClusters[1],1); matrix.splice(closeClusters[0],1); matrix.splice(closeClusters[1],1); for (var i = 0; i < matrix.length; i++) { if (matrix[i].length > closeClusters[0]) matrix[i].splice(closeClusters[0],1); if (matrix[i].length > closeClusters[1]) matrix[i].splice(closeClusters[1],1); } // add a new row of distances to the matrix and the new cluster clusters.push(newCluster); matrix.push(distances); } return clusters; } function GetFormationRequirements(formationTemplate) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(formationTemplate); if (!template.Formation) return false; return {"minCount": +template.Formation.RequiredMemberCount}; } function CanMoveEntsIntoFormation(ents, formationTemplate) { // TODO: should check the player's civ is allowed to use this formation // See simulation/components/Player.js GetFormations() for a list of all allowed formations var requirements = GetFormationRequirements(formationTemplate); if (!requirements) return false; var count = 0; var reqClasses = requirements.classesRequired || []; for each (var ent in ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate)) continue; count++; } return count >= requirements.minCount; } /** * Check if player can control this entity * returns: true if the entity is valid and owned by the player * or control all units is activated, else false */ function CanControlUnit(entity, player, controlAll) { return (IsOwnedByPlayer(player, entity) || controlAll); } /** * Check if player can control this entity * returns: true if the entity is valid and owned by the player * or the entity is owned by an mutualAlly * or control all units is activated, else false */ function CanControlUnitOrIsAlly(entity, player, controlAll) { return (IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll); } /** * Filter entities which the player can control */ function FilterEntityList(entities, player, controlAll) { return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} ); } /** * Filter entities which the player can control or are mutualAlly */ function FilterEntityListWithAllies(entities, player, controlAll) { return entities.filter(function(ent) { return CanControlUnitOrIsAlly(ent, player, controlAll);} ); } /** * Try to transform a wall to a gate */ function TryTransformWallToGate(ent, cmpPlayer, template) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) return; // Check if this is a valid long wall segment if (!cmpIdentity.HasClass("LongWall")) { if (g_DebugCommands) warn("Invalid command: invalid wall conversion to gate for player "+player+": "+uneval(cmd)); return; } var civ = cmpIdentity.GetCiv(); var gate = Engine.AddEntity(template); var cmpCost = Engine.QueryInterface(gate, IID_Cost); if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts())) { if (g_DebugCommands) warn("Invalid command: convert gate cost check failed for player "+player+": "+uneval(cmd)); Engine.DestroyEntity(gate); return; } ReplaceBuildingWith(ent, gate); } /** * Unconditionally replace a building with another one */ function ReplaceBuildingWith(ent, building) { // Move the building to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position); var pos = cmpPosition.GetPosition2D(); cmpBuildingPosition.JumpTo(pos.x, pos.y); var rot = cmpPosition.GetRotation(); cmpBuildingPosition.SetYRotation(rot.y); cmpBuildingPosition.SetXZRotation(rot.x, rot.z); // Copy ownership var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership); cmpBuildingOwnership.SetOwner(cmpOwnership.GetOwner()); // Copy control groups var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction); cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup()); cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2()); // Copy health level from the old entity to the new var cmpHealth = Engine.QueryInterface(ent, IID_Health); var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health); var healthFraction = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints())); var buildingHitpoints = Math.round(cmpBuildingHealth.GetMaxHitpoints() * healthFraction); cmpBuildingHealth.SetHitpoints(buildingHitpoints); PlaySound("constructed", building); Engine.PostMessage(ent, MT_ConstructionFinished, { "entity": ent, "newentity": building }); Engine.BroadcastMessage(MT_EntityRenamed, { entity: ent, newentity: building }); Engine.DestroyEntity(ent); } Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("GetDockAngle", GetDockAngle); Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Engine.RegisterGlobal("commands", commands); Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 16550) @@ -1,70 +1,70 @@ own neutral MercenaryCamp 100 300 100 0 100 12.0 1200 rubble/rubble_stone_5x5 ptol Mercenary Camp (Egyptian) Stratópedo Misthophóron Aigyptiakós MercenaryCamp The Greco-Macedonian Ptolemy Dynasty relied on large numbers of Greek and foreign mercenaries for the bulk of its military force, mainly because the loyalty of native Egyptian units was often suspect. Indeed, during one native uprising, Upper Egypt was lost to the Ptolemies for decades. Mercenaries were often battle-hardened and their loyalty can be bought, sometimes cheaply, sometimes not cheaply. This was of no matter, since Egypt under the Ptolemies was so prosperous as to be the richest of Alexander's successor states. Capture this structure to train mercenaries from Hellenistic Egypt. structures/military_settlement.png phase_town units/ptol_infantry_spearman_merc_b units/ptol_infantry_swordsman_merc_b units/ptol_cavalry_spearman_merc_b units/ptol_cavalry_javelinist_merc_b -unlock_champion_units upgrade_rank_advanced_infantry upgrade_rank_elite_infantry upgrade_rank_advanced_cavalry upgrade_rank_elite_cavalry interface/complete/building/complete_gymnasium.xml attack/destruction/building_collapse_large.xml - 1 + 1 structures/mercenaries/camp_egyptian.xml structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_mercenary_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_mercenary_camp.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_mercenary_camp.xml (revision 16550) @@ -1,72 +1,72 @@ own neutral MercenaryCamp 100 300 100 0 100 12.0 1200 rubble/rubble_stone_5x5 ptol Mercenary Camp Stratópedo Misthophóron MercenaryCamp The Greco-Macedonian Ptolemy Dynasty relied on large numbers of Greek and foreign mercenaries for the bulk of its military force, mainly because the loyalty of native Egyptian units was often suspect. Indeed, during one native uprising, Upper Egypt was lost to the Ptolemies for decades. Mercenaries were often battle-hardened and their loyalty can be bought, sometimes cheaply, sometimes not cheaply. This was of no matter, since Egypt under the Ptolemies was so prosperous as to be the richest of Alexander's successor states. Cheap barracks-like structure that is buildable in Neutral territory, but casts no territory influence. - Train Mercenaries. - Min. distance from other Military Settlements: 100 meters. structures/military_settlement.png phase_town units/ptol_infantry_spearman_merc_b units/ptol_infantry_swordsman_merc_b units/ptol_cavalry_spearman_merc_b units/ptol_cavalry_javelinist_merc_b -unlock_champion_units upgrade_rank_advanced_infantry upgrade_rank_elite_infantry upgrade_rank_advanced_cavalry upgrade_rank_elite_cavalry interface/complete/building/complete_gymnasium.xml attack/destruction/building_collapse_large.xml - 1 + 1 structures/ptolemies/settlement.xml structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_military_colony.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_military_colony.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_military_colony.xml (revision 16550) @@ -1,88 +1,88 @@ 0.0 15.0 0.0 72.0 10.0 75.0 1200 2000 1.5 1 1 own neutral Colony Colony 120 300 200 200 200 12.0 2000 rubble/rubble_stone_5x5 ptol Military Colony Klēroukhia Town Colony The Ptolemaic kings invited Greeks, Macedonians, Galatians (Gauls), Cretans, and Thracians alike to settle within Egypt in military colonies called cleruchies (klēroukhia). Under this arrangement, the settlers were given a plot of land, or a kleros, and in return were required to serve in the great king's army when called to duty. This created a upper-middle class of military settlers who owed their livelihoods and fortunes to the Ptolemaic kings and helped grow the available manpower for the imperial Ptolemaic army. A side effect of this system was that it drained the Greek homeland of military-aged men, a contributing factor to Greece's eventual conquest by Rome. This is the Ptolemaic expansion building, similar to Civic Centers for other factions. It is weaker and carries a smaller territory influence, but is cheaper and built faster. - Train settler-soldiers of various nationalities. - Min. distance from other Military Colonies: 120 meters. structures/military_settlement.png phase_town units/{civ}_infantry_spearman_merc_b units/{civ}_infantry_swordsman_merc_b units/{civ}_cavalry_spearman_merc_b units/{civ}_cavalry_javelinist_merc_b -phase_town_generic -phase_city_generic interface/complete/building/complete_gymnasium.xml attack/destruction/building_collapse_large.xml - 1 + 1 80 structures/ptolemies/settlement.xml structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 16550) @@ -1,104 +1,104 @@ 3.0 25.0 1.0 1.0 5.0 1.0 0.0 25.0 0.0 80.0 12.0 75.0 1200 2000 1.5 3 0.5 own neutral enemy Fortress 5 250 400 0 12.0 40 Support Infantry Cavalry Siege 1 6 2500 true rubble/rubble_rome_sb rome Entrenched Army Camp Castrum Vallum ArmyCamp structures/roman_camp.png Build anywhere on the map, even in enemy territory. Construct siege weapons and train citizen-soldiers. Heal garrisoned units slowly. Sometimes it was a temporary camp built facing the route by which the army is to march, other times a defensive or offensive (for sieges) structure. Within this gate the tents of the first centuries or cohorts are pitched, and the dragons (ensigns of cohorts) and other ensigns planted. The Decumane gate is directly opposite to the Praetorian in the rear of the camp, and through this the soldiers are conducted to the place appointed for punishment or execution. interface/complete/building/complete_broch.xml attack/destruction/building_collapse_large.xml - 10 + 10 0.7 units/{civ}_infantry_swordsman_b units/{civ}_infantry_spearman_a units/{civ}_infantry_javelinist_b units/{civ}_cavalry_spearman_b units/{civ}_mechanical_siege_ballista_packed units/{civ}_mechanical_siege_scorpio_packed units/{civ}_mechanical_siege_oxybeles_packed units/{civ}_mechanical_siege_lithobolos_packed units/{civ}_mechanical_siege_ram units/{civ}_mechanical_siege_tower 60 structures/romans/camp.xml structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_gate.xml (revision 16550) @@ -1,57 +1,57 @@ 15.0 35.0 5.0 4.0 7.0 3.0 own neutral enemy 80 0 7.0 2000 rome Siege Wall Gate Porta Circummunitionis SiegeWall Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia. structures/wooden_gate.png - 1 + 1 structures/romans/siege_wall_gate.xml structures/fndn_wall.xml 36.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml (revision 16550) @@ -1,68 +1,68 @@ 15.0 35.0 5.0 4.0 7.0 3.0 own neutral enemy 40 0 7.0 1500 05.70 45.70 -45.70 rome structures/rome_wallset_siege Siege Wall Murus Circummunitionis SiegeWall structures/palisade_wall.png A wooden and turf palisade buildable in enemy and neutral territories. Quick building, but expensive wooden and earthen walls used to surround and siege an enemy town or fortified position. The most famous examples are the Roman sieges of the Iberian stronghold of Numantia and the Gallic stronghold of Alesia. - 1 + 1 structures/romans/siege_wall_medium.xml structures/fndn_wall_short.xml 24.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_tower.xml (revision 16550) @@ -1,48 +1,48 @@ 15.0 35.0 5.0 4.0 7.0 3.0 own neutral enemy 100 0 14.0 2000 rome Siege Wall Tower Turris Circummunitionis (Insert history here) - 1 + 1 structures/romans/siege_wall_tower.xml 6.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_military_colony.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_military_colony.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_military_colony.xml (revision 16550) @@ -1,87 +1,87 @@ 0.0 15.0 0.0 72.0 10.0 75.0 1200 2000 1.5 1 1 own neutral Colony Colony 120 300 200 200 200 12.0 2000 rubble/rubble_stone_5x5 sele Military Colony Klēroukhia Town Colony The Seleucid kings invited Greeks, Macedonians, Galatians (Gauls), Cretans, and Thracians alike to settle in within the vast territories of the empire. They settled in military colonies called cleruchies (klēroukhia). Under this arrangement, the settlers were given a plot of land, or a kleros, and in return were required to serve in the great king's army when called to duty. This created a upper-middle class of military settlers who owed their livelihoods and fortunes to the Syrian kings and helped grow the available manpower for the imperial Seleucid army. A side effect of this system was that it drained the Greek homeland of military-aged men, a contributing factor to Greece's eventual conquest by Rome. This is the Seleucid expansion building, similar to Civic Centers for other factions. It is weaker and carries a smaller territory influence, but is cheaper and built faster. - Train settler-soldiers of various nationalities. - Min. distance from other Military Colonies: 120 meters. structures/military_settlement.png phase_town units/{civ}_infantry_swordsman_merc_b units/{civ}_infantry_archer_merc_b units/{civ}_cavalry_spearman_merc_b -phase_town_generic -phase_city_generic interface/complete/building/complete_gymnasium.xml attack/destruction/building_collapse_large.xml - 1 + 1 80 structures/ptolemies/settlement.xml structures/fndn_5x5.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 16550) @@ -1,123 +1,126 @@ 15 25 3 5 15 3 0.0 15.0 0.0 72.0 10.0 75.0 1200 2000 1.5 3 1 own neutral CivilCentre CivilCentre 200 + + 3000 + 20 500 0 500 500 500 8.0 20 0.1 Unit Support Infantry Cavalry 1 1 3000 rubble/rubble_stone_6x6 Civic Center Build to acquire large tracts of territory. Train citizens. Garrison: 20. Defensive CivCentre CivilCentre structures/civic_centre.png 200 0 50 50 50 0.8 units/{civ}_support_female_citizen phase_town_generic phase_city_generic food wood stone metal interface/complete/building/complete_civ_center.xml attack/weapon/arrowfly.xml attack/destruction/building_collapse_large.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml interface/alarm/alarm_alert_2.xml true 140 10000 2 140 90 structures/fndn_6x6.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defense_wall.xml (revision 16550) @@ -1,57 +1,58 @@ land-shore Wall + 10 30 8.0 4000 rubble/rubble_wall -ConquestCritical Defensive StoneWall Stone Wall Wall off your town for a stout defense. structures/wall.png phase_town 100 0 5 15 0 interface/complete/building/complete_wall.xml attack/destruction/building_collapse_large.xml false 20 65536 20 structures/fndn_wall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_farmstead.xml (revision 16550) @@ -1,68 +1,71 @@ Farmstead + + 300 + 45 100 8.0 900 rubble/rubble_3x3 Farmstead Dropsite for the food resource. Research food gathering improvements. -ConquestCritical DropsiteFood Village Farmstead structures/farmstead.png 100 0 10 0 0 0.7 gather_wicker_baskets gather_farming_plows gather_farming_training gather_farming_fertilizer food interface/complete/building/complete_farmstead.xml attack/destruction/building_collapse_large.xml false 20 30000 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 16550) @@ -1,78 +1,79 @@ 25 40 5 Field + 50 100 2.0 250 rubble/rubble_field Field Field Harvest vegetables for food. Max gatherers: 5. Farming originated around 9500 BC in India and the Middle East. The history of agriculture is a central element of human history, as agricultural progress has been a crucial factor in worldwide socio-economic change. Wealth-building and militaristic specializations rarely seen in hunter-gatherer cultures are commonplace in agricultural and agro-industrial societies - when farmers became capable of producing food beyond the needs of their own families, others in the tribe/nation/empire were freed to devote themselves to projects other than food acquisition. structures/field.png 100 100 0 0 0 false false false Infinity food.grain 5 14 interface/complete/building/complete_field.xml attack/destruction/building_collapse_large.xml 8.0 0 structures/plot_field_3D_8x8.xml structures/plot_field_found.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion.xml (revision 16550) @@ -1,16 +1,23 @@ + + + 3 + 4 + 1000 + + Champion Unit Organic Human Champion Soldier phase_city 150 10 10 0 20 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml (revision 16550) @@ -1,128 +1,133 @@ 2 4 15 + + 3 + 4 + 1000 + 50.0 0.0 0.0 4.0 1.0 structures/{civ}_house structures/{civ}_storehouse structures/{civ}_farmstead structures/{civ}_field structures/{civ}_corral structures/{civ}_outpost other/wallset_palisade other/palisades_rocks_fort structures/{civ}_dock structures/{civ}_barracks structures/{civ}_blacksmith structures/{civ}_temple structures/{civ}_market structures/{civ}_defense_tower structures/{civ}_wallset_stone structures/{civ}_civil_centre structures/{civ}_fortress structures/{civ}_wonder 10 50 0 0 0 2.5 80 Infantry CitizenSoldier Worker Human Organic Infantry Citizen Soldier Basic 100 5 0 0 0 100000000 2.0 1.0 0.5 0.25 1 0.75 5 0.5 2 0.5 circle/128x128.png circle/128x128_mask.png voice/hellenes/civ/civ_male_ack.xml voice/hellenes/civ/civ_male_attack.xml voice/hellenes/civ/civ_male_ack.xml voice/hellenes/civ/civ_male_ack.xml actor/human/movement/walk.xml actor/human/movement/run.xml attack/weapon/sword.xml actor/human/death/death.xml resource/construction/con_wood.xml resource/foraging/forage_leaves.xml resource/farming/farm.xml resource/lumbering/lumbering.xml resource/mining/pickaxe.xml resource/mining/mining.xml resource/mining/mining.xml interface/alarm/alarm_create_infantry.xml 1000 9 18.75 infantry 80 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml (revision 16550) @@ -1,73 +1,73 @@ 10 10 12 - + 17.5 0 25.0 8.0 1000 Structure 1.5 Cavalry 2 Gates 1.5 3 60 500 500 9.0 2000 Elephant actor/fauna/animal/elephant_order.xml actor/fauna/animal/elephant_attack.xml voice/hellenes/civ/civ_male_ack.xml actor/fauna/animal/elephant_attack.xml actor/mounted/movement/walk.xml actor/mounted/movement/walk.xml actor/fauna/animal/elephant_death.xml actor/fauna/animal/elephant_trained.xml 8.5 14.0 1000.0 10.0 80 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_elephant_melee.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_elephant_melee.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_elephant_melee.xml (revision 16550) @@ -1,22 +1,22 @@ - + 20 0 150.0 8.0 1500 40.0 0.0 300.0 8.0 0.0 Melee Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege.xml (revision 16549) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege.xml (revision 16550) @@ -1,30 +1,35 @@ 5 5 5 + + 100 + 0 + 1.5 + 5 Siege Siege phase_city false pitch-roll attack/siege/ram_move.xml attack/siege/ram_attack.xml attack/siege/ram_move.xml