Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 24962) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 24963) @@ -1,342 +1,342 @@ /** * This class parses and stores parsed template data. */ class TemplateParser { constructor(TemplateLoader) { this.TemplateLoader = TemplateLoader; /** * Parsed Data Stores */ this.auras = {}; this.entities = {}; this.techs = {}; this.phases = {}; this.modifiers = {}; this.phaseList = []; } getAura(auraName) { if (auraName in this.auras) return this.auras[auraName]; if (!AuraTemplateExists(auraName)) return null; let template = this.TemplateLoader.loadAuraTemplate(auraName); let parsed = GetAuraDataHelper(template); if (template.civ) parsed.civ = template.civ; this.auras[auraName] = parsed; return this.auras[auraName]; } /** * Load and parse a structure, unit, resource, etc from its entity template file. * * @param {string} templateName * @param {string} civCode * @return {(object|null)} Sanitized object about the requested template or null if entity template doesn't exist. */ getEntity(templateName, civCode) { if (!(civCode in this.entities)) this.entities[civCode] = {}; else if (templateName in this.entities[civCode]) return this.entities[civCode][templateName]; if (!Engine.TemplateExists(templateName)) return null; let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); let parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, this.modifiers[civCode] || {}); parsed.name.internal = templateName; parsed.history = template.Identity.History; parsed.production = this.TemplateLoader.deriveProductionQueue(template, civCode); if (template.Builder) parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode); // Set the minimum phase that this entity is available. // For gaia objects, this is meaningless. if (!parsed.requiredTechnology) parsed.phase = this.phaseList[0]; else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology)) parsed.phase = this.getActualPhase(parsed.requiredTechnology); else parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode); if (template.Identity.Rank) parsed.promotion = { "current_rank": template.Identity.Rank, "entity": template.Promotion && template.Promotion.Entity }; if (template.ResourceSupply) parsed.supply = { "type": template.ResourceSupply.Type.split("."), - "amount": template.ResourceSupply.Amount, + "amount": template.ResourceSupply.Max, }; if (parsed.upgrades) parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode); if (parsed.wallSet) { parsed.wallset = {}; if (!parsed.upgrades) parsed.upgrades = []; // Note: An assumption is made here that wall segments all have the same resistance and auras let struct = this.getEntity(parsed.wallSet.templates.long, civCode); parsed.resistance = struct.resistance; parsed.auras = struct.auras; // For technology cost multiplier, we need to use the tower struct = this.getEntity(parsed.wallSet.templates.tower, civCode); parsed.techCostMultiplier = struct.techCostMultiplier; let health; for (let wSegm in parsed.wallSet.templates) { if (wSegm == "fort" || wSegm == "curves") continue; let wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode); parsed.wallset[wSegm] = wPart; for (let research of wPart.production.techs) parsed.production.techs.push(research); if (wPart.upgrades) Array.prototype.push.apply(parsed.upgrades, wPart.upgrades); if (["gate", "tower"].indexOf(wSegm) != -1) continue; if (!health) { health = { "min": wPart.health, "max": wPart.health }; continue; } health.min = Math.min(health.min, wPart.health); health.max = Math.max(health.max, wPart.health); } if (parsed.wallSet.templates.curves) for (let curve of parsed.wallSet.templates.curves) { let wPart = this.getEntity(curve, civCode); health.min = Math.min(health.min, wPart.health); health.max = Math.max(health.max, wPart.health); } if (health.min == health.max) parsed.health = health.min; else parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), { "health_min": health.min, "health_max": health.max }); } this.entities[civCode][templateName] = parsed; return parsed; } /** * Load and parse technology from json template. * * @param {string} technologyName * @param {string} civCode * @return {Object} Sanitized data about the requested technology. */ getTechnology(technologyName, civCode) { if (!TechnologyTemplateExists(technologyName)) return null; if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases) return this.phases[technologyName]; if (!(civCode in this.techs)) this.techs[civCode] = {}; else if (technologyName in this.techs[civCode]) return this.techs[civCode][technologyName]; let template = this.TemplateLoader.loadTechnologyTemplate(technologyName); let tech = GetTechnologyDataHelper(template, civCode, g_ResourceData); tech.name.internal = technologyName; if (template.pair !== undefined) { tech.pair = template.pair; tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs); } if (this.TemplateLoader.isPhaseTech(technologyName)) { tech.actualPhase = technologyName; if (tech.replaces !== undefined) tech.actualPhase = tech.replaces[0]; this.phases[technologyName] = tech; } else this.techs[civCode][technologyName] = tech; return tech; } /** * @param {string} phaseCode * @param {string} civCode * @return {Object} Sanitized object containing phase data */ getPhase(phaseCode, civCode) { return this.getTechnology(phaseCode, civCode); } /** * Provided with an array containing basic information about possible * upgrades, such as that generated by globalscript's GetTemplateDataHelper, * this function loads the actual template data of the upgrades, overwrites * certain values within, then passes an array containing the template data * back to caller. */ getActualUpgradeData(upgradesInfo, civCode) { let newUpgrades = []; for (let upgrade of upgradesInfo) { upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode); let data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData, this.modifiers[civCode] || {}); data.name.internal = upgrade.entity; data.cost = upgrade.cost; data.icon = upgrade.icon || data.icon; data.tooltip = upgrade.tooltip || data.tooltip; data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology; if (!data.requiredTechnology) data.phase = this.phaseList[0]; else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology)) data.phase = this.getActualPhase(data.requiredTechnology); else data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode); newUpgrades.push(data); } return newUpgrades; } /** * Determines and returns the phase in which a given technology can be * first researched. Works recursively through the given tech's * pre-requisite and superseded techs if necessary. * * @param {string} techName - The Technology's name * @param {string} civCode * @return The name of the phase the technology belongs to, or false if * the current civ can't research this tech */ getPhaseOfTechnology(techName, civCode) { let phaseIdx = -1; if (basename(techName).startsWith("phase")) { if (!this.phases[techName].reqs) return false; phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName)); if (phaseIdx > 0) return this.phaseList[phaseIdx - 1]; } let techReqs = this.getTechnology(techName, civCode).reqs; if (!techReqs) return false; for (let option of techReqs) if (option.techs) for (let tech of option.techs) { if (basename(tech).startsWith("phase")) return tech; if (basename(tech).startsWith("pair")) continue; phaseIdx = Math.max(phaseIdx, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode))); } return this.phaseList[phaseIdx] || false; } /** * Returns the actual phase a certain phase tech represents or stands in for. * * For example, passing `phase_city_athen` would result in `phase_city`. * * @param {string} phaseName * @return {string} */ getActualPhase(phaseName) { if (this.phases[phaseName]) return this.phases[phaseName].actualPhase; warn("Unrecognized phase (" + phaseName + ")"); return this.phaseList[0]; } getModifiers(civCode) { return this.modifiers[civCode]; } deriveModifications(civCode) { this.modifiers[civCode] = this.TemplateLoader.deriveModifications(civCode); } derivePhaseList(technologyList, civCode) { // Load all of a civ's specific phase technologies for (let techcode of technologyList) if (this.TemplateLoader.isPhaseTech(techcode)) this.getTechnology(techcode, civCode); this.phaseList = UnravelPhases(this.phases); // Make sure all required generic phases are loaded and parsed for (let phasecode of this.phaseList) this.getTechnology(phasecode, civCode); } mergeRequirements(reqsA, reqsB) { if (!reqsA || !reqsB) return false; let finalReqs = clone(reqsA); for (let option of reqsB) for (let type in option) for (let opt in finalReqs) { if (!finalReqs[opt][type]) finalReqs[opt][type] = []; Array.prototype.push.apply(finalReqs[opt][type], option[type]); } return finalReqs; } } Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 24963) @@ -1,1004 +1,1004 @@ var API3 = function(m) { // defines a template. m.Template = m.Class({ "_init": function(sharedAI, templateName, template) { this._templateName = templateName; this._template = template; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; this._tpCache = new Map(); }, // Helper function to return a template value, adjusting for tech. "get": function(string) { let value = this._template; if (this._entityModif && this._entityModif.has(string)) return this._entityModif.get(string); else if (this._templateModif) { let owner = this._entity ? this._entity.owner : PlayerID; if (this._templateModif[owner] && this._templateModif[owner].has(string)) return this._templateModif[owner].get(string); } if (!this._tpCache.has(string)) { let args = string.split("/"); for (let arg of args) { if (value[arg] != undefined) value = value[arg]; else { value = undefined; break; } } this._tpCache.set(string, value); } return this._tpCache.get(string); }, "templateName": function() { return this._templateName; }, "genericName": function() { return this.get("Identity/GenericName"); }, "civ": function() { return this.get("Identity/Civ"); }, "matchLimit": function() { if (!this.get("TrainingRestrictions")) return undefined; return this.get("TrainingRestrictions/MatchLimit"); }, "classes": function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, "hasClass": function(name) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; return classes && classes.indexOf(name) != -1; }, "hasClasses": function(array) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; if (!classes) return false; for (let cls of array) if (classes.indexOf(cls) == -1) return false; return true; }, "requiredTech": function() { return this.get("Identity/RequiredTechnology"); }, "available": function(gameState) { let techRequired = this.requiredTech(); if (!techRequired) return true; return gameState.isResearched(techRequired); }, // specifically "phase": function() { let techRequired = this.requiredTech(); if (!techRequired) return 0; if (techRequired == "phase_village") return 1; if (techRequired == "phase_town") return 2; if (techRequired == "phase_city") return 3; if (techRequired.startsWith("phase_")) return 4; return 0; }, "cost": function(productionQueue) { if (!this.get("Cost")) return {}; let ret = {}; for (let type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, "costSum": function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return 0; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1); }, /** * Returns { "max": max, "min": min } or undefined if no obstruction. * max: radius of the outer circle surrounding this entity's obstruction shape * min: radius of the inner circle */ "obstructionRadius": function() { if (!this.get("Obstruction")) return undefined; if (this.get("Obstruction/Static")) { let w = +this.get("Obstruction/Static/@width"); let h = +this.get("Obstruction/Static/@depth"); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } if (this.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); return { "max": r, "min": r }; } let right = this.get("Obstruction/Obstructions/Right"); let left = this.get("Obstruction/Obstructions/Left"); if (left && right) { let w = +right["@x"] + right["@width"]/2 - left["@x"] + left["@width"]/2; let h = Math.max(+right["@z"] + right["@depth"]/2, +left["@z"] + left["@depth"]/2) - Math.min(+right["@z"] - right["@depth"]/2, +left["@z"] - left["@depth"]/2); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } return { "max": 0, "min": 0 }; // Units have currently no obstructions }, /** * Returns the radius of a circle surrounding this entity's footprint. */ "footprintRadius": function() { if (!this.get("Footprint")) return undefined; if (this.get("Footprint/Square")) { let w = +this.get("Footprint/Square/@width"); let h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w*w + h*h) / 2; } if (this.get("Footprint/Circle")) return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, "maxHitpoints": function() { return +(this.get("Health/Max") || 0); }, "isHealable": function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, "isRepairable": function() { return this.get("Repairable") !== undefined; }, "getPopulationBonus": function() { if (!this.get("Population")) return 0; return +this.get("Population/Bonus"); }, "resistanceStrengths": function() { let resistanceTypes = this.get("Resistance"); if (!resistanceTypes || !resistanceTypes.Entity) return undefined; let resistance = {}; if (resistanceTypes.Entity.Capture) resistance.Capture = +this.get("Resistance/Entity/Capture"); if (resistanceTypes.Entity.Damage) { resistance.Damage = {}; for (let damageType in resistanceTypes.Entity.Damage) resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType); } // ToDo: Resistance to StatusEffects. return resistance; }, "attackTypes": function() { if (!this.get("Attack")) return undefined; let ret = []; for (let type in this.get("Attack")) ret.push(type); return ret; }, "attackRange": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { "max": +this.get("Attack/" + type +"/MaxRange"), "min": +(this.get("Attack/" + type +"/MinRange") || 0) }; }, "attackStrengths": function(type) { let attackDamageTypes = this.get("Attack/" + type + "/Damage"); if (!attackDamageTypes) return undefined; let damage = {}; for (let damageType in attackDamageTypes) damage[damageType] = +attackDamageTypes[damageType]; return damage; }, "captureStrength": function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Capture") || 0; }, "attackTimes": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { "prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0), "repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses- , multiplier], … ]. "getCounteredClasses": function() { if (!this.get("Attack")) return undefined; let Classes = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]); } } return Classes; }, // returns true if the entity counters those classes. // TODO: refine using the multiplier "countersClasses": function(classes) { if (!this.get("Attack")) return false; let mcounter = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) mcounter.concat(bonusClasses.split(" ")); } } for (let i in classes) if (mcounter.indexOf(classes[i]) != -1) return true; return false; }, // returns, if it exists, the multiplier from each attack against a given class "getMultiplierAgainst": function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; if (this.get("Attack/" + type + "/Bonuses")) { for (let b in this.get("Attack/" + type + "/Bonuses")) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (!bonusClasses) continue; for (let bcl of bonusClasses.split(" ")) if (bcl == againstClass) return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier"); } } return 1; }, "buildableEntities": function(civ) { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "trainableEntities": function(civ) { let templates = this.get("ProductionQueue/Entities/_string"); if (!templates) return undefined; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "researchableTechs": function(gameState, civ) { let templates = this.get("ProductionQueue/Technologies/_string"); if (!templates) return undefined; let techs = templates.split(/\s+/); for (let i = 0; i < techs.length; ++i) { let tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; let civTech = tech.replace("{civ}", civ); techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } return techs; }, "resourceSupplyType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); return { "generic": type, "specific": subtype }; }, // will return either "food", "wood", "stone", "metal" and not treasure. "getResourceType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }, - "resourceSupplyMax": function() { return +this.get("ResourceSupply/Amount"); }, + "resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); }, "maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }, "resourceGatherRates": function() { if (!this.get("ResourceGatherer")) return undefined; let ret = {}; let baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (let r in this.get("ResourceGatherer/Rates")) ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed; return ret; }, "resourceDropsiteTypes": function() { if (!this.get("ResourceDropsite")) return undefined; let types = this.get("ResourceDropsite/Types"); return types ? types.split(/\s+/) : []; }, "garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); }, "garrisonMax": function() { return this.get("GarrisonHolder/Max"); }, "garrisonSize": function() { return this.get("Garrisonable/Size"); }, "garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); }, "getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); }, "getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, "getGarrisonArrowClasses": function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, "buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); }, "promotion": function() { return this.get("Promotion/Entity"); }, "isPackable": function() { return this.get("Pack") != undefined; }, "isHuntable": function() { // Do not hunt retaliating animals (dead animals can be used). // Assume entities which can attack, will attack. return this.get("ResourceSupply/KillBeforeGather") && (!this.get("Health") || !this.get("Attack")); }, "walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); }, "trainingCategory": function() { return this.get("TrainingRestrictions/Category"); }, "buildTime": function(productionQueue) { let time = +this.get("Cost/BuildTime"); if (productionQueue) time *= productionQueue.techCostMultiplier("time"); return time; }, "buildCategory": function() { return this.get("BuildRestrictions/Category"); }, "buildDistance": function() { let distance = this.get("BuildRestrictions/Distance"); if (!distance) return undefined; let ret = {}; for (let key in distance) ret[key] = this.get("BuildRestrictions/Distance/" + key); return ret; }, "buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); }, "buildTerritories": function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory")) return undefined; return this.get("BuildRestrictions/Territory").split(/\s+/); }, "hasBuildTerritory": function(territory) { let territories = this.buildTerritories(); return territories && territories.indexOf(territory) != -1; }, "hasTerritoryInfluence": function() { return this.get("TerritoryInfluence") !== undefined; }, "hasDefensiveFire": function() { if (!this.get("Attack/Ranged")) return false; return this.getDefaultArrow() || this.getArrowMultiplier(); }, "territoryInfluenceRadius": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); return -1; }, "territoryInfluenceWeight": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); return -1; }, "territoryDecayRate": function() { return +(this.get("TerritoryDecay/DecayRate") || 0); }, "defaultRegenRate": function() { return +(this.get("Capturable/RegenRate") || 0); }, "garrisonRegenRate": function() { return +(this.get("Capturable/GarrisonRegenRate") || 0); }, "visionRange": function() { return +this.get("Vision/Range"); }, "gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); }, "isBuilder": function() { return this.get("Builder") !== undefined; }, "isGatherer": function() { return this.get("ResourceGatherer") !== undefined; }, "canGather": function(type) { let gatherRates = this.get("ResourceGatherer/Rates"); if (!gatherRates) return false; for (let r in gatherRates) if (r.split('.')[0] === type) return true; return false; }, "isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; }, /** * returns true if the tempalte can capture the given target entity * if no target is given, returns true if the template has the Capture attack */ "canCapture": function(target) { if (!this.get("Attack/Capture")) return false; if (!target) return true; if (!target.get("Capturable")) return false; let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string"); return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return "Garrisonable" in this._template; }, }); // defines an entity, with a super Template. // also redefines several of the template functions where the only change is applying aura and tech modifications. m.Entity = m.Class({ "_super": m.Template, "_init": function(sharedAI, entity) { this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template)); this._entity = entity; this._ai = sharedAI; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; // save a reference to the entity tech/aura modifications if (!sharedAI._entitiesModifications.has(entity.id)) sharedAI._entitiesModifications.set(entity.id, new Map()); this._entityModif = sharedAI._entitiesModifications.get(entity.id); }, "toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, "id": function() { return this._entity.id; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data should not be shared with any other AI scripts.) */ "getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); }, /** * Sets extra data to be associated with this entity. */ "setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); }, "deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; }, "deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); }, "position": function() { return this._entity.position; }, "angle": function() { return this._entity.angle; }, "isIdle": function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, "getStance": function() { return this._entity.stance !== undefined ? this._entity.stance : undefined; }, "unitAIState": function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; }, "unitAIOrderData": function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; }, "hitpoints": function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; }, "isHurt": function() { return this.hitpoints() < this.maxHitpoints(); }, "healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); }, "needsHeal": function() { return this.isHurt() && this.isHealable(); }, "needsRepair": function() { return this.isHurt() && this.isRepairable(); }, "decaying": function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; }, "capturePoints": function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; }, "isInvulnerable": function() { return this._entity.invulnerability || false; }, "isSharedDropsite": function() { return this._entity.sharedDropsite === true; }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ "trainingQueue": function() { let queue = this._entity.trainingQueue; return queue; }, "trainingQueueTime": function() { let queue = this._entity.trainingQueue; if (!queue) return undefined; let time = 0; for (let item of queue) time += item.timeRemaining; return time/1000; }, "foundationProgress": function() { if (this._entity.foundationProgress === undefined) return undefined; return this._entity.foundationProgress; }, "getBuilders": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return []; return this._entity.foundationBuilders; }, "getBuildersNb": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return 0; return this._entity.foundationBuilders.length; }, "owner": function() { return this._entity.owner; }, "isOwn": function(player) { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === player; }, "resourceSupplyAmount": function() { if (this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, "resourceSupplyNumGatherers": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this._entity.resourceSupplyNumGatherers; return undefined; }, "isFull": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, "resourceCarrying": function() { if (this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, "currentGatherRate": function() { // returns the gather rate for the current target if applicable. if (!this.get("ResourceGatherer")) return undefined; if (this.unitAIOrderData().length && (this.unitAIState().split(".")[1] == "GATHER" || this.unitAIState().split(".")[1] == "RETURNRESOURCE")) { let res; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[0].target); else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[1].target); if (!res) return 0; let type = res.resourceSupplyType(); if (!type) return 0; if (type.generic == "treasure") return 1000; let tstring = type.generic + "." + type.specific; let rate = +this.get("ResourceGatherer/BaseSpeed"); rate *= +this.get("ResourceGatherer/Rates/" +tstring); if (rate) return rate; return 0; } return undefined; }, "garrisoned": function() { return this._entity.garrisoned; }, "garrisonedSlots": function() { let count = 0; if (this._entity.garrisoned) for (let ent of this._entity.garrisoned) count += +this._ai._entities.get(ent).garrisonSize(); return count; }, "canGarrisonInside": function() { return this.garrisonedSlots() < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { if (!this.get("Attack")) return false; for (let type in this.get("Attack")) { if (type == "Slaughter") continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses)) return true; } return false; }, /** * Derived from Attack.js' similary named function. * @return {boolean} - Whether an entity can attack a given target. */ "canAttackTarget": function(target, allowCapture) { let attackTypes = this.get("Attack"); if (!attackTypes) return false; let canCapture = allowCapture && this.canCapture(target); let health = target.get("Health"); if (!health) return canCapture; for (let type in attackTypes) { if (type == "Capture" ? !canCapture : target.isInvulnerable()) continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses)) return true; }; return false; }, "move": function(x, z, queued = false) { Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, "moveToRange": function(x, z, min, max, queued = false) { Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); return this; }, "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued }); return this; }, // violent, aggressive, defensive, passive, standground "setStance": function(stance, queued = false) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance, "queued": queued }); return this; }, "stopMoving": function() { Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false }); }, "unload": function(id) { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] }); return this; }, // Unloads all owned units, don't unload allies "unloadAll": function() { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] }); return this; }, "garrison": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "attack": function(unitId, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued }); return this; }, // moveApart from a point in the opposite direction with a distance dist "moveApart": function(point, dist) { if (this.position() !== undefined) { let direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; let norm = m.VectorDistance(point, this.position()); if (norm === 0) direction = [1, 0]; else { direction[0] /= norm; direction[1] /= norm; } Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false }); } return this; }, // Flees from a unit in the opposite direction. "flee": function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0], this.position()[1] - unitToFleeFrom.position()[1]]; let dist = m.VectorDistance(unitToFleeFrom.position(), this.position()); FleeDirection[0] = 40 * FleeDirection[0]/dist; FleeDirection[1] = 40 * FleeDirection[1]/dist; Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false }); } return this; }, "gather": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "repair": function(target, autocontinue = false, queued = false) { Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued }); return this; }, "returnResources": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "destroy": function() { Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] }); return this; }, "barter": function(buyType, sellType, amount) { Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount }); return this; }, "tradeRoute": function(target, source) { Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false }); return this; }, "setRallyPoint": function(target, command) { let data = { "command": command, "target": target.id() }; Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data }); return this; }, "unsetRallyPoint": function() { Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] }); return this; }, "train": function(civ, type, count, metadata, promotedTypes) { let trainable = this.trainableEntities(civ); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) == -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } Engine.PostCommand(PlayerID, { "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata, "promoted": promotedTypes }); return this; }, "construct": function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting Engine.PostCommand(PlayerID, { "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false, "metadata": metadata // can be undefined }); return this; }, "research": function(template) { Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template }); return this; }, "stopProduction": function(id) { Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id }); return this; }, "stopAllProduction": function(percentToStopAt) { let queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (let item of queue) if (item.progress < percentToStopAt) Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id }); return this; }, "guard": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "removeGuard": function() { Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] }); return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 24963) @@ -1,185 +1,471 @@ function ResourceSupply() {} ResourceSupply.prototype.Schema = "Provides a supply of one particular type of resource." + "" + - "1000" + + "1000" + + "1000" + "food.meat" + "false" + "25" + "0.8" + + "" + + "" + + "2" + + "1000" + + "" + + "" + + "alive" + + "2" + + "1000" + + "500" + + "" + + "" + + "dead notGathered" + + "-2" + + "1000" + + "" + + "" + + "dead" + + "-1" + + "1000" + + "500" + + "" + + "" + "" + "" + "" + "" + - "" + + "" + "Infinity" + "" + + "" + + "" + + "Infinity" + + "" + + "" + "" + Resources.BuildChoicesSchema(true, true) + "" + "" + "" + "" + "" + "" + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "alive" + + "dead" + + "gathered" + + "notGathered" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + ""; ResourceSupply.prototype.Init = function() { - // Current resource amount (non-negative) - this.amount = this.GetMaxAmount(); + this.amount = +(this.template.Initial || this.template.Max); + // Includes the ones that are tasked but not here yet, i.e. approaching. this.gatherers = []; + this.activeGatherers = []; let [type, subtype] = this.template.Type.split('.'); this.cachedType = { "generic": type, "specific": subtype }; + + if (this.template.Change) + { + this.timers = {}; + this.cachedChanges = {}; + } }; ResourceSupply.prototype.IsInfinite = function() { - return !isFinite(+this.template.Amount); + return !isFinite(+this.template.Max); }; ResourceSupply.prototype.GetKillBeforeGather = function() { return this.template.KillBeforeGather == "true"; }; ResourceSupply.prototype.GetMaxAmount = function() { - return +this.template.Amount; + return this.maxAmount; }; ResourceSupply.prototype.GetCurrentAmount = function() { return this.amount; }; ResourceSupply.prototype.GetMaxGatherers = function() { return +this.template.MaxGatherers; }; ResourceSupply.prototype.GetNumGatherers = function() { return this.gatherers.length; }; /** + * @return {number} - The number of currently active gatherers. + */ +ResourceSupply.prototype.GetNumActiveGatherers = function() +{ + return this.activeGatherers.length; +}; + +/** * @return {{ "generic": string, "specific": string }} An object containing the subtype and the generic type. All resources must have both. */ ResourceSupply.prototype.GetType = function() { return this.cachedType; }; /** * @param {number} gathererID - The gatherer's entity id. * @return {boolean} - Whether the ResourceSupply can have this additional gatherer or it is already gathering. */ ResourceSupply.prototype.IsAvailableTo = function(gathererID) { return this.IsAvailable() || this.IsGatheringUs(gathererID); }; /** * @return {boolean} - Whether this entity can have an additional gatherer. */ ResourceSupply.prototype.IsAvailable = function() { return this.amount && this.gatherers.length < this.GetMaxGatherers(); }; /** * @param {number} entity - The entityID to check for. * @return {boolean} - Whether the given entity is already gathering at us. */ ResourceSupply.prototype.IsGatheringUs = function(entity) { return this.gatherers.indexOf(entity) !== -1; }; /** * Each additional gatherer decreases the rate following a geometric sequence, with diminishingReturns as ratio. * @return {number} The diminishing return if any, null otherwise. */ ResourceSupply.prototype.GetDiminishingReturns = function() { if (!this.template.DiminishingReturns) return null; let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity); if (!diminishingReturns) return null; let numGatherers = this.GetNumGatherers(); if (numGatherers > 1) return diminishingReturns == 1 ? 1 : (1 - Math.pow(diminishingReturns, numGatherers)) / (1 - diminishingReturns) / numGatherers; return null; }; /** * @param {number} amount The amount of resources that should be taken from the resource supply. The amount must be positive. * @return {{ "amount": number, "exhausted": boolean }} The current resource amount in the entity and whether it's exhausted or not. */ ResourceSupply.prototype.TakeResources = function(amount) { + if (this.IsInfinite()) + return { "amount": amount, "exhausted": false }; + + return { + "amount": Math.abs(this.Change(-amount)), + "exhausted": this.amount == 0 + }; +}; + +/** + * @param {number} change - The amount to change the resources with (can be negative). + * @return {number} - The actual change in resourceSupply. + */ +ResourceSupply.prototype.Change = function(change) +{ // Before changing the amount, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); - if (this.IsInfinite()) - return { "amount": amount, "exhausted": false }; - - let oldAmount = this.GetCurrentAmount(); - this.amount = Math.max(0, oldAmount - amount); + let oldAmount = this.amount; + this.amount = Math.min(Math.max(oldAmount + change, 0), this.maxAmount); - let isExhausted = this.GetCurrentAmount() == 0; - // Remove entities that have been exhausted - if (isExhausted) + // Remove entities that have been exhausted. + if (this.amount == 0) Engine.DestroyEntity(this.entity); - Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { "from": oldAmount, "to": this.GetCurrentAmount() }); + let actualChange = this.amount - oldAmount; + if (actualChange != 0) + { + Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { + "from": oldAmount, + "to": this.amount + }); + this.CheckTimers(); + } + return actualChange; +}; - return { "amount": oldAmount - this.GetCurrentAmount(), "exhausted": isExhausted }; +/** + * @param {number} newValue - The value to set the current amount to. + */ +ResourceSupply.prototype.SetAmount = function(newValue) +{ + this.Change(newValue - this.amount); }; /** * @param {number} gathererID - The gatherer to add. * @return {boolean} - Whether the gatherer was successfully added to the entity's gatherers list * or the entity was already gathering us. */ ResourceSupply.prototype.AddGatherer = function(gathererID) { if (!this.IsAvailable()) return false; if (this.IsGatheringUs(gathererID)) return true; this.gatherers.push(gathererID); Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); return true; }; /** + * @param {number} player - The playerID owning the gatherer. + * @param {number} entity - The entityID gathering. + * + * @return {boolean} - Whether the gatherer was successfully added to the active-gatherers list + * or the entity was already in that list. + */ +ResourceSupply.prototype.AddActiveGatherer = function(entity) +{ + if (!this.AddGatherer(entity)) + return false; + + if (this.activeGatherers.indexOf(entity) == -1) + { + this.activeGatherers.push(entity); + this.CheckTimers(); + } + return true; +}; + +/** * @param {number} gathererID - The gatherer's entity id. * @todo: Should this return false if the gatherer didn't gather from said resource? */ ResourceSupply.prototype.RemoveGatherer = function(gathererID) { let index = this.gatherers.indexOf(gathererID); + if (index != -1) + { + this.gatherers.splice(index, 1); + Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); + } + + index = this.activeGatherers.indexOf(gathererID); if (index == -1) return; + this.activeGatherers.splice(index, 1); + this.CheckTimers(); +}; - this.gatherers.splice(index, 1); - Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); +/** + * Checks whether a timer ought to be added or removed. + */ +ResourceSupply.prototype.CheckTimers = function() +{ + if (!this.template.Change || this.IsInfinite()) + return; + + for (let changeKey in this.template.Change) + { + if (!this.CheckState(changeKey)) + { + this.StopTimer(changeKey); + continue; + } + let template = this.template.Change[changeKey]; + if (this.amount < +(template.LowerLimit || -1) || + this.amount > +(template.UpperLimit || this.GetMaxAmount())) + { + this.StopTimer(changeKey); + continue; + } + + if (this.cachedChanges[changeKey] == 0) + { + this.StopTimer(changeKey); + continue; + } + + if (!this.timers[changeKey]) + this.StartTimer(changeKey); + } +}; + +/** + * This verifies whether the current state of the supply matches the ones needed + * for the specific timer to run. + * + * @param {string} changeKey - The name of the Change to verify the state for. + * @return {boolean} - Whether the timer may run. + */ +ResourceSupply.prototype.CheckState = function(changeKey) +{ + let template = this.template.Change[changeKey]; + if (!template.State) + return true; + + let states = template.State; + let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); + if (states.indexOf("alive") != -1 && !cmpHealth && states.indexOf("dead") == -1 || + states.indexOf("dead") != -1 && cmpHealth && states.indexOf("alive") == -1) + return false; + + let activeGatherers = this.GetNumActiveGatherers(); + if (states.indexOf("gathered") != -1 && activeGatherers == 0 && states.indexOf("notGathered") == -1 || + states.indexOf("notGathered") != -1 && activeGatherers > 0 && states.indexOf("gathered") == -1) + return false; + + return true; +}; + +/** + * @param {string} changeKey - The name of the Change to apply to the entity. + */ +ResourceSupply.prototype.StartTimer = function(changeKey) +{ + if (this.timers[changeKey]) + return; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let interval = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Interval", +(this.template.Change[changeKey].Interval || 1000), this.entity); + this.timers[changeKey] = cmpTimer.SetInterval(this.entity, IID_ResourceSupply, "TimerTick", interval, interval, changeKey); +}; + +/** + * @param {string} changeKey - The name of the change to stop the timer for. + */ +ResourceSupply.prototype.StopTimer = function(changeKey) +{ + if (!this.timers[changeKey]) + return; + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timers[changeKey]); + delete this.timers[changeKey]; +}; + +/** + * @param {string} changeKey - The name of the change to apply to the entity. + */ +ResourceSupply.prototype.TimerTick = function(changeKey) +{ + let template = this.template.Change[changeKey]; + if (!template || !this.Change(this.cachedChanges[changeKey])) + this.StopTimer(changeKey); +}; + +/** + * Since the supposed changes can be affected by modifications, and applying those + * are slow, do not calculate them every timer tick. + */ +ResourceSupply.prototype.RecalculateValues = function() +{ + this.maxAmount = ApplyValueModificationsToEntity("ResourceSupply/Max", +this.template.Max, this.entity); + if (!this.template.Change || this.IsInfinite()) + return; + + for (let changeKey in this.template.Change) + this.cachedChanges[changeKey] = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Value", +this.template.Change[changeKey].Value, this.entity); + + this.CheckTimers(); +}; + +/** + * @param {{ "component": string, "valueNames": string[] }} msg - Message containing a list of values that were changed. + */ +ResourceSupply.prototype.OnValueModification = function(msg) +{ + if (msg.component != "ResourceSupply") + return; + + this.RecalculateValues(); +}; + +/** + * @param {{ "from": number, "to": number }} msg - Message containing the old new owner. + */ +ResourceSupply.prototype.OnOwnershipChanged = function(msg) +{ + if (msg.to == INVALID_PLAYER) + { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + for (let changeKey in this.timers) + cmpTimer.CancelTimer(this.timers[changeKey]); + } + else + this.RecalculateValues(); +}; + +/** + * @param {{ "entity": number, "newentity": number }} msg - Message to what the entity has been renamed. + */ +ResourceSupply.prototype.OnEntityRenamed = function(msg) +{ + let cmpResourceSupplyNew = Engine.QueryInterface(msg.newentity, IID_ResourceSupply); + if (cmpResourceSupplyNew) + cmpResourceSupplyNew.SetAmount(this.GetCurrentAmount()); }; Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 24963) @@ -1,6527 +1,6527 @@ function UnitAI() {} UnitAI.prototype.Schema = "Controls the unit's movement, attacks, etc, in response to commands from the player." + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "standground" + "skittish" + "passive-defensive" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + ""; // 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! // There are some response options, triggered when targets are detected: // respondFlee: run away // respondFleeOnSight: run away when an enemy is sighted // 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, "respondFlee": false, "respondFleeOnSight": false, "respondChase": true, "respondChaseBeyondVision": true, "respondStandGround": false, "respondHoldGround": false, "selectable": true }, "aggressive": { "targetVisibleEnemies": true, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": true, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": true }, "defensive": { "targetVisibleEnemies": true, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": true, "selectable": true }, "passive": { "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": true, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": true }, "standground": { "targetVisibleEnemies": true, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": true, "respondHoldGround": false, "selectable": true }, "skittish": { "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": true, "respondFleeOnSight": true, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": false }, "passive-defensive": { "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": true, "selectable": false }, "none": { // Only to be used by AI or trigger scripts "targetVisibleEnemies": false, "targetAttackersAlways": false, "respondFlee": false, "respondFleeOnSight": false, "respondChase": false, "respondChaseBeyondVision": false, "respondStandGround": false, "respondHoldGround": false, "selectable": false } }; // These orders always require a packed unit, so if a unit that is unpacking is given one of these orders, // it will immediately cancel unpacking. var g_OrdersCancelUnpacking = new Set([ "FormationWalk", "Walk", "WalkAndFight", "WalkToTarget", "Patrol", "Garrison" ]); // When leaving a foundation, we want to be clear of it by this distance. var g_LeaveFoundationRange = 4; UnitAI.prototype.notifyToCheerInRange = 30; const ACCEPT_ORDER = true; const REJECT_ORDER = false; // See ../helpers/FSM.js for some documentation of this FSM specification syntax UnitAI.prototype.UnitFsmSpec = { // Default event handlers: "MovementUpdate": function(msg) { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }, "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. }, "LosAttackRangeUpdate": function(msg) { // Ignore newly-seen enemy units by default. }, "Attacked": function(msg) { // ignore attacker }, "PackFinished": function(msg) { // ignore }, "PickupCanceled": function(msg) { // ignore }, "TradingCanceled": function(msg) { // ignore }, "GuardedAttacked": function(msg) { // ignore }, "OrderTargetRenamed": function() { // By default, trigger an exit-reenter // so that state preconditions are checked against the new entity // (there is no reason to assume the target is still valid). this.SetNextState(this.GetCurrentState()); }, // Formation handlers: "FormationLeave": function(msg) { // Overloaded by FORMATIONMEMBER // We end up here if LeaveFormation was called when the entity // was executing an order in an individual state, so we must // discard the order now that it has been executed. if (this.order && this.order.type === "LeaveFormation") this.FinishOrder(); }, // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { if (!this.AbleToMove()) return REJECT_ORDER; if (this.CanPack()) { // If the controller is IDLE, this is just the regular reformation timer. // In that case we don't actually want to move, as that would unpack us. let cmpControllerAI = Engine.QueryInterface(this.GetFormationController(), IID_UnitAI); if (cmpControllerAI.IsIdle()) return REJECT_ORDER; this.PushOrderFront("Pack", { "force": true }); } else this.SetNextState("FORMATIONMEMBER.WALKING"); return ACCEPT_ORDER; }, // Special orders: // (these will be overridden by various states) "Order.LeaveFoundation": function(msg) { if (!this.WillMoveFromFoundation(msg.data.target)) return REJECT_ORDER; this.order.data.min = g_LeaveFoundationRange; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, // Individual orders: "Order.LeaveFormation": function() { if (!this.IsFormationMember()) return REJECT_ORDER; let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { cmpFormation.SetRearrange(false); // Triggers FormationLeave, which ultimately will FinishOrder, // discarding this order. cmpFormation.RemoveMembers([this.entity]); cmpFormation.SetRearrange(true); } return ACCEPT_ORDER; }, "Order.Stop": function(msg) { this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Walk": function(msg) { if (!this.AbleToMove()) return REJECT_ORDER; if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } this.SetHeldPosition(this.order.data.x, this.order.data.z); this.order.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, "Order.WalkAndFight": function(msg) { if (!this.AbleToMove()) return REJECT_ORDER; if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } this.SetHeldPosition(this.order.data.x, this.order.data.z); this.order.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING"); return ACCEPT_ORDER; }, "Order.WalkToTarget": function(msg) { if (!this.AbleToMove()) return REJECT_ORDER; if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } if (this.CheckRange(this.order.data)) return REJECT_ORDER; this.order.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, "Order.PickupUnit": function(msg) { let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) return REJECT_ORDER; let range = cmpGarrisonHolder.GetLoadingRange(); this.order.data.min = range.min; this.order.data.max = range.max; if (this.CheckRange(this.order.data)) return REJECT_ORDER; // Check if we need to move // If the target can reach us and we are reasonably close, don't move. // TODO: it would be slightly more optimal to check for real, not bird-flight distance. let cmpPassengerMotion = Engine.QueryInterface(this.order.data.target, IID_UnitMotion); if (cmpPassengerMotion && cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) && PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) < 200) this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); else this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING"); return ACCEPT_ORDER; }, "Order.Guard": function(msg) { if (!this.AddGuard(this.order.data.target)) return REJECT_ORDER; if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("INDIVIDUAL.GUARD.ESCORTING"); else this.SetNextState("INDIVIDUAL.GUARD.GUARDING"); return ACCEPT_ORDER; }, "Order.Flee": function(msg) { this.SetNextState("INDIVIDUAL.FLEEING"); return ACCEPT_ORDER; }, "Order.Attack": function(msg) { let type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture); if (!type) return REJECT_ORDER; this.order.data.attackType = type; this.RememberTargetPosition(); if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { if (this.CanUnpack()) { this.PushOrderFront("Unpack", { "force": true }); return ACCEPT_ORDER; } // Cancel any current packing order. if (this.EnsureCorrectPackStateForAttack(false)) this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); return ACCEPT_ORDER; } // If 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.AbleToMove()) return REJECT_ORDER; if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } // If we're currently packing/unpacking, make sure we are packed, so we can move. if (this.EnsureCorrectPackStateForAttack(true)) this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); return ACCEPT_ORDER; }, "Order.Patrol": function(msg) { if (!this.AbleToMove()) return REJECT_ORDER; if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } this.order.data.relaxed = true; this.SetNextState("INDIVIDUAL.PATROL.PATROLLING"); return ACCEPT_ORDER; }, "Order.Heal": function(msg) { if (!this.TargetIsAlive(this.order.data.target)) return REJECT_ORDER; // Healers can't heal themselves. if (this.order.data.target == this.entity) return REJECT_ORDER; if (this.CheckTargetRange(this.order.data.target, IID_Heal)) { this.SetNextState("INDIVIDUAL.HEAL.HEALING"); return ACCEPT_ORDER; } if (this.GetStance().respondStandGround && !this.order.data.force) return REJECT_ORDER; this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); return ACCEPT_ORDER; }, "Order.Gather": function(msg) { if (!this.CanGather(this.order.data.target)) { this.SetNextState("INDIVIDUAL.GATHER.FINDINGNEWTARGET"); return ACCEPT_ORDER; } // If the unit is full go to the nearest dropsite instead of trying to gather. // Unless our target is a treasure which we cannot be full enough with (we can't carry treasures). let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (msg.data.type.generic !== "treasure" && cmpResourceGatherer && !cmpResourceGatherer.CanCarryMore(msg.data.type.generic)) { let nearestDropsite = this.FindNearestDropsite(msg.data.type.generic); if (nearestDropsite) this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false, "type": msg.data.type }); // Players expect the unit to move, so walk to the target instead of trying to gather. else if (!this.FinishOrder()) this.WalkToTarget(msg.data.target, false); return ACCEPT_ORDER; } 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, false)) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed return REJECT_ORDER; } // 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); } // We couldn't move there, or the target moved away else { let data = this.order.data; if (!this.FinishOrder()) this.PushOrderFront("GatherNearPosition", { "x": data.lastPos.x, "z": data.lastPos.z, "type": data.type, "template": data.template }); } return ACCEPT_ORDER; } this.PushOrderFront("Attack", { "target": this.order.data.target, "force": !!this.order.data.force, "hunting": true, "allowCapture": false }); return ACCEPT_ORDER; } this.RememberTargetPosition(); if (!this.order.data.initPos) this.order.data.initPos = this.order.data.lastPos; if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) this.SetNextState("INDIVIDUAL.GATHER.GATHERING"); else this.SetNextState("INDIVIDUAL.GATHER.APPROACHING"); return ACCEPT_ORDER; }, "Order.GatherNearPosition": function(msg) { this.SetNextState("INDIVIDUAL.GATHER.WALKING"); this.order.data.initPos = { 'x': this.order.data.x, 'z': this.order.data.z }; this.order.data.relaxed = true; return ACCEPT_ORDER; }, "Order.ReturnResource": function(msg) { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer)) { cmpResourceGatherer.CommitResources(this.order.data.target); this.SetDefaultAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order. this.FinishOrder(); } else this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING"); return ACCEPT_ORDER; }, "Order.Trade": function(msg) { // We must check if this trader has both markets in case it was a back-to-work order. let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader || !cmpTrader.HasBothMarkets()) return REJECT_ORDER; this.waypoints = []; this.SetNextState("TRADE.APPROACHINGMARKET"); return ACCEPT_ORDER; }, "Order.Repair": function(msg) { if (this.CheckTargetRange(this.order.data.target, IID_Builder)) this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING"); else this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING"); return ACCEPT_ORDER; }, "Order.Garrison": function(msg) { if (!this.AbleToMove()) { // Garrisoned turrets (unable to move) go IDLE. this.SetNextState("IDLE"); return ACCEPT_ORDER; } if (this.IsGarrisoned()) { this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); return ACCEPT_ORDER; } // Also pack when we are in range. if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } if (this.CheckGarrisonRange(this.order.data.target)) this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); else this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); return ACCEPT_ORDER; }, "Order.Ungarrison": function() { this.FinishOrder(); this.isGarrisoned = false; return ACCEPT_ORDER; }, "Order.Cheer": function(msg) { return REJECT_ORDER; }, "Order.Pack": function(msg) { if (!this.CanPack()) return REJECT_ORDER; this.SetNextState("INDIVIDUAL.PACKING"); return ACCEPT_ORDER; }, "Order.Unpack": function(msg) { if (!this.CanUnpack()) return REJECT_ORDER; this.SetNextState("INDIVIDUAL.UNPACKING"); return ACCEPT_ORDER; }, "Order.MoveToChasingPoint": function(msg) { // Overriden by the CHASING state. // Can however happen outside of it when renaming... // TODO: don't use an order for that behaviour. return REJECT_ORDER; }, // 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.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkAndFight": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.SetNextState("WALKINGANDFIGHTING"); return ACCEPT_ORDER; }, "Order.MoveIntoFormation": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.SetNextState("FORMING"); return ACCEPT_ORDER; }, // Only used by other orders to walk there in formation. "Order.WalkToTargetRange": function(msg) { if (this.CheckRange(this.order.data)) return REJECT_ORDER; this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkToTarget": function(msg) { if (this.CheckRange(this.order.data)) return REJECT_ORDER; this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkToPointRange": function(msg) { if (this.CheckRange(this.order.data)) return REJECT_ORDER; this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.Patrol": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.SetNextState("PATROL.PATROLLING"); return ACCEPT_ORDER; }, "Order.Guard": function(msg) { this.CallMemberFunction("Guard", [msg.data.target, false]); Engine.QueryInterface(this.entity, IID_Formation).Disband(); return ACCEPT_ORDER; }, "Order.Stop": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.ResetOrderVariant(); if (!this.IsAttackingAsFormation()) this.CallMemberFunction("Stop", [false]); this.FinishOrder(); return ACCEPT_ORDER; // Don't move the members back into formation, // as the formation then resets and it looks odd when walk-stopping. // TODO: this should be improved in the formation reshaping code. }, "Order.Attack": function(msg) { let target = msg.data.target; let allowCapture = msg.data.allowCapture; let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) target = cmpTargetUnitAI.GetFormationController(); if (!this.CheckFormationTargetAttackRange(target)) { if (this.CanAttack(target) && this.CheckTargetVisible(target)) { this.SetNextState("COMBAT.APPROACHING"); return ACCEPT_ORDER; } return REJECT_ORDER; } this.CallMemberFunction("Attack", [target, allowCapture, false]); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (cmpAttack && cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Garrison": function(msg) { if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder)) return REJECT_ORDER; if (!this.CheckGarrisonRange(msg.data.target)) { if (!this.CheckTargetVisible(msg.data.target)) return REJECT_ORDER; this.SetNextState("GARRISON.APPROACHING"); } else this.SetNextState("GARRISON.GARRISONING"); return ACCEPT_ORDER; }, "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); } // We couldn't move there, or the target moved away else { let data = msg.data; if (!this.FinishOrder()) this.PushOrderFront("GatherNearPosition", { "x": data.lastPos.x, "z": data.lastPos.z, "type": data.type, "template": data.template }); } return ACCEPT_ORDER; } this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "allowCapture": false, "min": 0, "max": 10 }); return ACCEPT_ORDER; } // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) return REJECT_ORDER; // TODO: Should we issue a gather-near-position order // if the target isn't gatherable/doesn't exist anymore? if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return REJECT_ORDER; } this.CallMemberFunction("Gather", [msg.data.target, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.GatherNearPosition": function(msg) { // TODO: on what should we base this range? 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 ACCEPT_ORDER; } this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Heal": function(msg) { // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) return REJECT_ORDER; if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return REJECT_ORDER; } this.CallMemberFunction("Heal", [msg.data.target, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Repair": function(msg) { // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) return REJECT_ORDER; if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return REJECT_ORDER; } this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.ReturnResource": function(msg) { // TODO: on what should we base this range? if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.CheckTargetVisible(msg.data.target)) return REJECT_ORDER; if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return REJECT_ORDER; } this.CallMemberFunction("ReturnResource", [msg.data.target, false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Pack": function(msg) { this.CallMemberFunction("Pack", [false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "Order.Unpack": function(msg) { this.CallMemberFunction("Unpack", [false]); this.SetNextState("MEMBER"); return ACCEPT_ORDER; }, "IDLE": { "enter": function(msg) { // Turn rearrange off. Otherwise, if the formation is idle // but individual units go off to fight, // any death will rearrange the formation, which looks odd. // Instead, move idle units in formation on a timer. let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(false); // Start the timer on the next turn to catch up with potential stragglers. this.StartTimer(100, 2000); this.isIdle = true; this.CallMemberFunction("ResetIdle"); return false; }, "leave": function() { this.isIdle = false; this.StopTimer(); }, "Timer": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; if (this.TestAllMemberFunction("IsIdle")) cmpFormation.MoveMembersIntoFormation(false, false); }, }, "WALKING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopTimer(); this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.veryObstructed && !this.timer) { // It's possible that the controller (with large clearance) // is stuck, but not the individual units. // Ask them to move individually for a little while. this.CallMemberFunction("MoveTo", [this.order.data]); this.StartTimer(3000); return; } else if (this.timer) return; if (msg.likelyFailure || this.CheckRange(this.order.data)) this.FinishOrder(); }, "Timer": function() { // Reenter to reset the pathfinder state. this.SetNextState("WALKING"); } }, "WALKINGANDFIGHTING": { "enter": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true, "combat"); if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.StartTimer(0, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckRange(this.order.data)) this.FinishOrder(); }, }, "PATROL": { "enter": function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { this.FinishOrder(); return true; } // Memorize the origin position in case that we want to go back. if (!this.patrolStartPosOrder) { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; } this.SetAnimationVariant("combat"); return false; }, "leave": function() { delete this.patrolStartPosOrder; this.SetDefaultAnimationVariant(); }, "PATROLLING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true, "combat"); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld() || !this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.StartTimer(0, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange)) return; if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); this.PushOrder(this.order.type, this.order.data); this.SetNextState("CHECKINGWAYPOINT"); }, }, "CHECKINGWAYPOINT": { "enter": function() { this.StartTimer(0, 1000); this.stopSurveying = 0; // TODO: pick a proper animation return false; }, "leave": function() { this.StopTimer(); delete this.stopSurveying; }, "Timer": function(msg) { if (this.stopSurveying >= +this.template.PatrolWaitTime) { this.FinishOrder(); return; } this.FindWalkAndFightTargets(); ++this.stopSurveying; } } }, "GARRISON": { "APPROACHING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); if (!this.MoveToGarrisonRange(this.order.data.target)) { this.FinishOrder(); return true; } // If the garrisonholder should pickup, warn it so it can take needed action. let 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 }); } return false; }, "leave": function() { this.StopMoving(); if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.likelySuccess) this.SetNextState("GARRISONING"); }, }, "GARRISONING": { "enter": function() { this.CallMemberFunction("Garrison", [this.order.data.target, false]); // We might have been disbanded due to the lack of members. if (Engine.QueryInterface(this.entity, IID_Formation).GetMemberCount()) this.SetNextState("MEMBER"); return true; }, }, }, "FORMING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !this.CheckRange(this.order.data)) return; this.FinishOrder(); } }, "COMBAT": { "APPROACHING": { "enter": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true, "combat"); if (!this.MoveFormationToTargetAttackRange(this.order.data.target)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { let target = this.order.data.target; let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) target = cmpTargetUnitAI.GetFormationController(); let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.CallMemberFunction("Attack", [target, this.order.data.allowCapture, false]); if (cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); }, }, "ATTACKING": { // Wait for individual members to finish "enter": function(msg) { let target = this.order.data.target; let allowCapture = this.order.data.allowCapture; if (!this.CheckFormationTargetAttackRange(target)) { if (this.CanAttack(target) && this.CheckTargetVisible(target)) { this.SetNextState("COMBAT.APPROACHING"); return true; } this.FinishOrder(); return true; } let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // TODO fix the rearranging while attacking as formation cmpFormation.SetRearrange(!this.IsAttackingAsFormation()); cmpFormation.MoveMembersIntoFormation(false, false, "combat"); this.StartTimer(200, 200); return false; }, "Timer": function(msg) { let target = this.order.data.target; let allowCapture = this.order.data.allowCapture; if (!this.CheckFormationTargetAttackRange(target)) { if (this.CanAttack(target) && this.CheckTargetVisible(target)) { this.SetNextState("COMBAT.APPROACHING"); return; } this.FinishOrder(); return; } }, "leave": function(msg) { this.StopTimer(); var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.SetRearrange(true); }, }, }, // Wait for individual members to finish "MEMBER": { "OrderTargetRenamed": function(msg) { // In general, don't react - we don't want to send spurious messages to members. // This looks odd for hunting however because we wait for all // entities to have clumped around the dead resource before proceeding // so explicitly handle this case. if (this.order && this.order.data && this.order.data.hunting && this.order.data.target == msg.data.newentity && this.orderQueue.length > 1) this.FinishOrder(); }, "enter": function(msg) { // Don't rearrange the formation, as that forces all units to stop // what they're doing. let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.SetRearrange(false); // While waiting on members, the formation is more like // a group of unit and does not have a well-defined position, // so move the controller out of the world to enforce that. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) cmpPosition.MoveOutOfWorld(); this.StartTimer(1000, 1000); return false; }, "Timer": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation && !cmpFormation.AreAllMembersWaiting()) return; if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) this.FindWalkAndFightTargets(); return; } return; }, "leave": function(msg) { this.StopTimer(); // Reform entirely as members might be all over the place now. let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.MoveMembersIntoFormation(true); // Update the held position so entities respond to orders. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { let pos = cmpPosition.GetPosition2D(); this.CallMemberFunction("SetHeldPosition", [pos.x, pos.y]); } }, }, }, // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { // Stop moving as soon as the formation disbands // Keep current rotation let facePointAfterMove = this.GetFacePointAfterMove(); this.SetFacePointAfterMove(false); this.StopMoving(); this.SetFacePointAfterMove(facePointAfterMove); // 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; } this.formationAnimationVariant = undefined; 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 (!this.WillMoveFromFoundation(msg.data.target)) return REJECT_ORDER; this.order.data.min = g_LeaveFoundationRange; this.SetNextState("WALKINGTOPOINT"); return ACCEPT_ORDER; }, "enter": function() { let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity); if (this.formationAnimationVariant) this.SetAnimationVariant(this.formationAnimationVariant); else this.SetDefaultAnimationVariant(); } return false; }, "leave": function() { this.SetDefaultAnimationVariant(); this.formationAnimationVariant = undefined; }, "IDLE": "INDIVIDUAL.IDLE", "CHEERING": "INDIVIDUAL.CHEERING", "WALKING": { "enter": function() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.MoveToFormationOffset(this.order.data.target, this.order.data.x, this.order.data.z); if (this.order.data.offsetsChanged) { let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) this.formationAnimationVariant = cmpFormation.GetFormationAnimationVariant(this.entity); } if (this.formationAnimationVariant) this.SetAnimationVariant(this.formationAnimationVariant); else if (this.order.data.variant) this.SetAnimationVariant(this.order.data.variant); else this.SetDefaultAnimationVariant(); return false; }, "leave": function() { // Don't use the logic from unitMotion, as SetInPosition // has already given us a custom rotation // (or we failed to move and thus don't care.) let facePointAfterMove = this.GetFacePointAfterMove(); this.SetFacePointAfterMove(false); this.StopMoving(); this.SetFacePointAfterMove(facePointAfterMove); }, // Occurs when the unit has reached its destination and the controller // is done moving. The controller is notified. "MovementUpdate": function(msg) { // When walking in formation, we'll only get notified in case of failure // if the formation controller has stopped walking. // Formations can start lagging a lot if many entities request short path // so prefer to finish order early than retry pathing. // (see https://code.wildfiregames.com/rP23806) // (if the message is likelyFailure of likelySuccess, we also want to stop). this.FinishOrder(); }, }, // Special case used by Order.LeaveFoundation "WALKINGTOPOINT": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function() { if (!this.CheckRange(this.order.data)) return; this.FinishOrder(); }, }, }, // States for entities not part of a formation: "INDIVIDUAL": { "Attacked": function(msg) { if (this.GetStance().targetAttackersAlways || !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.CanAttack(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.IsInjured()) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf)) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI); if (cmpBuildingAI && this.CanRepair(this.isGuardOf)) { this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } if (this.CheckTargetVisible(msg.data.attacker)) this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true }); 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); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); } } }, "IDLE": { "Order.Cheer": function() { // Do not cheer if there is no cheering time and we are not idle yet. if (!this.cheeringTime || !this.isIdle) return REJECT_ORDER; this.SetNextState("CHEERING"); return ACCEPT_ORDER; }, "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation this.SelectAnimation("idle"); // Idle is the default state. If units try, from the IDLE.enter sub-state, to // begin another order, and that order fails (calling FinishOrder), they might // end up in an infinite loop. To avoid this, all methods that could put the unit in // a new state are done on the next turn. // This wastes a turn but avoids infinite loops. // Further, the GUI and AI want to know when a unit is idle, // but sending this info in Idle.enter will send spurious messages. // Pick 100 to execute on the next turn in SP and MP. this.StartTimer(100); return false; }, "leave": function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) cmpRangeManager.DisableActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) cmpRangeManager.DisableActiveQuery(this.losHealRangeQuery); if (this.losAttackRangeQuery) cmpRangeManager.DisableActiveQuery(this.losAttackRangeQuery); this.StopTimer(); if (this.isIdle) { this.isIdle = false; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, "Attacked": function(msg) { if (this.isIdle && (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)) this.RespondToTargetedEntities([msg.data.attacker]); }, // On the range updates: // We check for idleness to prevent an entity to react only to newly seen entities // when receiving a Los*RangeUpdate on the same turn as the entity becomes idle // since this.FindNew*Targets is called in the timer. "LosRangeUpdate": function(msg) { if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToSightedEntities(msg.data.added); }, "LosHealRangeUpdate": function(msg) { if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToHealableEntities(msg.data.added); }, "LosAttackRangeUpdate": function(msg) { if (this.isIdle && msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { if (this.isGuardOf) { this.Guard(this.isGuardOf, false); return; } // 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; // 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 LosAttackRangeUpdate.) if (this.FindNewTargets()) return; if (this.FindSightedEnemies()) return; if (!this.isIdle) { // Move back to the held position if we drifted away. // (only if not a formation member). if (!this.IsFormationMember() && this.GetStance().respondHoldGround && this.heldPosition && !this.CheckPointRangeExplicit(this.heldPosition.x, this.heldPosition.z, 0, 10) && this.WalkToHeldPosition()) return; if (this.IsFormationMember()) { let cmpFormationAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (!cmpFormationAI || !cmpFormationAI.IsIdle()) return; } this.isIdle = true; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } // Go linger first to prevent all roaming entities // to move all at the same time on map init. if (this.template.RoamDistance) this.SetNextState("LINGERING"); }, "ROAMING": { "enter": function() { this.SetFacePointAfterMove(false); this.MoveRandomly(+this.template.RoamDistance); this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); this.SetFacePointAfterMove(true); }, "Timer": function(msg) { this.SetNextState("LINGERING"); }, "MovementUpdate": function() { this.MoveRandomly(+this.template.RoamDistance); }, }, "LINGERING": { "enter": function() { // ToDo: rename animations? this.SelectAnimation("feeding"); this.StartTimer(randIntInclusive(+this.template.FeedTimeMin, +this.template.FeedTimeMax)); return false; }, "leave": function() { this.ResetAnimation(); this.StopTimer(); }, "Timer": function(msg) { this.SetNextState("ROAMING"); }, }, }, "WALKING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { // If it looks like the path is failing, and we are close enough stop anyways. // This avoids pathing for an unreachable goal and reduces lag considerably. if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.FinishOrder(); }, }, "WALKINGANDFIGHTING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); return false; }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "leave": function(msg) { this.StopMoving(); this.StopTimer(); this.SetDefaultAnimationVariant(); }, "MovementUpdate": function(msg) { // If it looks like the path is failing, and we are close enough stop anyways. // This avoids pathing for an unreachable goal and reduces lag considerably. if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.FinishOrder(); }, }, "PATROL": { "enter": function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { this.FinishOrder(); return true; } // Memorize the origin position in case that we want to go back. if (!this.patrolStartPosOrder) { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; } this.SetAnimationVariant("combat"); return false; }, "leave": function() { delete this.patrolStartPosOrder; this.SetDefaultAnimationVariant(); }, "PATROLLING": { "enter": function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld() || !this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.StartTimer(0, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !msg.likelySuccess && !this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange)) return; if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); this.PushOrder(this.order.type, this.order.data); this.SetNextState("CHECKINGWAYPOINT"); }, }, "CHECKINGWAYPOINT": { "enter": function() { this.StartTimer(0, 1000); this.stopSurveying = 0; // TODO: pick a proper animation return false; }, "leave": function() { this.StopTimer(); delete this.stopSurveying; }, "Timer": function(msg) { if (this.stopSurveying >= +this.template.PatrolWaitTime) { this.FinishOrder(); return; } this.FindWalkAndFightTargets(); ++this.stopSurveying; } } }, "GUARD": { "RemoveGuard": function() { this.FinishOrder(); }, "ESCORTING": { "enter": function() { if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) { this.FinishOrder(); return true; } // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, "Timer": function(msg) { if (!this.ShouldGuard(this.isGuardOf)) { this.FinishOrder(); return; } let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, false)) this.TryMatchTargetSpeed(this.isGuardOf, false); this.SetHeldPositionOnEntity(this.isGuardOf); }, "leave": function(msg) { this.StopMoving(); this.ResetSpeedMultiplier(); this.StopTimer(); this.SetDefaultAnimationVariant(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("GUARDING"); }, }, "GUARDING": { "enter": function() { this.StartTimer(1000, 1000); this.SetHeldPositionOnEntity(this.entity); this.SetAnimationVariant("combat"); this.FaceTowardsTarget(this.order.data.target); return false; }, "LosAttackRangeUpdate": function(msg) { if (this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { if (!this.ShouldGuard(this.isGuardOf)) { this.FinishOrder(); return; } // TODO: find out what to do if we cannot move. if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange) && this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("ESCORTING"); else { this.FaceTowardsTarget(this.order.data.target); var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health); if (cmpHealth && cmpHealth.IsInjured()) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf)) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); } } }, "leave": function(msg) { this.StopTimer(); this.SetDefaultAnimationVariant(); }, }, }, "FLEEING": { "enter": function() { // We use the distance between the entities to account for ranged attacks this.order.data.distanceToFlee = PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); // Use unit motion directly to ignore the visibility check. TODO: change this if we add LOS to fauna. if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) || !cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1)) { this.FinishOrder(); return true; } this.PlaySound("panic"); this.SetSpeedMultiplier(this.GetRunMultiplier()); return false; }, "OrderTargetRenamed": function(msg) { // To avoid replaying the panic sound, handle this explicitly. let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) || !cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1)) this.FinishOrder(); }, "Attacked": function(msg) { if (msg.data.attacker == this.order.data.target) return; let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); if (cmpObstructionManager.DistanceToTarget(this.entity, msg.data.target) > cmpObstructionManager.DistanceToTarget(this.entity, this.order.data.target)) return; if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force) this.RespondToTargetedEntities([msg.data.attacker]); }, "leave": function() { this.ResetSpeedMultiplier(); this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1)) this.FinishOrder(); }, }, "COMBAT": { "Order.LeaveFoundation": function(msg) { // Ignore the order as we're busy. return REJECT_ORDER; }, "Attacked": function(msg) { // If we're already in combat mode, ignore anyone else who's attacking us // unless it's a melee attack since they may be blocking our way to the target if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force)) this.RespondToTargetedEntities([msg.data.attacker]); }, "leave": function() { if (!this.formationAnimationVariant) this.SetDefaultAnimationVariant(); }, "APPROACHING": { "enter": function() { if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.FinishOrder(); return true; } if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); this.StartTimer(1000, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.FinishOrder(); if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } else { this.RememberTargetPosition(); if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); } }, "MovementUpdate": function(msg) { if (msg.likelyFailure) { // This also handles hunting. if (this.orderQueue.length > 1) { this.FinishOrder(); return; } else if (!this.order.data.force || !this.order.data.lastPos) { this.SetNextState("COMBAT.FINDINGNEWTARGET"); return; } // If the order was forced, try moving to the target position, // under the assumption that this is desirable if the target // was somewhat far away - we'll likely end up closer to where // the player hoped we would. let lastPos = this.order.data.lastPos; this.PushOrder("WalkAndFight", { "x": lastPos.x, "z": lastPos.z, "force": false, // Force to true - otherwise structures might be attacked instead of captured, // which is generally not expected (attacking units usually has allowCapture false). "allowCapture": true }); return; } if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { if (this.CanUnpack()) { this.PushOrderFront("Unpack", { "force": true }); return; } this.SetNextState("ATTACKING"); } else if (msg.likelySuccess) // Try moving again, // attack range uses a height-related formula and our actual max range might have changed. if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) this.FinishOrder(); }, }, "ATTACKING": { "enter": function() { let target = this.order.data.target; let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) { this.order.data.formationTarget = target; target = cmpFormation.GetClosestMember(this.entity); this.order.data.target = target; } this.shouldCheer = false; if (!this.CanAttack(target)) { this.SetNextState("COMBAT.FINDINGNEWTARGET"); return true; } if (!this.CheckTargetAttackRange(target, this.order.data.attackType)) { if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return true; } this.SetNextState("COMBAT.APPROACHING"); return true; } let 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. let prepare = this.attackTimers.prepare; if (this.lastAttacked) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); this.oldAttackType = this.order.data.attackType; this.SelectAnimation("attack_" + this.order.data.attackType.toLowerCase()); 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; this.FaceTowardsTarget(this.order.data.target); let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) { cmpBuildingAI.SetUnitAITarget(this.order.data.target); return false; } let cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); // Units with no cheering time do not cheer. this.shouldCheer = cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()) && this.cheeringTime > 0; return false; }, "leave": function() { let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); this.ResetAnimation(); }, "Timer": function(msg) { let target = this.order.data.target; let attackType = this.order.data.attackType; if (!this.CanAttack(target)) { this.SetNextState("COMBAT.FINDINGNEWTARGET"); return; } this.RememberTargetPosition(); if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastAttacked = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); // BuildingAI has it's own attack-routine let cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (!cmpBuildingAI) { let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); cmpAttack.PerformAttack(attackType, target); } // PerformAttack might have triggered messages that moved us to another state. // (use 'ends with' to handle formation members copying our state). if (!this.GetCurrentState().endsWith("COMBAT.ATTACKING")) return; // Check we can still reach the target for the next attack if (this.CheckTargetAttackRange(target, attackType)) { if (this.resyncAnimation) { this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); this.resyncAnimation = false; } return; } if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return; } this.SetNextState("COMBAT.CHASING"); return; } this.SetNextState("FINDINGNEWTARGET"); }, // TODO: respond to target deaths immediately, rather than waiting // until the next Timer event "Attacked": function(msg) { if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture") this.RespondToTargetedEntities([msg.data.attacker]); }, }, "FINDINGNEWTARGET": { "Order.Cheer": function() { if (!this.cheeringTime) return REJECT_ORDER; this.SetNextState("CHEERING"); return ACCEPT_ORDER; }, "enter": function() { // Try to find the formation the target was a part of. let cmpFormation = Engine.QueryInterface(this.order.data.target, IID_Formation); if (!cmpFormation) cmpFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation); // If the target is a formation, pick closest member. if (cmpFormation) { let filter = (t) => this.CanAttack(t); this.order.data.formationTarget = this.order.data.target; let target = cmpFormation.GetClosestMember(this.entity, filter); this.order.data.target = target; this.SetNextState("COMBAT.ATTACKING"); return true; } // 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 enemies around before moving again. if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) this.FindWalkAndFightTargets(); return true; } if (this.FindNewTargets()) return true; if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); if (this.shouldCheer) { this.Cheer(); this.CallPlayerOwnedEntitiesFunctionInRange("Cheer", [], this.notifyToCheerInRange); } return true; }, }, "CHASING": { "Order.MoveToChasingPoint": function(msg) { if (this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, msg.data.max)) return REJECT_ORDER; this.order.data.relaxed = true; this.StopTimer(); this.SetNextState("MOVINGTOPOINT"); return ACCEPT_ORDER; }, "enter": function() { if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.FinishOrder(); return true; } if (!this.formationAnimationVariant) this.SetAnimationVariant("combat"); var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsFleeing()) this.SetSpeedMultiplier(this.GetRunMultiplier()); this.StartTimer(1000, 1000); return false; }, "leave": function() { this.ResetSpeedMultiplier(); this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.FinishOrder(); if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } else { this.RememberTargetPosition(); if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); } }, "MovementUpdate": function(msg) { if (msg.likelyFailure) { // This also handles hunting. if (this.orderQueue.length > 1) { this.FinishOrder(); return; } else if (!this.order.data.force) { this.SetNextState("COMBAT.FINDINGNEWTARGET"); return; } else if (this.order.data.lastPos) { let lastPos = this.order.data.lastPos; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.PushOrder("MoveToChasingPoint", { "x": lastPos.x, "z": lastPos.z, "max": cmpAttack.GetRange(this.order.data.attackType).max, "force": true }); return; } } if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { if (this.CanUnpack()) { this.PushOrderFront("Unpack", { "force": true }); return; } this.SetNextState("ATTACKING"); } else if (msg.likelySuccess) // Try moving again, // attack range uses a height-related formula and our actual max range might have changed. if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) this.FinishOrder(); }, "MOVINGTOPOINT": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { // If it looks like the path is failing, and we are close enough from wanted range // stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably. if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.order.data.max + this.DefaultRelaxedMaxRange) || !msg.obstructed && this.CheckRange(this.order.data)) this.FinishOrder(); }, }, }, }, "GATHER": { "leave": function() { // Show the carried resource, if we've gathered anything. this.SetDefaultAnimationVariant(); }, "APPROACHING": { "enter": function() { this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); let cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage); if ((!cmpMirage || !cmpMirage.Mirages(IID_ResourceSupply)) && (!cmpSupply || !cmpSupply.AddGatherer(this.entity)) || !this.MoveTo(this.order.data, IID_ResourceGatherer)) { // If the target's last known position is in FOW, try going there // and hope that we might find it then. let lastPos = this.order.data.lastPos; if (this.gatheringTarget != INVALID_ENTITY && lastPos && !this.CheckPositionVisible(lastPos.x, lastPos.z)) { this.PushOrderFront("Walk", { "x": lastPos.x, "z": lastPos.z, "force": this.order.data.force }); return true; } this.SetNextState("FINDINGNEWTARGET"); return true; } this.SetAnimationVariant("approach_" + this.order.data.type.specific); let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) cmpResourceGatherer.AddToPlayerCounter(this.order.data.type.generic); return false; }, "MovementUpdate": function(msg) { // The GATHERING timer will handle finding a valid resource. if (msg.likelyFailure) this.SetNextState("FINDINGNEWTARGET"); else if (this.CheckRange(this.order.data, IID_ResourceGatherer)) this.SetNextState("GATHERING"); }, "leave": function() { this.StopMoving(); if (!this.gatheringTarget) return; // don't use ownership because this is called after a conversion/resignation // and the ownership would be invalid then. let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) cmpResourceGatherer.RemoveFromPlayerCounter(); delete this.gatheringTarget; }, }, // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } this.SetAnimationVariant("approach_" + this.order.data.type.specific); return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { // If we failed, the GATHERING timer will handle finding a valid resource. if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.SetNextState("GATHERING"); }, }, "GATHERING": { "enter": function() { this.gatheringTarget = this.order.data.target || INVALID_ENTITY; // deleted in "leave". // Check if the resource is full. // Will only be added if we're not already in. let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); - if (!cmpSupply || !cmpSupply.AddGatherer(this.entity)) + if (!cmpSupply || !cmpSupply.AddActiveGatherer(this.entity)) { this.SetNextState("FINDINGNEWTARGET"); return true; } // 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. let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); let 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)) { this.SetNextState("FINDINGNEWTARGET"); return true; } // 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. let offset = 1000 / rate; this.StartTimer(offset, offset); // 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)) { this.SetDefaultAnimationVariant(); this.FaceTowardsTarget(this.order.data.target); this.SelectAnimation("gather_" + this.order.data.type.specific); cmpResourceGatherer.AddToPlayerCounter(this.order.data.type.generic); } 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. let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) cmpResourceGatherer.RemoveFromPlayerCounter(); delete this.gatheringTarget; this.ResetAnimation(); }, "Timer": function(msg) { let resourceTemplate = this.order.data.template; let resourceType = this.order.data.type; // TODO: we are leaking information here - if the target died in FOW, we'll know it's dead // straight away. // Seems one would have to listen to ownership changed messages to make it work correctly // but that's likely prohibitively expansive performance wise. let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); // If we can't gather from the target, find a new one. if (!cmpSupply || !cmpSupply.IsAvailableTo(this.entity) || !this.CanGather(this.gatheringTarget)) { this.SetNextState("FINDINGNEWTARGET"); return; } if (!this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { // Try to follow the target if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer)) this.SetNextState("APPROACHING"); // Our target is no longer visible - go to its last known position first // and then hopefully it will become visible. else if (!this.CheckTargetVisible(this.gatheringTarget) && this.order.data.lastPos) this.PushOrderFront("Walk", { "x": this.order.data.lastPos.x, "z": this.order.data.lastPos.z, "force": this.order.data.force }); else this.SetNextState("FINDINGNEWTARGET"); return; } // Gather the resources: let 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(); this.FaceTowardsTarget(this.order.data.target); // Collect from the target let status = cmpResourceGatherer.PerformGather(this.gatheringTarget); // If we've collected as many resources as possible, // return to the nearest dropsite if (status.filled) { let nearestDropsite = this.FindNearestDropsite(resourceType.generic); if (nearestDropsite) { // (Keep this Gather order on the stack so we'll // continue gathering after returning) // However mark our target as invalid if it's exhausted, so we don't waste time // trying to gather from it. if (status.exhausted) this.order.data.target = INVALID_ENTITY; this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false }); return; } // Oh no, couldn't find any drop sites. Give up on gathering. this.FinishOrder(); return; } if (status.exhausted) this.SetNextState("FINDINGNEWTARGET"); }, }, "FINDINGNEWTARGET": { "enter": function() { let previousTarget = this.order.data.target; let resourceTemplate = this.order.data.template; let resourceType = this.order.data.type; // Give up on this order and try our next queued order // but first check what is our next order and, if needed, insert a returnResource order let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer.IsCarrying(resourceType.generic) && this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" && (this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic)) { let nearestDropsite = this.FindNearestDropsite(resourceType.generic); if (nearestDropsite) this.orderQueue.splice(1, 0, { "type": "ReturnResource", "data": { "target": nearestDropsite, "force": false } }); } // Must go before FinishOrder or this.order will be undefined. let initPos = this.order.data.initPos; if (this.FinishOrder()) return true; // No remaining orders - pick a useful default behaviour // Give up if we're not in the world right now. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return true; // If we have no known initial position of our target, look around our own position // as a fallback. if (!initPos) { let pos = cmpPosition.GetPosition(); initPos = { 'x': pos.X, 'z': pos.Z }; } // Try to find a new resource of the same specific type near the initial resource position: // Also don't switch to a different type of huntable animal let nearbyResource = this.FindNearbyResource(new Vector2D(initPos.x, initPos.z), (ent, type, template) => { if (previousTarget == ent) return false; if (type.generic == "treasure" && resourceType.generic == "treasure") return true; return type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template); }); if (nearbyResource) { this.PerformGather(nearbyResource, false, false); return true; } // Failing that, try to move there and se if we are more lucky: maybe there are resources in FOW. // Only move if we are some distance away (TODO: pick the distance better?) if (!this.CheckPointRangeExplicit(initPos.x, initPos.z, 0, 10)) { this.GatherNearPosition(initPos.x, initPos.z, resourceType, resourceTemplate); return true; } // 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 let nearestDropsite = this.FindNearestDropsite(resourceType.generic); if (nearestDropsite) { this.PushOrderFront("ReturnResource", { "target": nearestDropsite, "force": false }); return true; } // No dropsites - just give up. return true; }, }, }, "HEAL": { "Attacked": function(msg) { if (!this.GetStance().respondStandGround && !this.order.data.force) this.Flee(msg.data.attacker, false); }, "APPROACHING": { "enter": function() { if (this.CheckRange(this.order.data, IID_Heal)) { this.SetNextState("HEALING"); return true; } if (!this.MoveTo(this.order.data, IID_Heal)) { this.SetNextState("FINDINGNEWTARGET"); return true; } this.StartTimer(1000, 1000); return false; }, "leave": function() { this.StopMoving(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null)) this.SetNextState("FINDINGNEWTARGET"); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckRange(this.order.data, IID_Heal)) this.SetNextState("HEALING"); }, }, "HEALING": { "enter": function() { if (!this.CheckRange(this.order.data, IID_Heal)) { this.SetNextState("APPROACHING"); return true; } if (!this.TargetIsAlive(this.order.data.target) || !this.CanHeal(this.order.data.target)) { this.SetNextState("FINDINGNEWTARGET"); return true; } let 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"); 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; this.FaceTowardsTarget(this.order.data.target); return false; }, "leave": function() { this.ResetAnimation(); this.StopTimer(); }, "Timer": function(msg) { let target = this.order.data.target; if (!this.TargetIsAlive(target) || !this.CanHeal(target)) { this.SetNextState("FINDINGNEWTARGET"); return; } if (!this.CheckRange(this.order.data, IID_Heal)) { if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return; } this.SetNextState("HEAL.APPROACHING"); } else this.SetNextState("FINDINGNEWTARGET"); return; } let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastHealed = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); cmpHeal.PerformHeal(target); if (this.resyncAnimation) { this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat); this.resyncAnimation = false; } }, }, "FINDINGNEWTARGET": { "enter": function() { // If we have another order, do that instead. if (this.FinishOrder()) return true; if (this.FindNewHealTargets()) return true; if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); // We quit this state right away. return true; }, }, }, // Returning to dropsite "RETURNRESOURCE": { "APPROACHING": { "enter": function() { if (!this.MoveTo(this.order.data, IID_ResourceGatherer)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { // Check the dropsite is in range and we can return our resource there // (we didn't get stopped before reaching it) let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer)) { cmpResourceGatherer.CommitResources(this.order.data.target); // Stop showing the carried resource animation. this.SetDefaultAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order. this.FinishOrder(); return; } if (msg.obstructed) return; // If we are here: we are in range but not carrying the right resources (or resources at all), // the dropsite was destroyed, or we couldn't reach it, or ownership changed. // Look for a new one. let genericType = cmpResourceGatherer.GetMainCarryingType(); let 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 }, "APPROACHINGMARKET": { "enter": function() { if (!this.MoveToMarket(this.order.data.target)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !this.CheckRange(this.order.data.nextTarget, IID_Trader)) return; if (this.waypoints && this.waypoints.length) { if (!this.MoveToMarket(this.order.data.target)) this.StopTrading(); } else this.PerformTradeAndMoveToNextMarket(this.order.data.target); }, }, "TradingCanceled": function(msg) { if (msg.market != this.order.data.target) return; let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); let otherMarket = cmpTrader && cmpTrader.GetFirstMarket(); this.StopTrading(); if (otherMarket) this.WalkToTarget(otherMarket); }, }, "REPAIR": { "APPROACHING": { "enter": function() { if (!this.MoveTo(this.order.data, IID_Builder)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.likelySuccess) 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; // Needed to remove the entity from the builder list when leaving this state. this.repairTarget = this.order.data.target; if (!this.CanRepair(this.repairTarget)) { this.FinishOrder(); return true; } if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) { this.SetNextState("APPROACHING"); return true; } let 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.ConstructionFinished({ "entity": this.repairTarget, "newentity": this.repairTarget }); return true; } let cmpBuilderList = QueryBuilderListInterface(this.repairTarget); if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); this.FaceTowardsTarget(this.repairTarget); this.SelectAnimation("build"); this.StartTimer(1000, 1000); return false; }, "leave": function() { let cmpBuilderList = QueryBuilderListInterface(this.repairTarget); if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.repairTarget; this.StopTimer(); this.ResetAnimation(); }, "Timer": function(msg) { if (!this.CanRepair(this.repairTarget)) { this.FinishOrder(); return; } this.FaceTowardsTarget(this.repairTarget); let 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.CheckTargetRange(this.repairTarget, IID_Builder)) this.SetNextState("APPROACHING"); }, }, "ConstructionFinished": function(msg) { if (msg.data.entity != this.order.data.target) return; // ignore other buildings let 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). let oldState = this.GetCurrentState(); let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); let canReturnResources = this.CanReturnResource(msg.data.newentity, true, cmpResourceGatherer); if (this.CheckTargetRange(msg.data.newentity, IID_Builder) && canReturnResources) { cmpResourceGatherer.CommitResources(msg.data.newentity); this.SetDefaultAnimationVariant(); } // Switch to the next order (if any) if (this.FinishOrder()) { if (canReturnResources) { // We aren't in range, but we can still return resources there: always do so. this.SetDefaultAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } return; } if (canReturnResources) { // We aren't in range, but we can still return resources there: always do so. this.SetDefaultAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } // 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 received // 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, cmpResourceGatherer)) { let cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); let 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! let nearby = this.FindNearbyResource(this.TargetPosOrEntPos(msg.data.newentity), (ent, type, template) => types.indexOf(type.generic) != -1); if (nearby) { this.PerformGather(nearby, true, false); return; } } let nearbyFoundation = this.FindNearbyFoundation(this.TargetPosOrEntPos(msg.data.newentity)); 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.endsWith("REPAIR.APPROACHING")) // We're already walking to the given point, so add this as a order. this.WalkToTarget(msg.data.newentity, true); }, }, "GARRISON": { "leave": function() { if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } return false; }, "APPROACHING": { "enter": function() { if (!this.MoveToGarrisonRange(this.order.data.target)) { this.FinishOrder(); return true; } if (this.pickup) Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); let 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 }); } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.likelySuccess) this.SetNextState("GARRISONED"); }, }, "GARRISONED": { "enter": function() { let target = this.order.data.target; if (!target) { this.FinishOrder(); return true; } // Called when autogarrisoning. if (this.isGarrisoned) { this.SetImmobile(true); if (this.IsTurret()) { this.SetNextState("IDLE"); return true; } return false; } if (this.CanGarrison(target)) if (this.CheckGarrisonRange(target)) { var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (cmpGarrisonHolder.Garrison(this.entity)) { this.isGarrisoned = true; this.SetImmobile(true); if (this.formationController) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { var rearrange = cmpFormation.rearrange; cmpFormation.SetRearrange(false); cmpFormation.RemoveMembers([this.entity]); cmpFormation.SetRearrange(rearrange); } } let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (this.CanReturnResource(target, true, cmpResourceGatherer)) { cmpResourceGatherer.CommitResources(target); this.SetDefaultAnimationVariant(); } if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } if (this.IsTurret()) { this.SetNextState("IDLE"); return true; } 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) && !cmpUnitAI.IsIdle())) { this.FinishOrder(); return true; } } this.SetNextState("APPROACHING"); return true; } this.FinishOrder(); return true; }, "leave": function() { } }, }, "CHEERING": { "enter": function() { this.SelectAnimation("promotion"); this.StartTimer(this.cheeringTime); return false; }, "leave": function() { // PushOrderFront preserves the cheering order, // which can lead to very bad behaviour, so make // sure to delete any queued ones. for (let i = 1; i < this.orderQueue.length; ++i) if (this.orderQueue[i].type == "Cheer") this.orderQueue.splice(i--, 1); this.StopTimer(); this.ResetAnimation(); }, "LosRangeUpdate": function(msg) { if (msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToSightedEntities(msg.data.added); }, "LosHealRangeUpdate": function(msg) { if (msg && msg.data && msg.data.added && msg.data.added.length) this.RespondToHealableEntities(msg.data.added); }, "LosAttackRangeUpdate": function(msg) { if (msg && msg.data && msg.data.added && msg.data.added.length && this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { this.FinishOrder(); }, }, "PACKING": { "enter": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Pack(); return false; }, "Order.CancelPack": function(msg) { this.FinishOrder(); return ACCEPT_ORDER; }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.CancelPack(); }, "Attacked": function(msg) { // Ignore attacks while packing }, }, "UNPACKING": { "enter": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Unpack(); return false; }, "Order.CancelUnpack": function(msg) { this.FinishOrder(); return ACCEPT_ORDER; }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.CancelPack(); }, "Attacked": function(msg) { // Ignore attacks while unpacking }, }, "PICKUP": { "APPROACHING": { "enter": function() { if (!this.MoveTo(this.order.data)) { this.FinishOrder(); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (msg.likelyFailure || msg.likelySuccess) this.SetNextState("LOADING"); }, "PickupCanceled": function() { this.FinishOrder(); }, }, "LOADING": { "enter": function() { let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) { this.FinishOrder(); return true; } return false; }, "PickupCanceled": function() { this.FinishOrder(); }, }, }, }, }; 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.isImmobile = false; // True if the unit is currently unable to move (garrisoned,...) this.heldPosition = undefined; // Queue of remembered works this.workOrders = []; this.isGuardOf = undefined; // For preventing increased action rate due to Stop orders or target death. this.lastAttacked = undefined; this.lastHealed = undefined; this.formationAnimationVariant = undefined; this.cheeringTime = +(this.template.CheeringTime || 0); this.SetStance(this.template.DefaultStance); }; UnitAI.prototype.IsTurret = function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY; }; UnitAI.prototype.IsFormationController = function() { return (this.template.FormationController == "true"); }; UnitAI.prototype.IsFormationMember = function() { return (this.formationController != INVALID_ENTITY); }; /** * For now, entities with a RoamDistance are animals. */ UnitAI.prototype.IsAnimal = function() { return !!this.template.RoamDistance; }; /** * ToDo: Make this not needed by fixing gaia * range queries in BuildingAI and UnitAI regarding * animals and other gaia entities. */ UnitAI.prototype.IsDangerousAnimal = function() { return this.IsAnimal() && this.GetStance().targetVisibleEnemies && !!Engine.QueryInterface(this.entity, IID_Attack); }; UnitAI.prototype.IsHealer = function() { return Engine.QueryInterface(this.entity, IID_Heal); }; UnitAI.prototype.IsIdle = function() { return this.isIdle; }; /** * Used by formation controllers to toggle the idleness of their members. */ UnitAI.prototype.ResetIdle = function() { let shouldBeIdle = this.GetCurrentState().endsWith(".IDLE"); if (this.isIdle == shouldBeIdle) return; this.isIdle = shouldBeIdle; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); }; UnitAI.prototype.IsGarrisoned = function() { return this.isGarrisoned; }; UnitAI.prototype.SetGarrisoned = function() { this.isGarrisoned = true; }; UnitAI.prototype.GetGarrisonHolder = function() { if (!this.isGarrisoned) return INVALID_ENTITY; let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable); return cmpGarrisonable ? cmpGarrisonable.HolderID() : INVALID_ENTITY; }; UnitAI.prototype.ShouldRespondToEndOfAlert = function() { return !this.orderQueue.length || this.orderQueue[0].type == "Garrison"; }; UnitAI.prototype.SetImmobile = function(immobile) { this.isImmobile = immobile; Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, { "entity": this.entity, "ableToMove": this.AbleToMove() }); }; /** * @param cmpUnitMotion - optionally pass unitMotion to avoid querying it here * @returns true if the entity can move, i.e. has UnitMotion and isn't immobile. */ UnitAI.prototype.AbleToMove = function(cmpUnitMotion) { if (this.isImmobile || this.IsTurret()) return false; if (!cmpUnitMotion) cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return !!cmpUnitMotion; }; 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 the current order is WalkAndFight or Patrol. */ UnitAI.prototype.IsWalkingAndFighting = function() { if (this.IsFormationMember()) return false; return this.orderQueue.length > 0 && (this.orderQueue[0].type == "WalkAndFight" || this.orderQueue[0].type == "Patrol"); }; UnitAI.prototype.OnCreate = function() { if (this.IsFormationController()) this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); else this.UnitFsm.Init(this, "INDIVIDUAL.IDLE"); this.isIdle = true; }; UnitAI.prototype.OnDiplomacyChanged = function(msg) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) this.SetupRangeQueries(); if (this.isGuardOf && !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf)) this.RemoveGuard(); }; UnitAI.prototype.OnOwnershipChanged = function(msg) { this.SetupRangeQueries(); if (this.isGuardOf && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))) this.RemoveGuard(); // If the unit isn't being created or dying, reset stance and clear orders if (msg.to != INVALID_PLAYER && msg.from != INVALID_PLAYER) { // Switch to a virgin state to let states execute their leave handlers. // Except if garrisoned or (un)packing, in which case we only clear the order queue. if (this.isGarrisoned || this.IsPacking()) { this.orderQueue.length = Math.min(this.orderQueue.length, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); } else { let index = this.GetCurrentState().indexOf("."); if (index != -1) this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index)); this.Stop(false); } this.workOrders = []; let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader) cmpTrader.StopTrading(); this.SetStance(this.template.DefaultStance); if (this.IsTurret()) this.SetTurretStance(); } }; UnitAI.prototype.OnDestroy = function() { // Switch to an empty state to let states execute their leave handlers. this.UnitFsm.SwitchToNextState(this, ""); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery); if (this.losAttackRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery); }; UnitAI.prototype.OnVisionRangeChanged = function(msg) { if (this.entity == msg.entity) this.SetupRangeQueries(); }; UnitAI.prototype.HasPickupOrder = function(entity) { return this.orderQueue.some(order => order.type == "PickupUnit" && order.data.target == entity); }; UnitAI.prototype.OnPickupRequested = function(msg) { if (this.HasPickupOrder(msg.entity)) return; this.PushOrderAfterForced("PickupUnit", { "target": msg.entity }); }; UnitAI.prototype.OnPickupCanceled = function(msg) { for (let i = 0; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity) continue; if (i == 0) this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg}); else this.orderQueue.splice(i, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); break; } }; /** * Wrapper function that sets up the LOS, healer and attack range queries. * This should be called whenever our ownership changes. */ UnitAI.prototype.SetupRangeQueries = function() { if (this.GetStance().respondFleeOnSight) this.SetupLOSRangeQuery(); if (this.IsHealer()) this.SetupHealRangeQuery(); if (Engine.QueryInterface(this.entity, IID_Attack)) this.SetupAttackRangeQuery(); }; UnitAI.prototype.UpdateRangeQueries = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) this.SetupLOSRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery)); if (this.losHealRangeQuery) this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery)); if (this.losAttackRangeQuery) this.SetupAttackRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losAttackRangeQuery)); }; /** * Set up a range query for all enemy units within LOS range. * @param {boolean} enable - Optional parameter whether to enable the query. */ UnitAI.prototype.SetupLOSRangeQuery = function(enable = true) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losRangeQuery); this.losRangeQuery = undefined; } let cmpPlayer = QueryOwnerInterface(this.entity); // If we are being destructed (owner == -1), creating a range query is pointless. if (!cmpPlayer) return; let players = cmpPlayer.GetEnemies(); if (!players.length) return; let range = this.GetQueryRange(IID_Vision); // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Identity, cmpRangeManager.GetEntityFlagMask("normal"), false); if (enable) cmpRangeManager.EnableActiveQuery(this.losRangeQuery); }; /** * Set up a range query for all own or ally units within LOS range * which can be healed. * @param {boolean} enable - Optional parameter whether to enable the query. */ UnitAI.prototype.SetupHealRangeQuery = function(enable = true) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losHealRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery); this.losHealRangeQuery = undefined; } let cmpPlayer = QueryOwnerInterface(this.entity); // If we are being destructed (owner == -1), creating a range query is pointless. if (!cmpPlayer) return; let players = cmpPlayer.GetAllies(); let range = this.GetQueryRange(IID_Heal); // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, cmpRangeManager.GetEntityFlagMask("injured"), false); if (enable) cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery); }; /** * Set up a range query for all enemy and gaia units within range * which can be attacked. * @param {boolean} enable - Optional parameter whether to enable the query. */ UnitAI.prototype.SetupAttackRangeQuery = function(enable = true) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losAttackRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery); this.losAttackRangeQuery = undefined; } let cmpPlayer = QueryOwnerInterface(this.entity); // If we are being destructed (owner == -1), creating a range query is pointless. if (!cmpPlayer) return; // TODO: How to handle neutral players - Special query to attack military only? let players = cmpPlayer.GetEnemies(); if (!players.length) return; let range = this.GetQueryRange(IID_Attack); // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"), false); if (enable) cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery); }; //// FSM linkage functions //// // Setting the next state to the current state will leave/re-enter the top-most substate. UnitAI.prototype.SetNextState = function(state) { this.UnitFsm.SetNextState(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 or if the unit is not * inWorld and not garrisoned (thus usually waiting to be destroyed). */ UnitAI.prototype.FinishOrder = function() { if (!this.orderQueue.length) { let stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let 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]; let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (this.orderQueue.length && (this.IsGarrisoned() || this.IsFormationController() || cmpPosition && cmpPosition.IsInWorld())) { let 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 === REJECT_ORDER) return this.FinishOrder(); // Otherwise we've successfully processed a new order return true; } this.orderQueue = []; this.order = undefined; // Switch to IDLE as a default state. this.SetNextState("IDLE"); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // Check if there are queued formation orders if (this.IsFormationMember()) { this.SetNextState("FORMATIONMEMBER.IDLE"); let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { // Inform the formation controller that we finished this task let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); cmpFormation.SetWaitingOnController(this.entity); // 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 (this.orderQueue.length == 1) { this.order = order; let ret = this.UnitFsm.ProcessMessage(this, { "type": "Order."+this.order.type, "data": this.order.data } ); // If the order was rejected then immediately take it off // and process the remaining queue if (ret === REJECT_ORDER) this.FinishOrder(); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * Add an order onto the front of the queue, * and execute it immediately. */ UnitAI.prototype.PushOrderFront = function(type, data, ignorePacking = false) { var order = { "type": type, "data": data }; // If current order is packing/unpacking then add new order after it. if (!ignorePacking && this.order && this.IsPacking()) { var packingOrder = this.orderQueue.shift(); this.orderQueue.unshift(packingOrder, order); } else { this.orderQueue.unshift(order); this.order = order; let ret = this.UnitFsm.ProcessMessage(this, { "type": "Order."+this.order.type, "data": this.order.data } ); // 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 === REJECT_ORDER) { this.orderQueue.shift(); this.order = this.orderQueue[0]; } } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * 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 (let 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}); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); return; } this.PushOrder(type, data); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * For a unit that is packing and trying to attack something, * either cancel packing or continue with packing, as appropriate. * Precondition: if the unit is packing/unpacking, then orderQueue * should have the Attack order at index 0, * and the Pack/Unpack order at index 1. * This precondition holds because if we are packing while processing "Order.Attack", * then we must have come from ReplaceOrder, which guarantees it. * * @param {boolean} requirePacked - true if the unit needs to be packed to continue attacking, * false if it needs to be unpacked. * @return {boolean} true if the unit can attack now, false if it must continue packing (or unpacking) first. */ UnitAI.prototype.EnsureCorrectPackStateForAttack = function(requirePacked) { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (!cmpPack || !cmpPack.IsPacking() || this.orderQueue.length != 2 || this.orderQueue[0].type != "Attack" || this.orderQueue[1].type != "Pack" && this.orderQueue[1].type != "Unpack") return true; if (cmpPack.IsPacked() == requirePacked) { // The unit is already in the packed/unpacked state we want. // Delete the packing order. this.orderQueue.splice(1, 1); cmpPack.CancelPack(); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // Continue with the attack order. return true; } // Move the attack order behind the unpacking order, to continue unpacking. let tmp = this.orderQueue[0]; this.orderQueue[0] = this.orderQueue[1]; this.orderQueue[1] = tmp; Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); return false; }; UnitAI.prototype.WillMoveFromFoundation = function(target, checkPacking = true) { let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (!IsOwnedByAllyOfEntity(this.entity, target) && cmpUnitAI && !cmpUnitAI.IsAnimal() && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() || checkPacking && this.IsPacking() || this.CanPack() || !this.AbleToMove()) return false; return !this.CheckTargetRangeExplicit(target, g_LeaveFoundationRange, -1); }; 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); } let garrisonHolder = this.IsGarrisoned() && type != "Ungarrison" ? this.GetGarrisonHolder() : null; // Do not replace packing/unpacking unless it is cancel order. // TODO: maybe a better way of doing this would be to use priority levels if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack" && type != "Stop") { var order = { "type": type, "data": data }; var packingOrder = this.orderQueue.shift(); if (type == "Attack") { // The Attack order is able to handle a packing unit, while other orders can't. this.orderQueue = [packingOrder]; this.PushOrderFront(type, data, true); } else if (packingOrder.type == "Unpack" && g_OrdersCancelUnpacking.has(type)) { // Immediately cancel unpacking before processing an order that demands a packed unit. let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.CancelPack(); this.orderQueue = []; this.PushOrder(type, data); } else this.orderQueue = [packingOrder, order]; } else if (this.IsFormationMember()) { // Don't replace orders after a LeaveFormation order // (this is needed to support queued no-formation orders). let idx = this.orderQueue.findIndex(o => o.type == "LeaveFormation"); if (idx === -1) { this.orderQueue = []; this.order = undefined; } else this.orderQueue.splice(0, idx); this.PushOrderFront(type, data); } else { this.orderQueue = []; this.PushOrder(type, data); } if (garrisonHolder) this.PushOrder("Garrison", { "target": garrisonHolder }); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.GetOrders = function() { return this.orderQueue.slice(); }; UnitAI.prototype.AddOrders = function(orders) { orders.forEach(order => this.PushOrder(order.type, order.data)); }; UnitAI.prototype.GetOrderData = function() { var orders = []; for (let order of this.orderQueue) if (order.data) orders.push(clone(order.data)); return orders; }; UnitAI.prototype.UpdateWorkOrders = function(type) { var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource"; if (isWorkType(type)) { this.workOrders = []; return; } if (this.workOrders.length) return; 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; if (this.IsGarrisoned()) { let cmpGarrisonHolder = Engine.QueryInterface(this.GetGarrisonHolder(), IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.PerformEject([this.entity], false)) return false; } this.orderQueue = []; this.AddOrders(this.workOrders); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); 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; }; UnitAI.prototype.OnMotionUpdate = function(msg) { if (msg.veryObstructed) msg.obstructed = true; this.UnitFsm.ProcessMessage(this, Object.assign({ "type": "MovementUpdate" }, msg)); }; /** * Called directly by cmpFoundation and cmpRepairable to * inform builders that repairing has finished. * This not done by listening to a global message due to performance. */ UnitAI.prototype.ConstructionFinished = function(msg) { this.UnitFsm.ProcessMessage(this, { "type": "ConstructionFinished", "data": msg }); }; UnitAI.prototype.OnGlobalEntityRenamed = function(msg) { let changed = false; let currentOrderChanged = false; for (let i = 0; i < this.orderQueue.length; ++i) { let order = this.orderQueue[i]; if (order.data && order.data.target && order.data.target == msg.entity) { changed = true; if (i == 0) currentOrderChanged = true; order.data.target = msg.newentity; } if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity) { changed = true; if (i == 0) currentOrderChanged = true; order.data.formationTarget = msg.newentity; } } if (!changed) return; if (currentOrderChanged) this.UnitFsm.ProcessMessage(this, { "type": "OrderTargetRenamed", "data": msg }); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.OnAttacked = function(msg) { if (msg.fromStatusEffect) return; this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; UnitAI.prototype.OnGuardedAttacked = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data}); }; 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 }); else if (msg.tag == this.losAttackRangeQuery) this.UnitFsm.ProcessMessage(this, { "type": "LosAttackRangeUpdate", "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() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpUnitMotion) return 0; return cmpUnitMotion.GetWalkSpeed(); }; UnitAI.prototype.GetRunMultiplier = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpUnitMotion) return 0; return cmpUnitMotion.GetRunMultiplier(); }; /** * 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 cmpHealth = QueryMiragedInterface(ent, IID_Health); return cmpHealth && 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 position of target or, if there is none, * the entity's position, or undefined. */ UnitAI.prototype.TargetPosOrEntPos = function(target) { let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (cmpTargetPosition && cmpTargetPosition.IsInWorld()) return cmpTargetPosition.GetPosition2D(); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) return cmpPosition.GetPosition2D(); return undefined; }; /** * Returns the entity ID of the nearest resource supply where the given * filter returns true, or undefined if none can be found. * "Nearest" is nearest from @param position. * TODO: extend this to exclude resources that already have lots of gatherers. */ UnitAI.prototype.FindNearbyResource = function(position, filter) { if (!position) return undefined; // We accept resources owned by Gaia or any player let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); let range = 64; // TODO: what's a sensible number? let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // Don't account for entity size, we need to match LOS visibility. let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_ResourceSupply, false); return nearby.find(ent => { if (!this.CanGather(ent) || !this.CheckTargetVisible(ent)) return false; let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); let type = cmpResourceSupply.GetType(); let amount = cmpResourceSupply.GetCurrentAmount(); let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (template.indexOf("resource|") != -1) template = template.slice(9); return amount > 0 && cmpResourceSupply.IsAvailableTo(this.entity) && filter(ent, type, template); }); }; /** * 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) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return undefined; let pos = cmpPosition.GetPosition2D(); let bestDropsite; let bestDist = Infinity; // Maximum distance a point on an obstruction can be from the center of the obstruction. let maxDifference = 40; let owner = cmpOwnership.GetOwner(); let cmpPlayer = QueryOwnerInterface(this.entity); let players = cmpPlayer && cmpPlayer.HasSharedDropsites() ? cmpPlayer.GetMutualAllies() : [owner]; let nearestDropsites = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite, false); let isShip = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship"); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); for (let dropsite of nearestDropsites) { // Ships are unable to reach land dropsites and shouldn't attempt to do so. if (isShip && !Engine.QueryInterface(dropsite, IID_Identity).HasClass("Naval")) continue; let cmpResourceDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite); if (!cmpResourceDropsite.AcceptsType(genericType) || !this.CheckTargetVisible(dropsite)) continue; if (Engine.QueryInterface(dropsite, IID_Ownership).GetOwner() != owner && !cmpResourceDropsite.IsShared()) continue; // The range manager sorts entities by the distance to their center, // but we want the distance to the point where resources will be dropped off. let dist = cmpObstructionManager.DistanceToPoint(dropsite, pos.x, pos.y); if (dist == -1) continue; if (dist < bestDist) { bestDropsite = dropsite; bestDist = dist; } else if (dist > bestDist + maxDifference) break; } return bestDropsite; }; /** * Returns the entity ID of the nearest building that needs to be constructed. * "Nearest" is nearest from @param position. */ UnitAI.prototype.FindNearbyFoundation = function(position) { if (!position) return undefined; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; let players = [cmpOwnership.GetOwner()]; let range = 64; // TODO: what's a sensible number? let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); // Don't account for entity size, we need to match LOS visibility. let nearby = cmpRangeManager.ExecuteQueryAroundPos(position, 0, range, players, IID_Foundation, false); // Skip foundations that are already complete. (This matters since // we process the ConstructionFinished message before the foundation // we're working on has been deleted.) return nearby.find(ent => !Engine.QueryInterface(ent, IID_Foundation).IsFinished()); }; /** * Play a sound appropriate to the current entity. */ UnitAI.prototype.PlaySound = function(name) { if (this.IsFormationController()) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); var member = cmpFormation.GetPrimaryMember(); if (member) PlaySound(name, member); } else { PlaySound(name, this.entity); } }; /* * Set a visualActor animation variant. * By changing the animation variant, you can change animations based on unitAI state. * If there are no specific variants or the variant doesn't exist in the actor, * the actor fallbacks to any existing animation. * @param type if present, switch to a specific animation variant. */ UnitAI.prototype.SetAnimationVariant = function(type) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetVariant("animationVariant", type); }; /* * Reset the animation variant to default behavior. * Default behavior is to pick a resource-carrying variant if resources are being carried. * Otherwise pick nothing in particular. */ UnitAI.prototype.SetDefaultAnimationVariant = function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { let type = cmpResourceGatherer.GetLastCarriedType(); if (type) { let typename = "carry_" + type.generic; if (type.specific == "meat") typename = "carry_" + type.specific; this.SetAnimationVariant(typename); return; } } this.SetAnimationVariant(""); }; UnitAI.prototype.ResetAnimation = function() { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation("idle", false, 1.0); }; UnitAI.prototype.SelectAnimation = function(name, once = false, speed = 1.0) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation(name, once, speed); }; 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() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.StopMoving(); }; /** * Generic dispatcher for other MoveTo functions. * @param iid - Interface ID (optional) implementing GetRange * @param type - Range type for the interface call * @returns whether the move succeeded or failed. */ UnitAI.prototype.MoveTo = function(data, iid, type) { if (data.target) { if (data.min || data.max) return this.MoveToTargetRangeExplicit(data.target, data.min || -1, data.max || -1); else if (!iid) return this.MoveToTarget(data.target); return this.MoveToTargetRange(data.target, iid, type); } else if (data.min || data.max) return this.MoveToPointRange(data.x, data.z, data.min || -1, data.max || -1); return this.MoveToPoint(data.x, data.z); }; UnitAI.prototype.MoveToPoint = function(x, z) { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, 0, 0); // For point goals, allow a max range of 0. }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; UnitAI.prototype.MoveToTarget = function(target) { if (!this.CheckTargetVisible(target)) return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, 0, 1); }; UnitAI.prototype.MoveToTargetRange = function(target, iid, type) { if (!this.CheckTargetVisible(target)) return false; let range = this.GetRange(iid, type); if (!range) return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && 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()) { let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation()) return false; } let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!this.AbleToMove(cmpUnitMotion)) return false; let 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; let range = this.GetRange(IID_Attack, type); if (!range) return false; let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!thisCmpPosition.IsInWorld()) return false; let s = thisCmpPosition.GetPosition(); let targetCmpPosition = Engine.QueryInterface(target, IID_Position); if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) return false; // Parabolic range compuation is the same as in BuildingAI's FireArrows. let t = targetCmpPosition.GetPosition(); // h is positive when I'm higher than the target let h = s.y - t.y + range.elevationBonus; let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); // No negative roots please if (h <= -range.max / 2) // return false? Or hope you come close enough? parabolicMaxRange = 0; // The parabole changes while walking so be cautious: let guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange; return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); }; UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) { if (!this.CheckTargetVisible(target)) return false; let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, min, max); }; /** * Move unit so we hope the target is in the attack range of the formation. * * @param {number} target - The target entity ID to attack. * @return {boolean} - Whether the order to move has succeeded. */ UnitAI.prototype.MoveFormationToTargetAttackRange = function(target) { let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation); if (cmpTargetFormation) target = cmpTargetFormation.GetClosestMember(this.entity); if (!this.CheckTargetVisible(target)) return false; let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpFormationAttack) return false; let range = cmpFormationAttack.GetRange(target); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.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(); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; /** * Generic dispatcher for other Check...Range functions. * @param iid - Interface ID (optional) implementing GetRange * @param type - Range type for the interface call */ UnitAI.prototype.CheckRange = function(data, iid, type) { if (data.target) { if (data.min || data.max) return this.CheckTargetRangeExplicit(data.target, data.min || -1, data.max || -1); else if (!iid) return this.CheckTargetRangeExplicit(data.target, 0, 1); return this.CheckTargetRange(data.target, iid, type); } else if (data.min || data.max) return this.CheckPointRangeExplicit(data.x, data.z, data.min || -1, data.max || -1); return this.CheckPointRangeExplicit(data.x, data.z, 0, 0); }; UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max) { let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, false); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) { let range = this.GetRange(iid, type); if (!range) return false; let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); }; /** * 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()) { let cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation() && cmpFormationUnitAI.order.data.target == target) return true; } let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); if (type != "Ranged") return this.CheckTargetRange(target, IID_Attack, type); let targetCmpPosition = Engine.QueryInterface(target, IID_Position); if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) return false; let range = this.GetRange(IID_Attack, type); if (!range) return false; let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!thisCmpPosition.IsInWorld()) return false; let s = thisCmpPosition.GetPosition(); let t = targetCmpPosition.GetPosition(); let h = s.y - t.y + range.elevationBonus; let maxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); if (maxRange < 0) return false; let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, maxRange, false); }; UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) { let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, false); }; /** * Check if the target is inside the attack range of the formation. * * @param {number} target - The target entity ID to attack. * @return {boolean} - Whether the entity is within attacking distance. */ UnitAI.prototype.CheckFormationTargetAttackRange = function(target) { let cmpTargetFormation = Engine.QueryInterface(target, IID_Formation); if (cmpTargetFormation) target = cmpTargetFormation.GetClosestMember(this.entity); let cmpFormationAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpFormationAttack) return false; let range = cmpFormationAttack.GetRange(target); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); }; UnitAI.prototype.CheckGarrisonRange = function(target) { let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; let range = cmpGarrisonHolder.GetLoadingRange(); return this.CheckTargetRangeExplicit(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; }; /** * Returns true if the given position is currentl visible (not in FoW/SoD). */ UnitAI.prototype.CheckPositionVisible = function(x, z) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return false; return cmpRangeManager.GetLosVisibilityPosition(x, z, cmpOwnership.GetOwner()) == "visible"; }; /** * How close to our goal do we consider it's OK to stop if the goal appears unreachable. * Currently 3 terrain tiles as that's relatively close but helps pathfinding. */ UnitAI.prototype.DefaultRelaxedMaxRange = 12; /** * @returns true if the unit is in the relaxed-range from the target. */ UnitAI.prototype.RelaxedMaxRangeCheck = function(data, relaxedRange) { if (!data.relaxed) return false; let ndata = data; ndata.min = 0; ndata.max = relaxedRange; return this.CheckRange(ndata); }; /** * Let an entity face its target. * @param {number} target - The entity-ID of the target. */ UnitAI.prototype.FaceTowardsTarget = function(target) { let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; let targetPosition = cmpTargetPosition.GetPosition2D(); // Use cmpUnitMotion for units that support that, otherwise try cmpPosition (e.g. turrets) let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) { cmpUnitMotion.FaceTowardsPoint(targetPosition.x, targetPosition.y); return; } let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) cmpPosition.TurnTo(cmpPosition.GetPosition2D().angleTo(targetPosition)); }; UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type) { let range = this.GetRange(iid, type); if (!range) return false; let cmpPosition = Engine.QueryInterface(target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; let halfvision = cmpVision.GetRange() / 2; let pos = cmpPosition.GetPosition(); let heldPosition = this.heldPosition; if (heldPosition === undefined) heldPosition = { "x": pos.x, "z": pos.z }; return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max; }; UnitAI.prototype.CheckTargetIsInVisionRange = function(target) { let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; let range = cmpVision.GetRange(); let distance = PositionHelper.DistanceBetweenEntities(this.entity, target); return distance < range; }; UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttackAgainst(target, allowCapture); }; /** * 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) { var target = ents.find(target => this.CanAttack(target)); if (!target) return false; this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true }); return true; }; /** * 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) { var target = ents.find(target => this.CanAttack(target) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true)) && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)) ); if (!target) return false; this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true }); return true; }; /** * 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); if (this.GetStance().respondStandGround) return this.AttackVisibleEntity(ents); if (this.GetStance().respondHoldGround) return this.AttackEntityInZone(ents); if (this.GetStance().respondFlee) { if (this.order && this.order.type == "Flee") this.orderQueue.shift(); this.PushOrderFront("Flee", { "target": ents[0], "force": false }); return true; } return false; }; /** * @param {number} ents - An array of the IDs of the spotted entities. * @return {boolean} - Whether we responded. */ UnitAI.prototype.RespondToSightedEntities = function(ents) { if (!ents || !ents.length) return false; if (this.GetStance().respondFleeOnSight) { this.Flee(ents[0], false); return true; } return false; }; /** * Try to respond to healable entities. * Returns true if it responded. */ UnitAI.prototype.RespondToHealableEntities = function(ents) { let ent = ents.find(ent => this.CanHeal(ent)); if (!ent) return false; this.PushOrderFront("Heal", { "target": ent, "force": false }); return true; }; /** * Returns true if we should stop following the target entity. */ UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type) { if (!this.CheckTargetVisible(target)) return true; // 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) { let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); let cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack && cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))) return false; } 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; return false; }; /* * Returns whether we should chase the targeted entity, * given our current stance. */ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force) { if (!this.AbleToMove()) 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) { let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); let cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack && cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))) return true; } return force; }; //// External interface functions //// /** * Order a unit to leave the formation it is in. * Used to handle queued no-formation orders for units in formation. */ UnitAI.prototype.LeaveFormation = function(queued = true) { // If queued, add the order even if we're not in formation, // maybe we will be later. if (!queued && !this.IsFormationMember()) return; if (queued) this.AddOrder("LeaveFormation", { "force": true }, queued); else this.PushOrderFront("LeaveFormation", { "force": true }); }; 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.GetFormationTemplate = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || NULL_FORMATION; }; 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(); 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": case "Patrol": 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": 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 distance; }; UnitAI.prototype.AddOrder = function(type, data, queued) { if (this.expectedRoute) this.expectedRoute = undefined; if (queued) this.PushOrder(type, data); else { // May happen if an order arrives on the same turn the unit is garrisoned // in that case, just forget the order as this will lead to an infinite loop if (this.IsGarrisoned() && !this.IsTurret() && type != "Ungarrison") return; 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 (target === this.entity) return; 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); }; /** * @return {boolean} - Whether it makes sense to guard the given entity. */ UnitAI.prototype.ShouldGuard = function(target) { return this.TargetIsAlive(target) || Engine.QueryInterface(target, IID_Capturable) || Engine.QueryInterface(target, IID_StatusEffectsReceiver); }; UnitAI.prototype.AddGuard = function(target) { if (!this.CanGuard()) return false; var cmpGuard = Engine.QueryInterface(target, IID_Guard); if (!cmpGuard) return false; this.isGuardOf = target; this.guardRange = cmpGuard.GetRange(this.entity); cmpGuard.AddGuard(this.entity); return true; }; UnitAI.prototype.RemoveGuard = function() { if (!this.isGuardOf) return; let 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 (let i = 1; i < this.orderQueue.length; ++i) if (this.orderQueue[i].type == "Guard") this.orderQueue.splice(i, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; 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; return this.template.CanGuard == "true"; }; UnitAI.prototype.CanPatrol = function() { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) return this.IsFormationController() || this.template.CanPatrol == "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", { "force": true }, 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, allowCapture = true, queued = false) { this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued); }; UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false) { if (!this.CanPatrol()) { this.Walk(x, z, queued); return; } this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "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. if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target))) return; if (this.orderQueue.length && this.orderQueue[0].type == "Unpack" && this.WillMoveFromFoundation(target, false)) { let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) cmpPack.CancelPack(); } if (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, allowCapture = true, queued = false) { 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; } let order = { "target": target, "force": true, "allowCapture": allowCapture, }; this.RememberTargetPosition(order); if (this.order && this.order.type == "Attack" && this.order.data && this.order.data.target === order.target && this.order.data.allowCapture === order.allowCapture) { this.order.data.lastPos = order.lastPos; this.order.data.force = order.force; return; } this.AddOrder("Attack", order, 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.SetImmobile(false); this.AddOrder("Ungarrison", null, false); } }; /** * Adds a garrison order for units that are already garrisoned in the garrison holder. */ UnitAI.prototype.Autogarrison = function(target) { this.isGarrisoned = true; this.PushOrderFront("Garrison", { "target": target }); }; /** * 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 = QueryMiragedInterface(target, IID_ResourceSupply); if (cmpResourceSupply) type = cmpResourceSupply.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); if (template.indexOf("resource|") != -1) template = template.slice(9); let order = { "target": target, "type": type, "template": template, "force": force, }; this.RememberTargetPosition(order); order.initPos = order.lastPos; if (this.order && (this.order.type == "Gather" || this.order.type == "Attack") && this.order.data && this.order.data.target === order.target) { this.order.data.lastPos = order.lastPos; this.order.data.force = order.force; return; } this.AddOrder("Gather", order, 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) { 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; } if (this.order && this.order.type == "Heal" && this.order.data && this.order.data.target === target) { this.order.data.force = true; 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); }; UnitAI.prototype.CancelSetupTradeRoute = function(target) { let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader) return; cmpTrader.RemoveTargetMarket(target); if (this.IsFormationController()) this.CallMemberFunction("CancelSetupTradeRoute", [target]); }; /** * 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; } // AI has currently no access to BackToWork let cmpPlayer = QueryOwnerInterface(this.entity); if (cmpPlayer && cmpPlayer.IsAI() && !this.IsFormationController() && this.workOrders.length && this.workOrders[0].type == "Trade") { let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets() && (cmpTrader.GetFirstMarket() == target && cmpTrader.GetSecondMarket() == source || cmpTrader.GetFirstMarket() == source && cmpTrader.GetSecondMarket() == target)) { this.BackToWork(); return; } } var marketsChanged = this.SetTargetMarket(target, source); if (!marketsChanged) return; var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets()) { let data = { "target": cmpTrader.GetFirstMarket(), "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]); let 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.SwitchMarketOrder = function(oldMarket, newMarket) { if (this.order && this.order.data && this.order.data.target && this.order.data.target == oldMarket) this.order.data.target = newMarket; }; UnitAI.prototype.MoveToMarket = function(targetMarket) { let nextTarget; if (this.waypoints && this.waypoints.length >= 1) nextTarget = this.waypoints.pop(); else nextTarget = { "target": targetMarket }; this.order.data.nextTarget = nextTarget; return this.MoveTo(this.order.data.nextTarget, IID_Trader); }; UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket) { if (!this.CanTrade(currentMarket)) { this.StopTrading(); return; } if (!this.CheckTargetRange(currentMarket, IID_Trader)) { if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again this.StopTrading(); return; } let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); let nextMarket = cmpTrader.PerformTrade(currentMarket); let amount = cmpTrader.GetGoods().amount; if (!nextMarket || !amount || !amount.traderGain) { this.StopTrading(); return; } this.order.data.target = nextMarket; if (this.order.data.route && this.order.data.route.length) { this.waypoints = this.order.data.route.slice(); if (this.order.data.target == cmpTrader.GetSecondMarket()) this.waypoints.reverse(); } this.SetNextState("APPROACHINGMARKET"); }; UnitAI.prototype.MarketRemoved = function(market) { if (this.order && this.order.data && this.order.data.target && this.order.data.target == market) this.UnitFsm.ProcessMessage(this, { "type": "TradingCanceled", "market": market }); }; UnitAI.prototype.StopTrading = function() { 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; } if (this.order && this.order.type == "Repair" && this.order.data && this.order.data.target === target && this.order.data.autocontinue === autocontinue) { this.order.data.force = true; 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); }; UnitAI.prototype.Cheer = function() { this.PushOrderFront("Cheer", { "force": false }); }; UnitAI.prototype.Pack = function(queued) { if (this.CanPack()) this.AddOrder("Pack", { "force": true }, queued); }; UnitAI.prototype.Unpack = function(queued) { 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; Engine.PostMessage(this.entity, MT_UnitStanceChanged, { "to": this.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); // 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 the losRangeQuery. * @return {boolean} - Whether there are targets in range that we ought to react upon. */ UnitAI.prototype.FindSightedEnemies = function() { if (!this.losRangeQuery) return false; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.RespondToSightedEntities(cmpRangeManager.ResetActiveQuery(this.losRangeQuery)); }; /** * 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; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.RespondToHealableEntities(cmpRangeManager.ResetActiveQuery(this.losHealRangeQuery)); }; /** * Resets losAttackRangeQuery, 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.losAttackRangeQuery) return false; if (!this.GetStance().targetVisibleEnemies) return false; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.AttackEntitiesByPreference(cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery)); }; UnitAI.prototype.FindWalkAndFightTargets = function() { if (this.IsFormationController()) { var cmpUnitAI; var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); for (var ent of 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": false, "allowCapture": this.order.data.allowCapture }); 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": false, "allowCapture": this.order.data.allowCapture }); return true; } // healers on a walk-and-fight order should heal injured units if (this.IsHealer()) return this.FindNewHealTargets(); return false; }; UnitAI.prototype.GetTargetsFromUnit = function() { if (!this.losAttackRangeQuery) return []; if (!this.GetStance().targetVisibleEnemies) return []; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return []; let attackfilter = function(e) { if (!cmpAttack.CanAttack(e)) return false; let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery); let targets = entities.filter(attackfilter).sort(function(a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }); return targets; }; UnitAI.prototype.GetQueryRange = function(iid) { let ret = { "min": 0, "max": 0 }; let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; let visionRange = cmpVision.GetRange(); if (iid === IID_Vision) { ret.max = visionRange; return ret; } if (this.GetStance().respondStandGround) { let range = this.GetRange(iid); if (!range) return ret; ret.min = range.min; ret.max = Math.min(range.max, visionRange); } else if (this.GetStance().respondChase) ret.max = visionRange; else if (this.GetStance().respondHoldGround) { let range = this.GetRange(iid); if (!range) return ret; ret.max = Math.min(range.max + visionRange / 2, visionRange); } // 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) ret.max = visionRange; return ret; }; UnitAI.prototype.GetStance = function() { return g_Stances[this.stance]; }; UnitAI.prototype.GetSelectableStances = function() { if (this.IsTurret()) return []; return Object.keys(g_Stances).filter(key => g_Stances[key].selectable); }; UnitAI.prototype.GetStanceName = function() { return this.stance; }; /* * Make the unit walk at its normal pace. */ UnitAI.prototype.ResetSpeedMultiplier = function() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetSpeedMultiplier(1); }; UnitAI.prototype.SetSpeedMultiplier = function(speed) { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetSpeedMultiplier(speed); }; /** * Try to match the targets current movement speed. * * @param {number} target - The entity ID of the target to match. * @param {boolean} mayRun - Whether the entity is allowed to run to match the speed. */ UnitAI.prototype.TryMatchTargetSpeed = function(target, mayRun = true) { let cmpUnitMotionTarget = Engine.QueryInterface(target, IID_UnitMotion); if (cmpUnitMotionTarget) { let targetSpeed = cmpUnitMotionTarget.GetCurrentSpeed(); if (targetSpeed) this.SetSpeedMultiplier(Math.min(mayRun ? this.GetRunMultiplier() : 1, targetSpeed / this.GetWalkSpeed())); } }; /* * Remember the position of the target (in lastPos), if any, in case it disappears later * and we want to head to its last known position. * @param orderData - The order data to set this on. Defaults to this.order.data */ UnitAI.prototype.RememberTargetPosition = function(orderData) { if (!orderData) orderData = this.order.data; let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) orderData.lastPos = cmpPosition.GetPosition(); }; 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 //// /** * General getter for ranges. * * @param {number} iid * @param {string} type - [Optional] * @return {Object | undefined} - The range in the form * { "min": number, "max": number } * Object."elevationBonus": number may be present when iid == IID_Attack. * Returns undefined when the entity does not have the requested component. */ UnitAI.prototype.GetRange = function(iid, type) { let component = Engine.QueryInterface(this.entity, iid); if (!component) return undefined; return component.GetRange(type); } UnitAI.prototype.CanAttack = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(target); }; 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; let cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target))) return false; return true; }; UnitAI.prototype.CanGather = function(target) { if (this.IsTurret()) return false; var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); if (!cmpResourceSupply) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; 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; let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); return cmpHeal && cmpHeal.CanHeal(target); }; /** * Check if the entity can return carried resources at @param target * @param checkCarriedResource check we are carrying resources * @param cmpResourceGatherer if present, use this directly instead of re-querying. */ UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource, cmpResourceGatherer = undefined) { 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; if (!cmpResourceGatherer) { cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; } let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return false; if (checkCarriedResource) { let type = cmpResourceGatherer.GetMainCarryingType(); if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; } let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) return true; let cmpPlayer = QueryOwnerInterface(this.entity); return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() && cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target); }; 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; var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); return cmpTrader && cmpTrader.CanTrade(target); }; 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; var cmpFoundation = QueryMiragedInterface(target, IID_Foundation); var cmpRepairable = Engine.QueryInterface(target, IID_Repairable); if (!cmpFoundation && !cmpRepairable) return false; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target); }; 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(); }; //// Formation specific functions //// UnitAI.prototype.IsAttackingAsFormation = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttackAsFormation() && this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING"; }; UnitAI.prototype.MoveRandomly = function(distance) { // To minimize drift all across the map, describe circles // approximated by polygons. // And to avoid getting stuck in obstacles or narrow spaces, each side // of the polygon is obtained by trying to go away from a point situated // half a meter backwards of the current position, after rotation. // We also add a fluctuation on the length of each side of the polygon (dist) // which, in addition to making the move more random, helps escaping narrow spaces // with bigger values of dist. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion) return; let pos = cmpPosition.GetPosition(); let ang = cmpPosition.GetRotation().y; if (!this.roamAngle) { this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6; ang -= this.roamAngle / 2; this.startAngle = ang; } else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2)) this.roamAngle *= randBool() ? 1 : -1; let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4); // First half rotation to decrease the impression of immediate rotation ang += halfDelta; cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang)); // Then second half of the rotation ang += halfDelta; let dist = randFloat(0.5, 1.5) * distance; cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1); }; UnitAI.prototype.SetFacePointAfterMove = function(val) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpMotion) cmpMotion.SetFacePointAfterMove(val); }; UnitAI.prototype.GetFacePointAfterMove = function() { let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion && cmpUnitMotion.GetFacePointAfterMove(); } UnitAI.prototype.AttackEntitiesByPreference = function(ents) { if (!ents.length) return false; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; let attackfilter = function(e) { if (!cmpAttack.CanAttack(e)) return false; let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; let entsByPreferences = {}; let preferences = []; let entsWithoutPref = []; for (let ent of ents) { if (!attackfilter(ent)) continue; let pref = cmpAttack.GetPreference(ent); if (pref === null || pref === undefined) entsWithoutPref.push(ent); else if (!entsByPreferences[pref]) { preferences.push(pref); entsByPreferences[pref] = [ent]; } else entsByPreferences[pref].push(ent); } if (preferences.length) { preferences.sort((a, b) => a - b); for (let pref of preferences) if (this.RespondToTargetedEntities(entsByPreferences[pref])) return true; } return this.RespondToTargetedEntities(entsWithoutPref); }; /** * Call UnitAI.funcname(args) on all formation members. * @param resetWaitingEntities - If true, call ResetWaitingEntities first. * If the controller wants to wait on its members to finish their order, * this needs to be reset before sending new orders (in case they instafail) * so it makes sense to do it here. * Only set this to false if you're sure it's safe. */ UnitAI.prototype.CallMemberFunction = function(funcname, args, resetWaitingEntities = true) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; if (resetWaitingEntities) cmpFormation.ResetWaitingEntities(); cmpFormation.GetMembers().forEach(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI[funcname].apply(cmpUnitAI, args); }); }; /** * Call obj.funcname(args) on UnitAI components owned by player in given range. */ UnitAI.prototype.CallPlayerOwnedEntitiesFunctionInRange = function(funcname, args, range) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; let owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER) return; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, [owner], IID_UnitAI, true); for (let i = 0; i < nearby.length; ++i) { let cmpUnitAI = Engine.QueryInterface(nearby[i], 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) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); return cmpFormation && cmpFormation.GetMembers().every(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI[funcname].apply(cmpUnitAI, args); }); }; UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec); Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ResourceSupply.js (revision 24963) @@ -1,74 +1,748 @@ Resources = { "BuildChoicesSchema": () => { let schema = ""; for (let res of ["food", "metal"]) { for (let subtype in ["meat", "grain"]) schema += "" + res + "." + subtype + ""; schema += " treasure." + res + ""; } return "" + schema + ""; } }; +Engine.LoadHelperScript("ValueModification.js"); +Engine.LoadComponentScript("interfaces/Health.js"); +Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); +Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("ResourceSupply.js"); +Engine.LoadComponentScript("Timer.js"); -const entity = 60; +let entity = 60; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": () => 3 }); AddMock(entity, IID_Fogging, { "Activate": () => {} }); let template = { - "Amount": 1000, + "Max": "1001", + "Initial": "1000", "Type": "food.meat", - "KillBeforeGather": false, - "MaxGatherers": 2 + "KillBeforeGather": "false", + "MaxGatherers": "2" }; let cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", template); +cmpResourceSupply.OnOwnershipChanged({ "to": 1 }); TS_ASSERT(!cmpResourceSupply.IsInfinite()); TS_ASSERT(!cmpResourceSupply.GetKillBeforeGather()); -TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 1000); +TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxAmount(), 1001); TS_ASSERT_EQUALS(cmpResourceSupply.GetMaxGatherers(), 2); TS_ASSERT_EQUALS(cmpResourceSupply.GetDiminishingReturns(), null); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 0); TS_ASSERT(cmpResourceSupply.IsAvailableTo(70)); TS_ASSERT(cmpResourceSupply.AddGatherer(70)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); TS_ASSERT(cmpResourceSupply.AddGatherer(71)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); TS_ASSERT(!cmpResourceSupply.AddGatherer(72)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); TS_ASSERT(cmpResourceSupply.IsAvailableTo(70)); TS_ASSERT(!cmpResourceSupply.IsAvailableTo(73)); TS_ASSERT(!cmpResourceSupply.AddGatherer(73)); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); cmpResourceSupply.RemoveGatherer(70); TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); +TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); +TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); + +cmpResourceSupply.RemoveGatherer(70); +TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); + +TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); +TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 2); + +cmpResourceSupply.RemoveGatherer(70); +TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 1); + TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.TakeResources(300), { "amount": 300, "exhausted": false }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 700); TS_ASSERT(cmpResourceSupply.IsAvailableTo(70)); TS_ASSERT_UNEVAL_EQUALS(cmpResourceSupply.TakeResources(800), { "amount": 700, "exhausted": true }); TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 0); // The resource is not available when exhausted TS_ASSERT(!cmpResourceSupply.IsAvailableTo(70)); + +cmpResourceSupply.RemoveGatherer(71); +TS_ASSERT_EQUALS(cmpResourceSupply.GetNumGatherers(), 0); + + +// Test Changes. + +let cmpTimer; +function reset(newTemplate) +{ + cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); + cmpResourceSupply = ConstructComponent(entity, "ResourceSupply", newTemplate); + cmpResourceSupply.OnOwnershipChanged({ "to": 1 }); +} + +// Decay. +template = { + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 999); +cmpTimer.OnUpdate({ "turnLength": 5 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); + +// Decay with minimum. +template = { + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000", + "LowerLimit": "997" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpTimer.OnUpdate({ "turnLength": 3 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 997); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); + +// Decay with maximum. +template = { + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000", + "UpperLimit": "995" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); + +// Decay with minimum and maximum. +template = { + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000", + "UpperLimit": "995", + "LowerLimit": "990" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); +cmpResourceSupply.TakeResources(6); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); +cmpTimer.OnUpdate({ "turnLength": 10 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 989); + +// Growth. +template = { + "Initial": "995", + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); +cmpTimer.OnUpdate({ "turnLength": 5 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 1000); + +// Growth with minimum. +template = { + "Initial": "995", + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "LowerLimit": "997" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); + +// Growth with maximum. +template = { + "Initial": "994", + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "UpperLimit": 995 + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 994); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); + +// Growth with minimum and maximum. +template = { + "Initial": "990", + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "UpperLimit": "995", + "LowerLimit": "990" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 990); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 991); +cmpTimer.OnUpdate({ "turnLength": 8 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); + +// Growth when resources are taken again. +template = { + "Initial": "995", + "Max": "1000", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 996); +cmpResourceSupply.TakeResources(6); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 990); +cmpTimer.OnUpdate({ "turnLength": 5 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 995); + +// Decay when dead. +template = { + "Max": "10", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000", + "State": "dead" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); + +// No growth when dead. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "State": "alive" + } + }, + "MaxGatherers": "2" +}; +reset(template); + +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); + +// Decay when dead or alive. +template = { + "Max": "10", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000", + "State": "dead alive" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); + +AddMock(entity, IID_Health, {}); // Bring the entity to life. + +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); + +// No decay when alive. +template = { + "Max": "10", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000", + "State": "dead" + } + }, + "MaxGatherers": "2" +}; +reset(template); + +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); + +// Growth when alive. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "State": "alive" + } + }, + "MaxGatherers": "2" +}; +reset(template); + +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); + +// Growth when dead or alive. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "State": "dead alive" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); + +DeleteMock(entity, IID_Health); // "Kill" the entity. + +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); + +// Decay *and* growth. +template = { + "Max": "10", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000" + }, + "Growth": { + "Value": "1", + "Interval": "1000" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); + +// Decay *and* growth with different health states. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Rotting": { + "Value": "-1", + "Interval": "1000", + "State": "dead" + }, + "Growth": { + "Value": "1", + "Interval": "1000", + "State": "alive" + } + }, + "MaxGatherers": "2" +}; +AddMock(entity, IID_Health, { }); // Bring the entity to life. +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); +DeleteMock(entity, IID_Health); // "Kill" the entity. +// We overshoot one due to lateness. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); + +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); + +// Two effects with different limits. +template = { + "Max": "20", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "SuperGrowth": { + "Value": "2", + "Interval": "1000", + "UpperLimit": "8" + }, + "Growth": { + "Value": "1", + "Interval": "1000", + "UpperLimit": "12" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 11); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 12); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13); + +// Two effects with different limits. +// This in an interesting case, where the order of the changes matters. +template = { + "Max": "20", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "UpperLimit": "12" + }, + "SuperGrowth": { + "Value": "2", + "Interval": "1000", + "UpperLimit": "8" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); +cmpTimer.OnUpdate({ "turnLength": 5 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 13); + +// Infinity with growth. +template = { + "Max": "Infinity", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); + +// Infinity with decay. +template = { + "Max": "Infinity", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Decay": { + "Value": "-1", + "Interval": "1000" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), Infinity); + +// Decay when not gathered. +template = { + "Max": "10", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Decay": { + "Value": "-1", + "Interval": "1000", + "State": "notGathered" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 10); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); +TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); +cmpResourceSupply.RemoveGatherer(70); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); + +// Grow when gathered. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "State": "gathered" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); +cmpResourceSupply.RemoveGatherer(70, 1); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); + +// Grow when gathered or not. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "State": "notGathered gathered" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); +TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 8); +cmpResourceSupply.RemoveGatherer(70); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 9); + +// Grow when gathered and alive. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Growth": { + "Value": "1", + "Interval": "1000", + "State": "alive gathered" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +AddMock(entity, IID_Health, { }); // Bring the entity to life. +cmpResourceSupply.CheckTimers(); // No other way to tell we've come to life. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 6); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); +cmpResourceSupply.RemoveGatherer(70); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); +DeleteMock(entity, IID_Health); // "Kill" the entity. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 7); + +// Decay when dead and not gathered. +template = { + "Max": "10", + "Initial": "5", + "Type": "food.meat", + "KillBeforeGather": "false", + "Change": { + "Decay": { + "Value": "-1", + "Interval": "1000", + "State": "dead notGathered" + } + }, + "MaxGatherers": "2" +}; +reset(template); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 5); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); +TS_ASSERT(cmpResourceSupply.AddActiveGatherer(70)); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); +AddMock(entity, IID_Health, {}); // Bring the entity to life. +cmpResourceSupply.CheckTimers(); // No other way to tell we've come to life. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); +cmpResourceSupply.RemoveGatherer(70); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 4); +DeleteMock(entity, IID_Health); // "Kill" the entity. +cmpResourceSupply.CheckTimers(); // No other way to tell we've died. +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 3); +cmpTimer.OnUpdate({ "turnLength": 1 }); +TS_ASSERT_EQUALS(cmpResourceSupply.GetCurrentAmount(), 2); Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_boar.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_boar.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_boar.xml (revision 24963) @@ -1,36 +1,36 @@ Tusks 12.0 0.0 0.0 1.0 1000 2.0 50 Wild Boar Sus scrofa gaia/fauna_boar.png - 150 + 150 3.0 fauna/boar.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_03.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_03.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_03.xml (revision 24963) @@ -1,12 +1,12 @@ Berries - 200 + 200 props/flora/berry_bush_03.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/date.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/date.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/date.xml (revision 24963) @@ -1,12 +1,12 @@ Date Palm - 400 + 400 flora/trees/palm_date_new_fruit.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/olive.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/olive.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/olive.xml (revision 24963) @@ -1,12 +1,12 @@ Olive - 400 + 400 flora/trees/olive.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_minor.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_minor.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_minor.xml (revision 24963) @@ -1,34 +1,34 @@ 10.0 Minor Pyramid gaia/special_pyramid.png - 5000 + 5000 90 6.0 0.6 12.0 40 special/pyramid_minor.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_kushite.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_kushite.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_kushite.xml (revision 24963) @@ -1,25 +1,25 @@ 12.0 Kushite Statue structures/statue.png - 300 + 300 props/special/eyecandy/statues_kush.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rhinoceros_white.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rhinoceros_white.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rhinoceros_white.xml (revision 24963) @@ -1,43 +1,43 @@ Horn 20 0 20 4 2000 3.0 160 White Rhinoceros Ceratotherium simum gaia/fauna_rhino.png - 300 + 300 actor/fauna/animal/lion_attack.xml actor/fauna/animal/lion_death.xml 4.0 fauna/rhino.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_walrus.xml (revision 24963) @@ -1,46 +1,46 @@ Tusks 15 10 0 5 1000 2000 Structure Ship Siege 3.0 120 Walrus Odobenus rosmarus gaia/fauna_walrus.png - 300 + 300 256x256/ellipse.png 256x256/ellipse_mask.png 4.0 fauna/walrus.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml (revision 24963) @@ -1,27 +1,27 @@ 2.5 50 Common Zebra Equus quagga gaia/fauna_zebra.png - 150 + 150 3.5 0.9 fauna/zebra.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/banana.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/banana.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/banana.xml (revision 24963) @@ -1,12 +1,12 @@ Banana - 400 + 400 flora/trees/banana.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_02.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_02.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_02.xml (revision 24963) @@ -1,12 +1,12 @@ Berries - 200 + 200 props/flora/berry_bush_02.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_05.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_05.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_05.xml (revision 24963) @@ -1,12 +1,12 @@ Berries - 200 + 200 props/flora/bush_berries_large.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/grapes.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/grapes.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/grapes.xml (revision 24963) @@ -1,13 +1,13 @@ Grapes gaia/flora_bush_grapes.png - 200 + 200 props/flora/forage_grapes.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_great.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_great.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/pyramid_great.xml (revision 24963) @@ -1,37 +1,37 @@ 10.0 Great Pyramid gaia/special_pyramid.png -2.0 - 10000 + 10000 120 6.0 0.6 12.0 72 special/pyramid.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_egyptian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_egyptian.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_egyptian.xml (revision 24963) @@ -1,25 +1,25 @@ 12.0 Ptolemaic Egyptian Statues structures/statue.png - 300 + 300 props/special/eyecandy/statues_ptol.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml (revision 24963) @@ -1,34 +1,34 @@ 5.5 50 Dromedary Camelus dromedarius gaia/fauna_camel.png - 200 + 200 actor/fauna/movement/camel_order.xml actor/fauna/death/death_camel.xml 6.5 0.45 fauna/camel.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_cow.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_cow.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_cow.xml (revision 24963) @@ -1,31 +1,31 @@ 60 150 3.5 150 Cow Bos taurus taurus gaia/fauna_cow.png - 300 + 300 5 4.5 fauna/cow.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_sanga.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_sanga.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_sanga.xml (revision 24963) @@ -1,31 +1,31 @@ 60 150 3.5 150 Sanga Cattle Bos taurus africanus gaia/fauna_sanga.png - 300 + 300 5 4.5 fauna/sanga_cattle.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_zebu.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_zebu.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_cattle_zebu.xml (revision 24963) @@ -1,31 +1,31 @@ 60 150 3.5 150 Zebu Bos taurus indicus gaia/fauna_zebu.png - 300 + 300 5 4.5 fauna/zebu_wild.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 24963) @@ -1,49 +1,49 @@ 1.5 5 Chicken Gallus gallus domesticus gaia/fauna_chicken.png false upright - 40 + 40 5 actor/fauna/animal/chickens.xml actor/fauna/animal/chickens.xml 2.5 4.0 12.0 2000 8000 10000 40000 0.15 fauna/chicken.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_donkey.xml (revision 24963) @@ -1,35 +1,35 @@ 2.5 50 Donkey Equus africanus asinus gaia/fauna_donkey.png - 200 + 200 actor/fauna/animal/horse_order.xml actor/fauna/animal/horse_death.xml actor/fauna/animal/horse_trained.xml 3.5 0.8 fauna/donkey.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_african_bush.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_african_bush.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_african_bush.xml (revision 24963) @@ -1,27 +1,27 @@ 9.0 300 African Bush Elephant Loxodonta africana gaia/fauna_elephant_african_bush.png 75 - 800 + 800 10.0 fauna/elephant_african_bush.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_asian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_asian.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_asian.xml (revision 24963) @@ -1,27 +1,27 @@ 7.0 250 Asian Elephant Elephas maximus gaia/fauna_elephant_asian.png 60 - 650 + 650 8.0 fauna/elephant_asian.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml (revision 24963) @@ -1,27 +1,27 @@ 6.5 200 African Forest Elephant Loxodonta cyclotis gaia/fauna_elephant_north_african.png 50 - 500 + 500 7.5 fauna/elephant_african_forest.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml (revision 24963) @@ -1,27 +1,27 @@ 13.0 80 Giraffe Giraffa camelopardalis gaia/fauna_giraffe.png - 350 + 350 14.0 0.6 fauna/giraffe_adult.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml (revision 24963) @@ -1,27 +1,27 @@ 7.5 40 Juvenile Giraffe Giraffa camelopardalis gaia/fauna_giraffe.png - 150 + 150 8.5 0.6 fauna/giraffe_baby.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_goat.xml (revision 24963) @@ -1,42 +1,42 @@ 30 35 2.0 35 Goat Capra aegagrus hircus gaia/fauna_goat.png - 70 + 70 2 actor/fauna/animal/goat_order.xml actor/fauna/death/goat.xml actor/fauna/animal/goat_trained.xml 3.0 0.45 fauna/goat.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_hippopotamus.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_hippopotamus.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_hippopotamus.xml (revision 24963) @@ -1,44 +1,44 @@ Tusks 12 12 3 3.5 200 Hippopotamus Hippopotamus amphibius gaia/fauna_hippopotamus.png 50 - 400 + 400 actor/fauna/animal/lion_attack.xml actor/fauna/animal/lion_death.xml 4.5 fauna/hippopotamus.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml (revision 24963) @@ -1,35 +1,35 @@ 4.0 50 Horse Equus ferus caballus gaia/fauna_horse.png - 200 + 200 actor/fauna/animal/horse_order.xml actor/fauna/animal/horse_death.xml actor/fauna/animal/horse_trained.xml 5.0 0.8 fauna/horse.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_muskox.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_muskox.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_muskox.xml (revision 24963) @@ -1,24 +1,24 @@ 3.5 50 Muskox Ovibos moschatus gaia/fauna_muskox.png - 200 + 200 4.5 fauna/muskox.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml (revision 24963) @@ -1,47 +1,47 @@ 1.5 10 Peacock Pavo cristatus gaia/fauna_peacock.png upright - 50 + 50 5 actor/fauna/animal/peacock_order.xml actor/fauna/animal/peacock_call.xml actor/fauna/animal/peacock_trained.xml 2.5 4.0 12.0 2000 8000 10000 40000 0.3 fauna/peacock.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_pig.xml (revision 24963) @@ -1,42 +1,42 @@ 50 75 1.5 75 Pig Sus scrofa domesticus gaia/fauna_pig.png - 150 + 150 4 actor/fauna/animal/pig_order.xml actor/fauna/animal/pig.xml actor/fauna/animal/pig_trained.xml 2.5 0.45 fauna/pig1.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_piglet.xml (revision 24963) @@ -1,26 +1,26 @@ 0.5 15 Piglet - 10 + 10 1 1.5 0.25 fauna/piglet.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rabbit.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rabbit.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_rabbit.xml (revision 24963) @@ -1,27 +1,27 @@ 1.0 5 Rabbit Oryctolagus cuniculus gaia/fauna_rabbit.png false - 50 + 50 2.0 fauna/rabbit1.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_sheep.xml (revision 24963) @@ -1,42 +1,42 @@ 40 50 2.0 50 Sheep Ovis aries gaia/fauna_sheep.png - 100 + 100 3 actor/fauna/animal/sheep_order.xml actor/fauna/animal/sheep.xml actor/fauna/animal/sheep_trained.xml 3.0 0.45 fauna/sheep3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml (revision 24963) @@ -1,27 +1,27 @@ 3.0 50 Blue Wildebeest Connochaetes taurinus gaia/fauna_wildebeest.png - 150 + 150 4.0 0.9 fauna/wildebeest.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/apple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/apple.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/apple.xml (revision 24963) @@ -1,12 +1,12 @@ Apple - 400 + 400 flora/trees/apple_bloom.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_01.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_01.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_01.xml (revision 24963) @@ -1,12 +1,12 @@ Berries - 200 + 200 props/flora/berry_bush.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_04.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_04.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/berry_04.xml (revision 24963) @@ -1,12 +1,12 @@ Berries - 200 + 200 props/flora/berry_bush_autumn_01.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/fig.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/fig.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fruit/fig.xml (revision 24963) @@ -1,18 +1,18 @@ Fig - 500 + 500 flora/trees/fig.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/column_doric.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/column_doric.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/column_doric.xml (revision 24963) @@ -1,17 +1,17 @@ 2.5 Ancient Ruins gaia/special_fence.png - 500 + 500 props/special/eyecandy/column_doric_fallen.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/standing_stone.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/standing_stone.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/standing_stone.xml (revision 24963) @@ -1,20 +1,20 @@ 2.0 Celtic Standing Stone gaia/special_treasure.png - 300 + 300 props/special/eyecandy/standing_stones.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrel.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrel.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrel.xml (revision 24963) @@ -1,26 +1,26 @@ 2.5 Food Treasure gaia/special_treasure_food.png - 100 + 100 treasure.food 128x128/ellipse.png 128x128/ellipse_mask.png props/special/eyecandy/barrel_a.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/unfinished_greek_temple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/unfinished_greek_temple.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/unfinished_greek_temple.xml (revision 24963) @@ -1,29 +1,29 @@ 12.0 Unfinished Greek Temple structures/temple.png - 2000 + 2000 30 6.0 0.6 12.0 40 props/special/eyecandy/greek_temple_unfinished.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_crate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_crate.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_crate.xml (revision 24963) @@ -1,21 +1,21 @@ 12.0 Food Treasure gaia/special_treasure_food.png - 200 + 200 treasure.food props/special/eyecandy/crate_a.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_roman.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_roman.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/ruins/stone_statues_roman.xml (revision 24963) @@ -1,25 +1,25 @@ 12.0 Roman Statues structures/statue.png - 300 + 300 props/special/eyecandy/statues_roman.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_bin.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_bin.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_bin.xml (revision 24963) @@ -1,21 +1,21 @@ 12.0 Food Treasure gaia/special_treasure_food.png - 300 + 300 treasure.food props/special/eyecandy/produce_bin_a.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrels_buried.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrels_buried.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_barrels_buried.xml (revision 24963) @@ -1,25 +1,25 @@ 2.5 Half-buried Barrels gaia/special_treasure_food.png false 0.0 - 200 + 200 treasure.food props/special/eyecandy/barrels_buried.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_jars.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_jars.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_jars.xml (revision 24963) @@ -1,21 +1,21 @@ 2.0 Food Treasure gaia/special_treasure_food.png - 300 + 300 treasure.food props/special/eyecandy/amphorae.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal.xml (revision 24963) @@ -1,22 +1,22 @@ 2.5 Secret Box - 300 + 300 treasure.metal props/special/eyecandy/barrel_a.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck.xml (revision 24963) @@ -1,24 +1,24 @@ 9.0 Shipwreck true 0.0 - 500 + 500 treasure.wood props/special/eyecandy/shipwreck_ram_side.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat_cut.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat_cut.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat_cut.xml (revision 24963) @@ -1,24 +1,24 @@ 9.0 Shipwreck true 0.0 - 450 + 450 treasure.wood props/special/eyecandy/shipwreck_sail_boat_cut.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/acacia.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/acacia.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/acacia.xml (revision 24963) @@ -1,12 +1,12 @@ Acacia - 200 + 200 flora/trees/acacia.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_single.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_single.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_single.xml (revision 24963) @@ -1,13 +1,13 @@ Bamboo - 100 + 100 1 flora/trees/bamboo_single.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_2_young.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_2_young.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_2_young.xml (revision 24963) @@ -1,12 +1,12 @@ Young Baobab - 200 + 200 flora/trees/baobab_new_young.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate.xml (revision 24963) @@ -1,12 +1,12 @@ Bush - 50 + 50 flora/trees/temperate_bush_biome.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_1_sapling.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_1_sapling.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_1_sapling.xml (revision 24963) @@ -1,12 +1,12 @@ Atlas Cedar Sapling - 50 + 50 flora/trees/cedar_atlas_sapling.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/golden_fleece.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/golden_fleece.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/golden_fleece.xml (revision 24963) @@ -1,17 +1,17 @@ 2.5 Golden Fleece - 1000 + 1000 treasure.metal special/golden_fleece.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/pegasus.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/pegasus.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/pegasus.xml (revision 24963) @@ -1,17 +1,17 @@ 2.0 Pegasus - 1000 + 1000 treasure.metal special/pegasus.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_sail_boat.xml (revision 24963) @@ -1,24 +1,24 @@ 9.0 Shipwreck true 0.0 - 400 + 400 treasure.wood props/special/eyecandy/shipwreck_sail_boat.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/wood.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/wood.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/wood.xml (revision 24963) @@ -1,25 +1,25 @@ 12.0 Wood Treasure - 300 + 300 treasure.wood props/special/eyecandy/wood_pile.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_dragon.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_dragon.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo_dragon.xml (revision 24963) @@ -1,20 +1,20 @@ 15.0 Dragon Bamboo - 1000 + 1000 12 flora/trees/bamboo_dragon.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_1_sapling.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_1_sapling.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_1_sapling.xml (revision 24963) @@ -1,13 +1,13 @@ Baobab Sapling - 50 + 50 2 flora/trees/baobab_new_sapling.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_badlands.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_badlands.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_badlands.xml (revision 24963) @@ -1,12 +1,12 @@ Hardy Bush - 75 + 75 props/flora/bush_tempe_a.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/carob.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/carob.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/carob.xml (revision 24963) @@ -1,12 +1,12 @@ Carob - 200 + 200 flora/trees/carob.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_4_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_4_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_4_dead.xml (revision 24963) @@ -1,12 +1,12 @@ Atlas Cedar - 300 + 300 flora/trees/cedar_atlas_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_small.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_small.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_small.xml (revision 24963) @@ -1,21 +1,21 @@ 12.0 Persian Food Treasure gaia/special_treasure_food.png - 400 + 400 treasure.food props/special/eyecandy/treasure_persian_food_small.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_small.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_small.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_small.xml (revision 24963) @@ -1,25 +1,25 @@ 12.0 Persian Rugs - 300 + 300 treasure.metal props/special/eyecandy/treasure_persian_metal_small.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_ram_bow.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_ram_bow.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_ram_bow.xml (revision 24963) @@ -1,24 +1,24 @@ 9.0 Shipwreck true 0.0 - 550 + 550 treasure.wood props/special/eyecandy/shipwreck_ram_bow.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/stone.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/stone.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/stone.xml (revision 24963) @@ -1,25 +1,25 @@ 2.0 Stone Treasure - 300 + 300 treasure.stone props/special/eyecandy/stone_pile.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bamboo.xml (revision 24963) @@ -1,12 +1,12 @@ Bamboo - 200 + 200 flora/trees/bamboo.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab.xml (revision 24963) @@ -1,20 +1,20 @@ 12.0 Baobab - 400 + 400 9 flora/trees/baobab.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_4_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_4_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_4_dead.xml (revision 24963) @@ -1,20 +1,20 @@ 15.0 Baobab - 550 + 550 12 flora/trees/baobab_new_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_tropic.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_tropic.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_tropic.xml (revision 24963) @@ -1,12 +1,12 @@ Bush - 50 + 50 flora/trees/tropic_bush_biome.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_3_mature.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_3_mature.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_3_mature.xml (revision 24963) @@ -1,12 +1,12 @@ Atlas Cedar - 350 + 350 flora/trees/cedar_atlas.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_big.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_big.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/food_persian_big.xml (revision 24963) @@ -1,21 +1,21 @@ 2.0 Persian Food Stores gaia/special_treasure_food.png - 600 + 600 treasure.food props/special/eyecandy/treasure_persian_food_big.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_big.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_big.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/metal_persian_big.xml (revision 24963) @@ -1,25 +1,25 @@ 12.0 Persian Wares - 500 + 500 treasure.metal props/special/eyecandy/treasure_persian_metal_big.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_debris.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_debris.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/shipwreck_debris.xml (revision 24963) @@ -1,27 +1,27 @@ 2.5 Shipwreck Cargo false false -0.1 true 0.0 - 200 + 200 treasure.food props/special/eyecandy/barrels_floating.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/standing_stone.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/standing_stone.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/treasure/standing_stone.xml (revision 24963) @@ -1,25 +1,25 @@ 2.0 Celtic Standing Stone - 300 + 300 treasure.stone props/special/eyecandy/standing_stones.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/aleppo_pine.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/aleppo_pine.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/aleppo_pine.xml (revision 24963) @@ -1,12 +1,12 @@ Aleppo Pine - 200 + 200 flora/trees/aleppo_pine.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/banyan.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/banyan.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/banyan.xml (revision 24963) @@ -1,20 +1,20 @@ 15.0 Banyan - 600 + 600 12 flora/trees/banyan.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_3_mature.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_3_mature.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/baobab_3_mature.xml (revision 24963) @@ -1,20 +1,20 @@ 15.0 Baobab - 600 + 600 12 flora/trees/baobab_new.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate_winter.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate_winter.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/bush_temperate_winter.xml (revision 24963) @@ -1,12 +1,12 @@ Bush - 50 + 50 flora/trees/temperate_bush_biome_winter.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_2_young.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_2_young.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cedar_atlas_2_young.xml (revision 24963) @@ -1,12 +1,12 @@ Atlas Cedar - 200 + 200 flora/trees/cedar_atlas_young.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_patch.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_patch.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_patch.xml (revision 24963) @@ -1,20 +1,20 @@ 12.0 Cretan Date Palm - 300 + 300 12 flora/trees/palm_cretan_date_patch.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_wild.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_wild.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_wild.xml (revision 24963) @@ -1,12 +1,12 @@ Cypress - 200 + 200 flora/trees/cypress_mediterranean_wild.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress.xml (revision 24963) @@ -1,12 +1,12 @@ Cypress - 200 + 200 flora/trees/mediterranean_cypress.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_tall.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_tall.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_tall.xml (revision 24963) @@ -1,12 +1,12 @@ Cretan Date Palm - 200 + 200 flora/trees/palm_cretan_date_tall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_short.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_short.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cretan_date_palm_short.xml (revision 24963) @@ -1,12 +1,12 @@ Cretan Date Palm - 100 + 100 flora/trees/palm_cretan_date_short.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_windswept.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_windswept.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/cypress_windswept.xml (revision 24963) @@ -1,12 +1,12 @@ Cypress - 200 + 200 flora/trees/cypress_mediterranean_windswept.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/dead.xml (revision 24963) @@ -1,12 +1,12 @@ Deciduous Tree - 200 + 200 flora/trees/temperate_dead_forest.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech_aut.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech_aut.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech_aut.xml (revision 24963) @@ -1,12 +1,12 @@ European Beech - 200 + 200 flora/trees/european_beech_aut.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_winter.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_winter.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_winter.xml (revision 24963) @@ -1,12 +1,12 @@ Fir - 200 + 200 flora/trees/fir_tree_winter.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/medit_fan_palm.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/medit_fan_palm.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/medit_fan_palm.xml (revision 24963) @@ -1,12 +1,12 @@ Mediterranean Fan Palm - 200 + 200 flora/trees/palm_medit_fan_new.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_dead.xml (revision 24963) @@ -1,12 +1,12 @@ Oak - 200 + 200 flora/trees/oak_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_large.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_large.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_large.xml (revision 24963) @@ -1,12 +1,12 @@ Large Oak - 300 + 300 flora/trees/oak_large.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_palmyra.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_palmyra.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_palmyra.xml (revision 24963) @@ -1,12 +1,12 @@ Palmyra Palm - 200 + 200 flora/trees/palm_palmyra.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black.xml (revision 24963) @@ -1,12 +1,12 @@ Black Pine - 200 + 200 flora/trees/pine_black.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_w.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_w.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_w.xml (revision 24963) @@ -1,12 +1,12 @@ Pine - 200 + 200 flora/trees/pine_w.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm_dead.xml (revision 24963) @@ -1,12 +1,12 @@ Date Palm - 200 + 200 flora/trees/palm_date_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_beech.xml (revision 24963) @@ -1,12 +1,12 @@ European Beech - 200 + 200 flora/trees/european_beech.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_sapling.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_sapling.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir_sapling.xml (revision 24963) @@ -1,12 +1,12 @@ Fir Sapling - 50 + 50 flora/trees/fir_sapling.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/maple.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/maple.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/maple.xml (revision 24963) @@ -1,12 +1,12 @@ Maple - 300 + 300 flora/trees/temperate_maple_trees.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut_new.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut_new.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut_new.xml (revision 24963) @@ -1,12 +1,12 @@ Oak - 200 + 200 flora/trees/oak_new_aut.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian_autumn.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian_autumn.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian_autumn.xml (revision 24963) @@ -1,12 +1,12 @@ Hungarian Oak - 200 + 200 flora/trees/oak_hungarian_autumn.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_doum.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_doum.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_doum.xml (revision 24963) @@ -1,12 +1,12 @@ Doum Palm - 200 + 200 flora/trees/palm_doum.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine.xml (revision 24963) @@ -1,12 +1,12 @@ Pine - 200 + 200 flora/trees/pine.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime_short.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime_short.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime_short.xml (revision 24963) @@ -1,12 +1,12 @@ Maritime Pine - 200 + 200 flora/trees/pine_maritime_short.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy.xml (revision 24963) @@ -1,12 +1,12 @@ Black Poplar - 200 + 200 flora/trees/poplar_lombardy.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/date_palm.xml (revision 24963) @@ -1,12 +1,12 @@ Date Palm - 200 + 200 flora/trees/palm_date_new.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm_dead.xml (revision 24963) @@ -1,12 +1,12 @@ Elm - 200 + 200 flora/trees/elm_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/fir.xml (revision 24963) @@ -1,12 +1,12 @@ Fir - 200 + 200 flora/trees/fir_tree.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/mangrove.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/mangrove.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/mangrove.xml (revision 24963) @@ -1,12 +1,12 @@ Mangrove - 200 + 200 flora/trees/mangrove.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_aut.xml (revision 24963) @@ -1,12 +1,12 @@ Oak - 200 + 200 flora/trees/oak_aut.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_hungarian.xml (revision 24963) @@ -1,12 +1,12 @@ Hungarian Oak - 200 + 200 flora/trees/oak_hungarian.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_areca.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_areca.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_areca.xml (revision 24963) @@ -1,12 +1,12 @@ Areca Palm - 200 + 200 flora/trees/palm_areca.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropical.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropical.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropical.xml (revision 24963) @@ -1,12 +1,12 @@ Tropical Palm - 200 + 200 flora/trees/palm_tropical.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_maritime.xml (revision 24963) @@ -1,12 +1,12 @@ Maritime Pine - 200 + 200 flora/trees/pine_maritime.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_dead.xml (revision 24963) @@ -1,12 +1,12 @@ Poplar - 200 + 200 flora/trees/poplar_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/elm.xml (revision 24963) @@ -1,12 +1,12 @@ Elm - 200 + 200 flora/trees/elm.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_birch.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_birch.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/euro_birch.xml (revision 24963) @@ -1,12 +1,12 @@ Silver Birch - 300 + 300 flora/trees/euro_birch_tree.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/juniper_prickly.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/juniper_prickly.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/juniper_prickly.xml (revision 24963) @@ -1,12 +1,12 @@ Prickly Juniper - 200 + 200 flora/trees/juniper_prickly.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak.xml (revision 24963) @@ -1,12 +1,12 @@ Oak - 200 + 200 flora/trees/oak.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_holly.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_holly.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_holly.xml (revision 24963) @@ -1,12 +1,12 @@ Holly Oak - 200 + 200 flora/trees/oak_holly.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_new.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_new.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/oak_new.xml (revision 24963) @@ -1,12 +1,12 @@ Oak - 200 + 200 flora/trees/oak_new.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropic.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropic.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/palm_tropic.xml (revision 24963) @@ -1,12 +1,12 @@ Palm - 200 + 200 flora/trees/palm_tropical_tall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/pine_black_dead.xml (revision 24963) @@ -1,12 +1,12 @@ Black Pine - 200 + 200 flora/trees/pine_black_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar.xml (revision 24963) @@ -1,12 +1,12 @@ Poplar - 200 + 200 flora/trees/poplar.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_autumn.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_autumn.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_autumn.xml (revision 24963) @@ -1,12 +1,12 @@ Poplar - 200 + 200 flora/trees/poplar_autumn.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tamarix.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tamarix.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tamarix.xml (revision 24963) @@ -1,12 +1,12 @@ Tamarix - 200 + 200 flora/trees/tamarix.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/strangler.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/strangler.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/strangler.xml (revision 24963) @@ -1,20 +1,20 @@ 15.0 Strangler Fig - 500 + 500 10 flora/trees/strangler.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/senegal_date_palm.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/senegal_date_palm.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/senegal_date_palm.xml (revision 24963) @@ -1,12 +1,12 @@ Senegal Date Palm - 200 + 200 flora/trees/palm_senegal_date.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_dead.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_dead.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/poplar_lombardy_dead.xml (revision 24963) @@ -1,12 +1,12 @@ Black Poplar - 200 + 200 flora/trees/poplar_lombardy_dead.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/teak.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/teak.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/teak.xml (revision 24963) @@ -1,19 +1,19 @@ 15.0 Teak - 500 + 500 flora/trees/teak.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_winter.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_winter.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_winter.xml (revision 24963) @@ -1,12 +1,12 @@ Deciduous Tree - 200 + 200 flora/trees/temperate_forest_biome_tree_winter.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fruit.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fruit.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fruit.xml (revision 24963) @@ -1,42 +1,42 @@ 3.0 Fruit Pick fruit for food. gaia/flora_bush_berry.png food true false - 1 + 1 food.fruit 8 3.0 0.5 4.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock_large.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock_large.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock_large.xml (revision 24963) @@ -1,18 +1,18 @@ 3.5 - 5000 + 5000 24 5.0 0.5 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_corral.xml (revision 24963) @@ -1,60 +1,63 @@ + + structures/corral + 50 100 5.0 500 decay|rubble/rubble_stone_3x3 Corral template_structure_resource_corral Raise Domestic Animals for food. Economic Village Corral structures/corral.png phase_village 20 0.7 gaia/fauna_goat_trainable gaia/fauna_sheep_trainable gaia/fauna_pig_trainable gaia/fauna_cattle_cow_trainable gather_animals_stockbreeding interface/complete/building/complete_corral.xml false 20 30000 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive_bear.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive_bear.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_aggressive_bear.xml (revision 24963) @@ -1,25 +1,25 @@ Paws 20 0 20 6 2000 - 300 + 300 actor/fauna/animal/lion_attack.xml actor/fauna/animal/lion_death.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_autumn.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_autumn.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate_autumn.xml (revision 24963) @@ -1,12 +1,12 @@ Deciduous Tree - 200 + 200 flora/trees/temperate_forest_biome_tree_autumn.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fish.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fish.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_fish.xml (revision 24963) @@ -1,45 +1,45 @@ 2.5 Fish SeaCreature Catch fish for food. food false false false true -2.0 true false - 1000 + 1000 food.fish 4 0.2 4.0 0.666 5.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_rock.xml (revision 24963) @@ -1,33 +1,33 @@ 3.5 Stone Quarry Quarry rock for stone. gaia/geology_stone_1.png stone pitch-roll false - 1000 + 1000 stone.rock 12 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_tree.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_tree.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_tree.xml (revision 24963) @@ -1,51 +1,51 @@ 15.0 Tree Chop trees for wood. gaia/flora_tree_generic.png wood true false - 1 + 1 wood.tree 8 128x128/ellipse.png 128x128/ellipse_mask.png 3.0 0.5 10.0 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt.xml (revision 24963) @@ -1,15 +1,15 @@ Kill to gather meat for food. 10 true - 100 + 100 food.meat 8 Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/temperate.xml (revision 24963) @@ -1,12 +1,12 @@ Deciduous Tree - 200 + 200 flora/trees/temperate_forest_biome_tree.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tropic_rainforest.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tropic_rainforest.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/tropic_rainforest.xml (revision 24963) @@ -1,12 +1,12 @@ Rainforest Tree - 200 + 200 flora/trees/tropic_forest_biome_tree.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore_large.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore_large.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore_large.xml (revision 24963) @@ -1,18 +1,18 @@ 3.5 - 5000 + 5000 24 5.0 0.5 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_treasure.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_treasure.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_treasure.xml (revision 24963) @@ -1,39 +1,39 @@ 12.0 Treasure Collect treasures for resources. gaia/special_treasure.png metal false - 300 + 300 1 0.3 3.75 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_herd.xml (revision 24963) @@ -1,15 +1,15 @@ Kill to gather meat for food. true - 100 + 100 food.meat 8 20 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml (revision 24963) @@ -1,57 +1,57 @@ true Kill to butcher for food. 10 false false upright true 0.0 true - 2000 + 2000 food.fish 5 128x512/ellipse.png 128x512/ellipse_mask.png 4.0 0.666 5.0 skittish 60.0 60.0 100000 300000 1 2 0 ship-small 1.8 1 Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/toona.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/toona.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/tree/toona.xml (revision 24963) @@ -1,12 +1,12 @@ Toona - 200 + 200 flora/trees/tree_tropic.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ore.xml (revision 24963) @@ -1,33 +1,33 @@ 3.5 Metal Mine Mine ore for metal. gaia/geology_metal.png metal pitch-roll false - 1000 + 1000 metal.ore 12 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ruins.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ruins.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_gaia_ruins.xml (revision 24963) @@ -1,32 +1,32 @@ 2.5 Ruins Demolish ruins for stone. gaia/geology_stone_1.png stone false - 500 + 500 stone.ruins 1 interface/complete/building/complete_universal.xml attack/destruction/building_collapse_large.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 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_resource_field.xml (revision 24963) @@ -1,74 +1,74 @@ 50 100 2.0 250 decay|rubble/rubble_field Field template_structure_resource_field Field Harvest grain for food. Each subsequent gatherer works less efficiently. structures/field.png 50 false false 15 40 5 false - Infinity + Infinity food.grain 5 0.90 interface/complete/building/complete_field.xml 8.0 5.0 0 structures/plot_field_found.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish_elephant_infant.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish_elephant_infant.xml (revision 24962) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_skittish_elephant_infant.xml (revision 24963) @@ -1,17 +1,17 @@ Elephant - 100 + 100 actor/fauna/animal/elephant_order.xml actor/fauna/animal/elephant_death.xml actor/fauna/animal/elephant_trained.xml