Index: ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js (revision 25087) @@ -1,169 +1,169 @@ function AttackDetection() {} AttackDetection.prototype.Schema = "Detects incoming attacks." + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; AttackDetection.prototype.Init = function() { this.suppressionTime = +this.template.SuppressionTime; // Use squared distance to avoid sqrts this.suppressionTransferRangeSquared = +this.template.SuppressionTransferRange * +this.template.SuppressionTransferRange; this.suppressionRangeSquared = +this.template.SuppressionRange * +this.template.SuppressionRange; this.suppressedList = []; }; AttackDetection.prototype.ActivateTimer = function() { Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetTimeout(this.entity, IID_AttackDetection, "HandleTimeout", this.suppressionTime); }; AttackDetection.prototype.AddSuppression = function(event) { this.suppressedList.push(event); this.ActivateTimer(); }; AttackDetection.prototype.UpdateSuppressionEvent = function(index, event) { this.suppressedList[index] = event; this.ActivateTimer(); }; -//// Message handlers //// +// Message handlers AttackDetection.prototype.OnGlobalAttacked = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); var cmpOwnership = Engine.QueryInterface(msg.target, IID_Ownership); if (cmpOwnership.GetOwner() != cmpPlayer.GetPlayerID()) return; Engine.PostMessage(msg.target, MT_MinimapPing); this.AttackAlert(msg.target, msg.attacker, msg.type, msg.attackerOwner); }; -//// External interface //// +// External interface AttackDetection.prototype.AttackAlert = function(target, attacker, type, attackerOwner) { let playerID = Engine.QueryInterface(this.entity, IID_Player).GetPlayerID(); // Don't register attacks dealt against other players if (Engine.QueryInterface(target, IID_Ownership).GetOwner() != playerID) return; let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership); let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner; // Don't register attacks dealt by myself if (atkOwner == playerID) return; // Since livestock can be attacked/gathered by other players // and generally are not so valuable as other units/buildings, // we have a lower priority notification for it, which can be // overriden by a regular one. var cmpTargetIdentity = Engine.QueryInterface(target, IID_Identity); var targetIsDomesticAnimal = cmpTargetIdentity && cmpTargetIdentity.HasClass("Animal") && cmpTargetIdentity.HasClass("Domestic"); var cmpPosition = Engine.QueryInterface(target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var event = { "target": target, "position": cmpPosition.GetPosition(), "time": Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(), "targetIsDomesticAnimal": targetIsDomesticAnimal }; // If we already have a low priority livestock event in suppressed list, // and now a more important target is attacked, we want to upgrade the // suppressed event and send the new notification var isPriorityIncreased = false; for (var i = 0; i < this.suppressedList.length; ++i) { var element = this.suppressedList[i]; // If the new attack is within suppression distance of this element, // then check if the element should be updated and return var dist = event.position.horizDistanceToSquared(element.position); if (dist >= this.suppressionRangeSquared) continue; isPriorityIncreased = element.targetIsDomesticAnimal && !targetIsDomesticAnimal; var isPriorityDescreased = !element.targetIsDomesticAnimal && targetIsDomesticAnimal; - if (isPriorityIncreased - || (!isPriorityDescreased && dist < this.suppressionTransferRangeSquared)) + if (isPriorityIncreased || + (!isPriorityDescreased && dist < this.suppressionTransferRangeSquared)) this.UpdateSuppressionEvent(i, event); // If priority has increased, exit the loop to send the upgraded notification below if (isPriorityIncreased) break; return; } // If priority has increased for an existing event, then we already have it // in the suppression list if (!isPriorityIncreased) this.AddSuppression(event); Engine.PostMessage(this.entity, MT_AttackDetected, { "player": playerID, "event": event }); Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "attack", "target": target, "players": [playerID], "attacker": atkOwner, "position": event.position, "targetIsDomesticAnimal": targetIsDomesticAnimal }); let soundGroup = "attacked"; if (type == "capture") soundGroup += "_capture"; if (attackerOwner === 0) soundGroup += "_gaia"; PlaySound(soundGroup, target); }; AttackDetection.prototype.GetSuppressionTime = function() { return this.suppressionTime; }; AttackDetection.prototype.HandleTimeout = function() { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var now = cmpTimer.GetTime(); for (var i = 0; i < this.suppressedList.length; ++i) { var event = this.suppressedList[i]; // Check if this event has timed out if (now - event.time >= this.suppressionTime) { this.suppressedList.splice(i, 1); return; } } }; AttackDetection.prototype.GetIncomingAttacks = function() { return this.suppressedList; }; Engine.RegisterComponentType(IID_AttackDetection, "AttackDetection", AttackDetection); Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 25087) @@ -1,330 +1,330 @@ function BuildRestrictions() {} BuildRestrictions.prototype.Schema = "Specifies building placement restrictions as they relate to terrain, territories, and distance." + "" + "" + "land" + "own" + "Structure" + "" + "CivilCentre" + "40" + "" + "" + "" + "" + "" + "land" + "shore" + "land-shore"+ "" + "" + "" + "" + "" + "" + "own" + "ally" + "neutral" + "enemy" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; BuildRestrictions.prototype.Init = function() { }; /** * Checks whether building placement is valid * 1. Visibility is not hidden (may be fogged or visible) * 2. Check foundation * a. Doesn't obstruct foundation-blocking entities * b. On valid terrain, based on passability class * 3. Territory type is allowed (see note below) * 4. Dock is on shoreline and facing into water * 5. Distance constraints satisfied * * Returns result object: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the GUI message * "translateMessage": always true * "translateParameters": list of parameters to translate * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": plural translation argument (optional) * } * * Note: The entity which is used to check this should be a preview entity * (template name should be "preview|"+templateName), as otherwise territory * checks for buildings with territory influence will not work as expected. */ BuildRestrictions.prototype.CheckPlacement = function() { var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building"; var result = { "success": false, "message": markForTranslation("%(name)s cannot be built due to unknown error"), "parameters": { "name": name, }, "translateMessage": true, "translateParameters": ["name"], }; var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (!cmpPlayer) return result; // Fail // TODO: AI has no visibility info if (!cmpPlayer.IsAI()) { // Check whether it's in a visible or fogged region var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpRangeManager || !cmpOwnership) return result; // Fail var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden"); if (!explored) { result.message = markForTranslation("%(name)s cannot be built in unexplored area"); return result; // Fail } } // Check obstructions and terrain passability var passClassName = ""; switch (this.template.PlacementType) { case "shore": passClassName = "building-shore"; break; case "land-shore": // 'default-terrain-only' is everywhere a normal unit can go, ignoring // obstructions (i.e. on passable land, and not too deep in the water) passClassName = "default-terrain-only"; break; case "land": default: passClassName = "building-land"; } var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return result; // Fail if (this.template.Category == "Wall") { // for walls, only test the center point var ret = cmpObstruction.CheckFoundation(passClassName, true); } else { var ret = cmpObstruction.CheckFoundation(passClassName, false); } if (ret != "success") { switch (ret) { case "fail_error": case "fail_no_obstruction": error("CheckPlacement: Error returned from CheckFoundation"); break; case "fail_obstructs_foundation": result.message = markForTranslation("%(name)s cannot be built on another building or resource"); break; case "fail_terrain_class": // TODO: be more specific and/or list valid terrain? result.message = markForTranslation("%(name)s cannot be built on invalid terrain"); } return result; // Fail } // Check territory restrictions var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpTerritoryManager || !cmpPosition || !cmpPosition.IsInWorld()) return result; // Fail var pos = cmpPosition.GetPosition2D(); var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); var isConnected = !cmpTerritoryManager.IsTerritoryBlinking(pos.x, pos.y); var isOwn = tileOwner == cmpPlayer.GetPlayerID(); var isMutualAlly = cmpPlayer.IsExclusiveMutualAlly(tileOwner); var isNeutral = tileOwner == 0; var invalidTerritory = ""; if (isOwn) { if (!this.HasTerritory("own")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "own"); else if (!isConnected && !this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own"); } else if (isMutualAlly) { if (!this.HasTerritory("ally")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "allied"); else if (!isConnected && !this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied"); } else if (isNeutral) { if (!this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "neutral"); } else { // consider everything else enemy territory if (!this.HasTerritory("enemy")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "enemy"); } if (invalidTerritory) { result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s"); result.translateParameters.push("territoryType"); result.translateParameters.push("validTerritories"); - result.parameters.territoryType = {"context": "Territory type", "message": invalidTerritory}; + result.parameters.territoryType = { "context": "Territory type", "message": invalidTerritory }; // gui code will join this array to a string - result.parameters.validTerritories = {"context": "Territory type list", "list": this.GetTerritories()}; + result.parameters.validTerritories = { "context": "Territory type list", "list": this.GetTerritories() }; return result; // Fail } // Check special requirements if (this.template.PlacementType == "shore") { if (!cmpObstruction.CheckShorePlacement()) { result.message = markForTranslation("%(name)s must be built on a valid shoreline"); return result; // Fail } } let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); let template = cmpTemplateManager.GetTemplate(removeFiltersFromTemplateName(templateName)); // Check distance restriction if (this.template.Distance) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cat = this.template.Distance.FromClass; var filter = function(id) { var cmpIdentity = Engine.QueryInterface(id, IID_Identity); return cmpIdentity.GetClassesList().indexOf(cat) > -1; }; if (this.template.Distance.MinDistance !== undefined) { let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template); if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) { let result = markForPluralTranslation( "%(name)s too close to a %(category)s, must be at least %(distance)s meter away", "%(name)s too close to a %(category)s, must be at least %(distance)s meters away", minDistance); result.success = false; result.translateMessage = true; result.parameters = { "name": name, "category": cat, "distance": minDistance }; result.translateParameters = ["name", "category"]; return result; // Fail } } if (this.template.Distance.MaxDistance !== undefined) { let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template); if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions, false).some(filter)) { let result = markForPluralTranslation( "%(name)s too far from a %(category)s, must be within %(distance)s meter", "%(name)s too far from a %(category)s, must be within %(distance)s meters", maxDistance); result.success = false; result.translateMessage = true; result.parameters = { "name": name, "category": cat, "distance": maxDistance }; result.translateParameters = ["name", "category"]; return result; // Fail } } } // Success result.success = true; result.message = ""; return result; }; BuildRestrictions.prototype.GetCategory = function() { return this.template.Category; }; BuildRestrictions.prototype.GetTerritories = function() { return ApplyValueModificationsToEntity("BuildRestrictions/Territory", this.template.Territory, this.entity).split(/\s+/); }; BuildRestrictions.prototype.HasTerritory = function(territory) { return (this.GetTerritories().indexOf(territory) != -1); }; // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "own"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "ally"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "neutral"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "enemy"); Engine.RegisterComponentType(IID_BuildRestrictions, "BuildRestrictions", BuildRestrictions); Index: ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 25087) @@ -1,372 +1,372 @@ function Capturable() {} Capturable.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + ""; Capturable.prototype.Init = function() { this.maxCapturePoints = +this.template.CapturePoints; this.garrisonRegenRate = +this.template.GarrisonRegenRate; this.regenRate = +this.template.RegenRate; this.capturePoints = []; }; -//// Interface functions //// +// Interface functions /** * Returns the current capture points array. */ Capturable.prototype.GetCapturePoints = function() { return this.capturePoints; }; Capturable.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; }; Capturable.prototype.GetGarrisonRegenRate = function() { return this.garrisonRegenRate; }; /** * Set the new capture points, used for cloning entities. * The caller should assure that the sum of capture points * matches the max. * @param {number[]} - Array with for all players the new value. */ Capturable.prototype.SetCapturePoints = function(capturePointsArray) { this.capturePoints = capturePointsArray; }; /** * Compute the amount of capture points to be reduced and reduce them. * @param {number} amount - Number of capture points to be taken. * @param {number} captor - The entity capturing us. * @param {number} captorOwner - Owner of the captor. * @return {Object} - Object of the form { "captureChange": number }, where number indicates the actual amount of capture points taken. */ Capturable.prototype.Capture = function(amount, captor, captorOwner) { if (captorOwner == INVALID_PLAYER || !this.CanCapture(captorOwner)) return {}; // TODO: implement loot return { "captureChange": this.Reduce(amount, captorOwner) }; }; /** * Reduces the amount of capture points of an entity, * in favour of the player of the source. * @param {number} amount - Number of capture points to be taken. * @param {number} playerID - ID of player the capture points should be awarded to. * @return {number} - The number of capture points actually taken. */ Capturable.prototype.Reduce = function(amount, playerID) { if (amount <= 0) return 0; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return 0; let cmpPlayerSource = QueryPlayerIDInterface(playerID); if (!cmpPlayerSource) return 0; // Before changing the value, activate Fogging if necessary to hide changes. let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); let numberOfEnemies = this.capturePoints.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length; if (numberOfEnemies == 0) return 0; // Distribute the capture points over all enemies. let distributedAmount = amount / numberOfEnemies; let removedAmount = 0; while (distributedAmount > 0.0001) { numberOfEnemies = 0; for (let i in this.capturePoints) { if (!this.capturePoints[i] || !cmpPlayerSource.IsEnemy(i)) continue; if (this.capturePoints[i] > distributedAmount) { removedAmount += distributedAmount; this.capturePoints[i] -= distributedAmount; ++numberOfEnemies; } else { removedAmount += this.capturePoints[i]; this.capturePoints[i] = 0; } } distributedAmount = numberOfEnemies ? (amount - removedAmount) / numberOfEnemies : 0; } // Give all capture points taken to the player. let takenCapturePoints = this.maxCapturePoints - this.capturePoints.reduce((a, b) => a + b); this.capturePoints[playerID] += takenCapturePoints; this.CheckTimer(); this.RegisterCapturePointsChanged(); return takenCapturePoints; }; /** * Check if the source can (re)capture points from this building. * @param {number} playerID - PlayerID of the source. * @return {boolean} - Whether the source can (re)capture points from this building. */ Capturable.prototype.CanCapture = function(playerID) { let cmpPlayerSource = QueryPlayerIDInterface(playerID); if (!cmpPlayerSource) warn(playerID + " has no player component defined on its id."); let capturePoints = this.GetCapturePoints(); let sourceEnemyCapturePoints = 0; for (let i in this.GetCapturePoints()) if (cmpPlayerSource.IsEnemy(i)) sourceEnemyCapturePoints += capturePoints[i]; return sourceEnemyCapturePoints > 0; }; -//// Private functions //// +// Private functions /** * This has to be called whenever the capture points are changed. * It notifies other components of the change, and switches ownership when needed. */ Capturable.prototype.RegisterCapturePointsChanged = function() { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints }); let owner = cmpOwnership.GetOwner(); if (owner == INVALID_PLAYER || this.capturePoints[owner] > 0) return; // If all capture points have been taken from the owner, convert it to player with the most capture points. let cmpLostPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpLostPlayerStatisticsTracker) cmpLostPlayerStatisticsTracker.LostEntity(this.entity); cmpOwnership.SetOwner(this.capturePoints.reduce((bestPlayer, playerCapturePoints, player, capturePoints) => playerCapturePoints > capturePoints[bestPlayer] ? player : bestPlayer, 0)); let cmpCapturedPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpCapturedPlayerStatisticsTracker) cmpCapturedPlayerStatisticsTracker.CapturedEntity(this.entity); }; Capturable.prototype.GetRegenRate = function() { let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder) return this.regenRate; return this.regenRate + this.GetGarrisonRegenRate() * cmpGarrisonHolder.GetEntities().length; }; Capturable.prototype.TimerTick = function() { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return; let owner = cmpOwnership.GetOwner(); let modifiedCapturePoints = 0; // Special handle for the territory decay. // Reduce capture points from the owner in favour of all neighbours (also allies). let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying()) { let neighbours = cmpTerritoryDecay.GetConnectedNeighbours(); let totalNeighbours = neighbours.reduce((a, b) => a + b); let decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.capturePoints[owner]); this.capturePoints[owner] -= decay; if (totalNeighbours) for (let p in neighbours) this.capturePoints[p] += decay * neighbours[p] / totalNeighbours; // Decay to gaia as default. else this.capturePoints[0] += decay; modifiedCapturePoints += decay; this.RegisterCapturePointsChanged(); } let regenRate = this.GetRegenRate(); if (regenRate < 0) modifiedCapturePoints += this.Reduce(-regenRate, 0); else if (regenRate > 0) modifiedCapturePoints += this.Reduce(regenRate, owner); if (modifiedCapturePoints) return; // Nothing changed, stop the timer. let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": false, "regenRate": 0, "territoryDecay": 0 }); }; /** * Start the regeneration timer when no timer exists. * When nothing can be modified (f.e. because it is fully regenerated), the * timer stops automatically after one execution. */ Capturable.prototype.CheckTimer = function() { if (this.timer) return; let regenRate = this.GetRegenRate(); let cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); let decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0; if (regenRate == 0 && decay == 0) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null); Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": true, "regenRate": regenRate, "territoryDecay": decay }); }; /** * Update all chached values that could be affected by modifications. */ Capturable.prototype.UpdateCachedValues = function() { this.garrisonRegenRate = ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity); this.regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", +this.template.RegenRate, this.entity); this.maxCapturePoints = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity); }; /** * Update all chached values that could be affected by modifications. * Check timer and send changed messages when required. * @param {boolean} message - Whether not to send a CapturePointsChanged message. When false, caller should take care of sending that message. */ Capturable.prototype.UpdateCachedValuesAndNotify = function(sendMessage = true) { let oldMaxCapturePoints = this.maxCapturePoints; let oldGarrisonRegenRate = this.garrisonRegenRate; let oldRegenRate = this.regenRate; this.UpdateCachedValues(); if (oldMaxCapturePoints != this.maxCapturePoints) { let scale = this.maxCapturePoints / oldMaxCapturePoints; for (let i in this.capturePoints) this.capturePoints[i] *= scale; if (sendMessage) Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints }); } if (oldGarrisonRegenRate != this.garrisonRegenRate || oldRegenRate != this.regenRate) this.CheckTimer(); }; -//// Message Listeners //// +// Message Listeners Capturable.prototype.OnValueModification = function(msg) { if (msg.component == "Capturable") this.UpdateCachedValuesAndNotify(); }; Capturable.prototype.OnGarrisonedUnitsChanged = function(msg) { this.CheckTimer(); }; Capturable.prototype.OnTerritoryDecayChanged = function(msg) { if (msg.to) this.CheckTimer(); }; Capturable.prototype.OnDiplomacyChanged = function(msg) { this.CheckTimer(); }; Capturable.prototype.OnOwnershipChanged = function(msg) { if (msg.to == INVALID_PLAYER) return; // Initialise the capture points when created. if (!this.capturePoints.length) { this.UpdateCachedValues(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { if (i == msg.to) this.capturePoints[i] = this.maxCapturePoints; else this.capturePoints[i] = 0; } this.CheckTimer(); return; } // When already initialised, this happens on defeat or wololo, // transfer the points of the old owner to the new one. if (this.capturePoints[msg.from]) { this.capturePoints[msg.to] += this.capturePoints[msg.from]; this.capturePoints[msg.from] = 0; this.UpdateCachedValuesAndNotify(false); this.RegisterCapturePointsChanged(); return; } this.UpdateCachedValuesAndNotify(); }; /** * When a player is defeated, reassign the capture points of non-owned entities to gaia. * Those owned by the defeated player are dealt with onOwnershipChanged. */ Capturable.prototype.OnGlobalPlayerDefeated = function(msg) { if (!this.capturePoints[msg.playerId]) return; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && (cmpOwnership.GetOwner() == INVALID_PLAYER || cmpOwnership.GetOwner() == msg.playerId)) return; this.capturePoints[0] += this.capturePoints[msg.playerId]; this.capturePoints[msg.playerId] = 0; this.RegisterCapturePointsChanged(); this.CheckTimer(); }; Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable); Index: ps/trunk/binaries/data/mods/public/simulation/components/CeasefireManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/CeasefireManager.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/CeasefireManager.js (revision 25087) @@ -1,134 +1,134 @@ function CeasefireManager() {} CeasefireManager.prototype.Schema = ""; CeasefireManager.prototype.Init = function() { // Weather or not ceasefire is active currently. this.ceasefireIsActive = false; // Ceasefire timeout in milliseconds this.ceasefireTime = 0; // Time elapsed when the ceasefire was started this.ceasefireStartedTime = 0; // diplomacy states before the ceasefire started this.diplomacyBeforeCeasefire = []; // Message duration for the countdown in milliseconds this.countdownMessageDuration = 10000; // Duration for the post ceasefire message in milliseconds this.postCountdownMessageDuration = 5000; }; CeasefireManager.prototype.IsCeasefireActive = function() { return this.ceasefireIsActive; }; CeasefireManager.prototype.GetCeasefireStartedTime = function() { return this.ceasefireStartedTime; }; CeasefireManager.prototype.GetCeasefireTime = function() { return this.ceasefireTime; }; CeasefireManager.prototype.GetDiplomacyBeforeCeasefire = function() { return this.diplomacyBeforeCeasefire; }; CeasefireManager.prototype.StartCeasefire = function(ceasefireTime) { // If invalid timeout given, return if (ceasefireTime <= 0) return; // Remove existing timers let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (this.ceasefireCountdownMessageTimer) cmpTimer.CancelTimer(this.ceasefireCountdownMessageTimer); if (this.stopCeasefireTimer) cmpTimer.CancelTimer(this.stopCeasefireTimer); // Remove existing messages let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (this.ceasefireCountdownMessage) cmpGuiInterface.DeleteTimeNotification(this.ceasefireCountdownMessage); if (this.ceasefireEndedMessage) cmpGuiInterface.DeleteTimeNotification(this.ceasefireEndedMessage); // Save diplomacy and set everyone neutral if (!this.ceasefireIsActive) { // Save diplomacy let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) this.diplomacyBeforeCeasefire.push(QueryPlayerIDInterface(i).GetDiplomacy()); // Set every enemy (except gaia) to neutral for (let i = 1; i < numPlayers; ++i) for (let j = 1; j < numPlayers; ++j) if (this.diplomacyBeforeCeasefire[i-1][j] < 0) QueryPlayerIDInterface(i).SetNeutral(j); } this.ceasefireIsActive = true; this.ceasefireTime = ceasefireTime; this.ceasefireStartedTime = cmpTimer.GetTime(); Engine.PostMessage(SYSTEM_ENTITY, MT_CeasefireStarted); // Add timers for countdown message and resetting diplomacy this.stopCeasefireTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_CeasefireManager, "StopCeasefire", this.ceasefireTime); this.ceasefireCountdownMessageTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_CeasefireManager, "ShowCeasefireCountdownMessage", this.ceasefireTime - this.countdownMessageDuration); }; CeasefireManager.prototype.ShowCeasefireCountdownMessage = function() { let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); this.ceasefireCountdownMessage = cmpGuiInterface.AddTimeNotification({ - "message": markForTranslation("You can attack in %(time)s"), - "translateMessage": true - }, this.countdownMessageDuration); + "message": markForTranslation("You can attack in %(time)s"), + "translateMessage": true + }, this.countdownMessageDuration); }; CeasefireManager.prototype.StopCeasefire = function() { let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (this.ceasefireCountdownMessage) cmpGuiInterface.DeleteTimeNotification(this.ceasefireCountdownMessage); this.ceasefireEndedMessage = cmpGuiInterface.AddTimeNotification({ "message": markForTranslation("You can attack now!"), "translateMessage": true }, this.postCountdownMessageDuration); // Reset diplomacies to original settings let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) QueryPlayerIDInterface(i).SetDiplomacy(this.diplomacyBeforeCeasefire[i-1]); this.ceasefireIsActive = false; this.ceasefireTime = 0; this.ceasefireStartedTime = 0; this.diplomacyBeforeCeasefire = []; Engine.PostMessage(SYSTEM_ENTITY, MT_CeasefireEnded); cmpGuiInterface.PushNotification({ "type": "ceasefire-ended", "players": [-1] // processed globally }); }; Engine.RegisterSystemComponentType(IID_CeasefireManager, "CeasefireManager", CeasefireManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/EndGameManager.js (revision 25087) @@ -1,202 +1,202 @@ /** * System component to store the victory conditions and their settings and * check for allied victory / last-man-standing. */ function EndGameManager() {} EndGameManager.prototype.Schema = ""; EndGameManager.prototype.Init = function() { // Contains settings specific to the victory condition, // for example wonder victory duration. this.gameSettings = {}; // Allied victory means allied players can win if victory conditions are met for each of them // False for a "last man standing" game this.alliedVictory = true; // Don't do any checks before the diplomacies were set for each player // or when marking a player as won. this.skipAlliedVictoryCheck = true; this.lastManStandingMessage = undefined; this.endlessGame = false; }; EndGameManager.prototype.GetGameSettings = function() { return this.gameSettings; }; EndGameManager.prototype.GetVictoryConditions = function() { return this.gameSettings.victoryConditions; }; EndGameManager.prototype.SetGameSettings = function(newSettings = {}) { this.gameSettings = newSettings; this.skipAlliedVictoryCheck = false; this.endlessGame = !this.gameSettings.victoryConditions.length; Engine.BroadcastMessage(MT_VictoryConditionsChanged, {}); }; /** * Sets the given player (and the allies if allied victory is enabled) as a winner. * * @param {number} playerID - The player that should win. * @param {function} victoryReason - Function that maps from number to plural string, for example * n => markForPluralTranslation( * "%(lastPlayer)s has won (game mode).", * "%(players)s and %(lastPlayer)s have won (game mode).", * n)); */ EndGameManager.prototype.MarkPlayerAndAlliesAsWon = function(playerID, victoryString, defeatString) { let state = QueryPlayerIDInterface(playerID).GetState(); if (state != "active") { warn("Can't mark player " + playerID + " as won, since the state is " + state); return; } let winningPlayers = [playerID]; if (this.alliedVictory) winningPlayers = QueryPlayerIDInterface(playerID).GetMutualAllies(playerID).filter( player => QueryPlayerIDInterface(player).GetState() == "active"); this.MarkPlayersAsWon(winningPlayers, victoryString, defeatString); }; /** * Sets the given players as won and others as defeated. * * @param {array} winningPlayers - The players that should win. * @param {function} victoryReason - Function that maps from number to plural string, for example * n => markForPluralTranslation( * "%(lastPlayer)s has won (game mode).", * "%(players)s and %(lastPlayer)s have won (game mode).", * n)); */ EndGameManager.prototype.MarkPlayersAsWon = function(winningPlayers, victoryString, defeatString) { this.skipAlliedVictoryCheck = true; for (let playerID of winningPlayers) { let cmpPlayer = QueryPlayerIDInterface(playerID); let state = cmpPlayer.GetState(); if (state != "active") { warn("Can't mark player " + playerID + " as won, since the state is " + state); continue; } cmpPlayer.SetState("won", undefined); } let defeatedPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers().filter( playerID => winningPlayers.indexOf(playerID) == -1); for (let playerID of defeatedPlayers) QueryPlayerIDInterface(playerID).SetState("defeated", undefined); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "won", "players": [winningPlayers[0]], - "allies" : winningPlayers, + "allies": winningPlayers, "message": victoryString(winningPlayers.length) }); if (defeatedPlayers.length) cmpGUIInterface.PushNotification({ "type": "defeat", "players": [defeatedPlayers[0]], - "allies" : defeatedPlayers, + "allies": defeatedPlayers, "message": defeatString(defeatedPlayers.length) }); this.skipAlliedVictoryCheck = false; }; EndGameManager.prototype.SetAlliedVictory = function(flag) { this.alliedVictory = flag; }; EndGameManager.prototype.GetAlliedVictory = function() { return this.alliedVictory; }; EndGameManager.prototype.AlliedVictoryCheck = function() { if (this.skipAlliedVictoryCheck || this.endlessGame) return; let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.DeleteTimeNotification(this.lastManStandingMessage); // Proceed if only allies are remaining let allies = []; let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let playerID = 1; playerID < numPlayers; ++playerID) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer.GetState() != "active") continue; if (allies.length && !cmpPlayer.IsMutualAlly(allies[0])) return; allies.push(playerID); } if (!allies.length) return; if (this.alliedVictory || allies.length == 1) { for (let playerID of allies) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer) cmpPlayer.SetState("won", undefined); } cmpGuiInterface.PushNotification({ "type": "won", "players": [allies[0]], - "allies" : allies, + "allies": allies, "message": markForPluralTranslation( "%(lastPlayer)s has won (last player alive).", "%(players)s and %(lastPlayer)s have won (last players alive).", allies.length) }); } else this.lastManStandingMessage = cmpGuiInterface.AddTimeNotification({ "message": markForTranslation("Last remaining player wins."), "translateMessage": true, }, 12 * 60 * 60 * 1000); // 12 hours }; EndGameManager.prototype.OnInitGame = function(msg) { this.AlliedVictoryCheck(); }; EndGameManager.prototype.OnGlobalDiplomacyChanged = function(msg) { this.AlliedVictoryCheck(); }; EndGameManager.prototype.OnGlobalPlayerDefeated = function(msg) { this.AlliedVictoryCheck(); }; Engine.RegisterSystemComponentType(IID_EndGameManager, "EndGameManager", EndGameManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/EntityLimits.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/EntityLimits.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/EntityLimits.js (revision 25087) @@ -1,298 +1,298 @@ function EntityLimits() {} EntityLimits.prototype.Schema = "Specifies per category limits on number of entities (buildings or units) that can be created for each player" + "" + "" + "1" + "10" + "1" + "5" + "25" + "1" + "" + "" + "" + "2" + "" + "" + "" + "" + "town_phase" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + ""; const TRAINING = "training"; const BUILD = "build"; EntityLimits.prototype.Init = function() { this.limit = {}; this.count = {}; // counts entities which change the limit of the given category this.changers = {}; this.removers = {}; this.classCount = {}; // counts entities with the given class, used in the limit removal this.removedLimit = {}; this.matchTemplateCount = {}; for (var category in this.template.Limits) { this.limit[category] = +this.template.Limits[category]; this.count[category] = 0; if (category in this.template.LimitChangers) { this.changers[category] = {}; for (var c in this.template.LimitChangers[category]) this.changers[category][c] = +this.template.LimitChangers[category][c]; } if (category in this.template.LimitRemovers) { this.removedLimit[category] = this.limit[category]; // keep a copy of removeable limits for possible restoration this.removers[category] = {}; for (var c in this.template.LimitRemovers[category]) { this.removers[category][c] = this.template.LimitRemovers[category][c]._string.split(/\s+/); if (c === "RequiredClasses") for (var cls of this.removers[category][c]) this.classCount[cls] = 0; } } } }; EntityLimits.prototype.ChangeCount = function(category, value) { if (this.count[category] !== undefined) this.count[category] += value; }; EntityLimits.prototype.ChangeMatchCount = function(template, value) { if (!this.matchTemplateCount[template]) this.matchTemplateCount[template] = 0; this.matchTemplateCount[template] += value; }; EntityLimits.prototype.GetLimits = function() { return this.limit; }; EntityLimits.prototype.GetCounts = function() { return this.count; }; EntityLimits.prototype.GetMatchCounts = function() { return this.matchTemplateCount; }; EntityLimits.prototype.GetLimitChangers = function() { return this.changers; }; EntityLimits.prototype.UpdateLimitsFromTech = function(tech) { for (var category in this.removers) - if ("RequiredTechs" in this.removers[category] && this.removers[category]["RequiredTechs"].indexOf(tech) !== -1) - this.removers[category]["RequiredTechs"].splice(this.removers[category]["RequiredTechs"].indexOf(tech), 1); + if ("RequiredTechs" in this.removers[category] && this.removers[category].RequiredTechs.indexOf(tech) !== -1) + this.removers[category].RequiredTechs.splice(this.removers[category].RequiredTechs.indexOf(tech), 1); this.UpdateLimitRemoval(); }; EntityLimits.prototype.UpdateLimitRemoval = function() { for (var category in this.removers) { var nolimit = true; if ("RequiredTechs" in this.removers[category]) - nolimit = !this.removers[category]["RequiredTechs"].length; + nolimit = !this.removers[category].RequiredTechs.length; if (nolimit && "RequiredClasses" in this.removers[category]) - for (var cls of this.removers[category]["RequiredClasses"]) + for (var cls of this.removers[category].RequiredClasses) nolimit = nolimit && this.classCount[cls] > 0; if (nolimit && this.limit[category] !== undefined) this.limit[category] = undefined; else if (!nolimit && this.limit[category] === undefined) this.limit[category] = this.removedLimit[category]; } }; EntityLimits.prototype.AllowedToCreate = function(limitType, category, count, templateName, matchLimit) { if (this.count[category] !== undefined && this.limit[category] !== undefined && this.count[category] + count > this.limit[category]) { this.NotifyLimit(limitType, category, this.limit[category]); return false; } if (this.matchTemplateCount[templateName] !== undefined && matchLimit !== undefined && this.matchTemplateCount[templateName] + count > matchLimit) { this.NotifyLimit(limitType, category, matchLimit); return false; } return true; }; EntityLimits.prototype.NotifyLimit = function(limitType, category, limit) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let notification = { "players": [cmpPlayer.GetPlayerID()], "translateMessage": true, "translateParameters": ["category"], "parameters": { "category": category, "limit": limit }, }; if (limitType == BUILD) notification.message = markForTranslation("%(category)s build limit of %(limit)s reached"); else if (limitType == TRAINING) notification.message = markForTranslation("%(category)s training limit of %(limit)s reached"); else { warn("EntityLimits.js: Unknown LimitType " + limitType); notification.message = markForTranslation("%(category)s limit of %(limit)s reached"); } let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); }; EntityLimits.prototype.AllowedToBuild = function(category) { // We pass count 0 as the creation of the building has already taken place and // the ownership has been set (triggering OnGlobalOwnershipChanged) return this.AllowedToCreate(BUILD, category, 0); }; EntityLimits.prototype.AllowedToTrain = function(category, count, templateName, matchLimit) { return this.AllowedToCreate(TRAINING, category, count, templateName, matchLimit); }; /** * @param {number} ent - id of the entity which would be replaced. * @param {string} template - name of the new template. * @return {boolean} - whether we can replace ent. */ EntityLimits.prototype.AllowedToReplace = function(ent, template) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let templateFrom = cmpTemplateManager.GetTemplate(cmpTemplateManager.GetCurrentTemplateName(ent)); let templateTo = cmpTemplateManager.GetTemplate(template); if (templateTo.TrainingRestrictions) { let category = templateTo.TrainingRestrictions.Category; return this.AllowedToCreate(TRAINING, category, templateFrom.TrainingRestrictions && templateFrom.TrainingRestrictions.Category == category ? 0 : 1); } if (templateTo.BuildRestrictions) { let category = templateTo.BuildRestrictions.Category; return this.AllowedToCreate(BUILD, category, templateFrom.BuildRestrictions && templateFrom.BuildRestrictions.Category == category ? 0 : 1); } return true; }; EntityLimits.prototype.OnGlobalOwnershipChanged = function(msg) { // check if we are adding or removing an entity from this player var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) { error("EntityLimits component is defined on a non-player entity"); return; } if (msg.from == cmpPlayer.GetPlayerID()) var modifier = -1; else if (msg.to == cmpPlayer.GetPlayerID()) var modifier = 1; else return; // Update entity counts var category = null; var cmpBuildRestrictions = Engine.QueryInterface(msg.entity, IID_BuildRestrictions); if (cmpBuildRestrictions) category = cmpBuildRestrictions.GetCategory(); var cmpTrainingRestrictions = Engine.QueryInterface(msg.entity, IID_TrainingRestrictions); if (cmpTrainingRestrictions) category = cmpTrainingRestrictions.GetCategory(); if (category) this.ChangeCount(category, modifier); // Update entity limits var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; // foundations shouldn't change the entity limits until they're completed var cmpFoundation = Engine.QueryInterface(msg.entity, IID_Foundation); if (cmpFoundation) return; var classes = cmpIdentity.GetClassesList(); for (var category in this.changers) for (var c in this.changers[category]) if (classes.indexOf(c) >= 0) { if (this.limit[category] != undefined) this.limit[category] += modifier * this.changers[category][c]; if (this.removedLimit[category] != undefined) // update removed limits in case we want to restore it this.removedLimit[category] += modifier * this.changers[category][c]; } for (var category in this.removers) if ("RequiredClasses" in this.removers[category]) - for (var cls of this.removers[category]["RequiredClasses"]) + for (var cls of this.removers[category].RequiredClasses) if (classes.indexOf(cls) !== -1) this.classCount[cls] += modifier; this.UpdateLimitRemoval(); }; Engine.RegisterComponentType(IID_EntityLimits, "EntityLimits", EntityLimits); Index: ps/trunk/binaries/data/mods/public/simulation/components/FormationAttack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/FormationAttack.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/FormationAttack.js (revision 25087) @@ -1,71 +1,71 @@ function FormationAttack() {} FormationAttack.prototype.Schema = "" + "" + ""; FormationAttack.prototype.Init = function() { this.canAttackAsFormation = this.template.CanAttackAsFormation == "true"; }; FormationAttack.prototype.CanAttackAsFormation = function() { return this.canAttackAsFormation; }; // Only called when making formation entities selectable for debugging FormationAttack.prototype.GetAttackTypes = function() { return []; }; FormationAttack.prototype.GetRange = function(target) { - var result = {"min": 0, "max": this.canAttackAsFormation ? -1 : 0}; + var result = { "min": 0, "max": this.canAttackAsFormation ? -1 : 0 }; var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) { warn("FormationAttack component used on a non-formation entity"); return result; } var members = cmpFormation.GetMembers(); for (var ent of members) { var cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (!cmpAttack) continue; var type = cmpAttack.GetBestAttackAgainst(target); if (!type) continue; // if the formation can attack, take the minimum max range (so units are certainly in range), // If the formation can't attack, take the maximum max range as the point where the formation will be disbanded // Always take the minimum min range (to not get impossible situations) var range = cmpAttack.GetRange(type); if (this.canAttackAsFormation) { if (range.max < result.max || result.max < 0) result.max = range.max; } else { if (range.max > result.max || range.max < 0) result.max = range.max; } if (range.min < result.min) result.min = range.min; } // add half the formation size, so it counts as the range for the units on the first row var extraRange = cmpFormation.GetSize().depth/2; if (result.max >= 0) result.max += extraRange; return result; }; Engine.RegisterComponentType(IID_Attack, "FormationAttack", FormationAttack); Index: ps/trunk/binaries/data/mods/public/simulation/components/RangeOverlayManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/RangeOverlayManager.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/RangeOverlayManager.js (revision 25087) @@ -1,92 +1,92 @@ function RangeOverlayManager() {} RangeOverlayManager.prototype.Schema = ""; RangeOverlayManager.prototype.Init = function() { this.enabled = false; this.enabledRangeTypes = { "Attack": false, "Auras": false, "Heal": false }; this.rangeVisualizations = new Map(); }; // The GUI enables visualizations RangeOverlayManager.prototype.Serialize = null; RangeOverlayManager.prototype.Deserialize = function(data) { this.Init(); }; RangeOverlayManager.prototype.UpdateRangeOverlays = function(componentName) { let cmp = Engine.QueryInterface(this.entity, global["IID_" + componentName]); if (cmp) this.rangeVisualizations.set(componentName, cmp.GetRangeOverlays()); }; RangeOverlayManager.prototype.SetEnabled = function(enabled, enabledRangeTypes, forceUpdate) { this.enabled = enabled; this.enabledRangeTypes = enabledRangeTypes; this.RegenerateRangeOverlays(forceUpdate); }; RangeOverlayManager.prototype.RegenerateRangeOverlays = function(forceUpdate) { let cmpRangeOverlayRenderer = Engine.QueryInterface(this.entity, IID_RangeOverlayRenderer); if (!cmpRangeOverlayRenderer) return; cmpRangeOverlayRenderer.ResetRangeOverlays(); if (!this.enabled && !forceUpdate) return; // Only render individual range types that have been enabled for (let rangeOverlayType of this.rangeVisualizations.keys()) if (this.enabledRangeTypes[rangeOverlayType]) for (let rangeOverlay of this.rangeVisualizations.get(rangeOverlayType)) cmpRangeOverlayRenderer.AddRangeOverlay( rangeOverlay.radius, rangeOverlay.texture, rangeOverlay.textureMask, rangeOverlay.thickness); }; RangeOverlayManager.prototype.OnOwnershipChanged = function(msg) { if (msg.to == INVALID_PLAYER) return; for (let type in this.enabledRangeTypes) this.UpdateRangeOverlays(type); this.RegenerateRangeOverlays(false); }; RangeOverlayManager.prototype.OnValueModification = function(msg) { if (msg.valueNames.indexOf("Heal/Range") == -1 && msg.valueNames.indexOf("Attack/Ranged/MinRange") == -1 && msg.valueNames.indexOf("Attack/Ranged/MaxRange") == -1) return; this.UpdateRangeOverlays(msg.component); this.RegenerateRangeOverlays(false); }; -/** +/** * RangeOverlayManager component is deserialized before the TechnologyManager, so need to update the ranges here */ RangeOverlayManager.prototype.OnDeserialized = function(msg) { for (let type in this.enabledRangeTypes) this.UpdateRangeOverlays(type); }; Engine.RegisterComponentType(IID_RangeOverlayManager, "RangeOverlayManager", RangeOverlayManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js (revision 25087) @@ -1,154 +1,154 @@ function Repairable() {} Repairable.prototype.Schema = "Deals with repairable structures and units." + "" + "2.0" + "" + "" + "" + ""; Repairable.prototype.Init = function() { this.builders = new Map(); // Map of builder entities to their work per second this.totalBuilderRate = 0; // Total amount of work the builders do each second this.buildMultiplier = 1; // Multiplier for the amount of work builders do this.buildTimePenalty = 0.7; // Penalty for having multiple builders this.repairTimeRatio = +this.template.RepairTimeRatio; }; /** * Returns the current build progress in a [0,1] range. */ Repairable.prototype.GetBuildProgress = function() { var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) return 0; var hitpoints = cmpHealth.GetHitpoints(); var maxHitpoints = cmpHealth.GetMaxHitpoints(); return hitpoints / maxHitpoints; }; /** * Returns the current builders. * * @return {number[]} - An array containing the entity IDs of assigned builders. */ Repairable.prototype.GetBuilders = function() { return Array.from(this.builders.keys()); }; Repairable.prototype.GetNumBuilders = function() { return this.builders.size; }; /** * Adds an array of builders. * * @param {number[]} - An array containing the entity IDs of builders to assign. */ Repairable.prototype.AddBuilders = function(builders) { for (let builder of builders) this.AddBuilder(builder); -} +}; Repairable.prototype.AddBuilder = function(builderEnt) { if (this.builders.has(builderEnt)) return; this.builders.set(builderEnt, Engine.QueryInterface(builderEnt, IID_Builder).GetRate()); this.totalBuilderRate += this.builders.get(builderEnt); this.SetBuildMultiplier(); }; Repairable.prototype.RemoveBuilder = function(builderEnt) { if (!this.builders.has(builderEnt)) return; this.totalBuilderRate -= this.builders.get(builderEnt); this.builders.delete(builderEnt); this.SetBuildMultiplier(); }; /** * The build multiplier is a penalty that is applied to each builder. * For example, ten women build at a combined rate of 10^0.7 = 5.01 instead of 10. */ Repairable.prototype.CalculateBuildMultiplier = function(num) { // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized return num < 2 ? 1 : Math.pow(num, this.buildTimePenalty) / num; }; Repairable.prototype.SetBuildMultiplier = function() { this.buildMultiplier = this.CalculateBuildMultiplier(this.GetNumBuilders()); }; Repairable.prototype.GetBuildTime = function() { let timeLeft = (1 - this.GetBuildProgress()) * Engine.QueryInterface(this.entity, IID_Cost).GetBuildTime() * this.repairTimeRatio; let rate = this.totalBuilderRate * this.buildMultiplier; // The rate if we add another woman to the repairs let rateNew = (this.totalBuilderRate + 1) * this.CalculateBuildMultiplier(this.GetNumBuilders() + 1); return { // Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized "timeRemaining": rate ? timeLeft / rate : 0, "timeRemainingNew": timeLeft / rateNew }; }; // TODO: should we have resource costs? Repairable.prototype.Repair = function(builderEnt, rate) { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpCost = Engine.QueryInterface(this.entity, IID_Cost); if (!cmpHealth || !cmpCost) return; let damage = cmpHealth.GetMaxHitpoints() - cmpHealth.GetHitpoints(); if (damage <= 0) return; // Calculate the amount of hitpoints that will be added (using diminishing rate when several builders) let work = rate * this.buildMultiplier * this.GetRepairRate(); let amount = Math.min(damage, work); cmpHealth.Increase(amount); // Update the total builder rate this.totalBuilderRate += rate - this.builders.get(builderEnt); this.builders.set(builderEnt, rate); // If we repaired all the damage, send a message to entities to stop repairing this building if (amount >= damage) { Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": this.entity }); // Inform the builders that repairing has finished. // This not done by listening to a global message due to performance. for (let builder of this.GetBuilders()) { let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI); if (cmpUnitAIBuilder) cmpUnitAIBuilder.ConstructionFinished({ "entity": this.entity, "newentity": this.entity }); } } }; Repairable.prototype.GetRepairRate = function() { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpCost = Engine.QueryInterface(this.entity, IID_Cost); let repairTime = this.repairTimeRatio * cmpCost.GetBuildTime(); return repairTime ? cmpHealth.GetMaxHitpoints() / repairTime : 1; }; Engine.RegisterComponentType(IID_Repairable, "Repairable", Repairable); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 25087) @@ -1,376 +1,376 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the entityID of the researcher. this.researchQueued = new Map(); // Holds technologies which are being researched currently (non-queued). this.researchStarted = new Set(); this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.unresearchedAutoResearchTechs = new Set(); let allTechs = TechnologyTemplates.GetAll(); for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(key); }; TechnologyManager.prototype.OnUpdate = function() { this.UpdateAutoResearch(); }; // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { for (let key of this.unresearchedAutoResearchTechs) { let tech = TechnologyTemplates.Get(key); - if ((tech.autoResearch && this.CanResearch(key)) - || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) + if ((tech.autoResearch && this.CanResearch(key)) || + (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } }; // Checks an entity template to see if its technology requirements have been met -TechnologyManager.prototype.CanProduce = function (templateName) +TechnologyManager.prototype.CanProduce = function(templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; TechnologyManager.prototype.IsTechnologyStarted = function(tech) { return this.researchStarted.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; if (this.IsTechnologyResearched(tech)) return false; return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {Object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg) { // This automatically updates classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (let cls of classes) { this.classCounts[cls] = this.classCounts[cls] || 0; this.classCounts[cls] += 1; this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; this.typeCountsByClass[cls][template] += 1; } } } if (msg.from == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (let cls of classes) { this.classCounts[cls] -= 1; if (this.classCounts[cls] <= 0) delete this.classCounts[cls]; this.typeCountsByClass[cls][template] -= 1; if (this.typeCountsByClass[cls][template] <= 0) delete this.typeCountsByClass[cls][template]; } } } } }; /** * Marks a technology as researched. * Note that this does not verify that the requirements are met. * * @param {string} tech - The technology to mark as researched. */ TechnologyManager.prototype.ResearchTechnology = function(tech) { this.StoppedResearch(tech, false); let modifiedComponents = {}; this.researchedTechs.add(tech); // Store the modifications in an easy to access structure. let template = TechnologyTemplates.Get(tech); if (template.modifications) { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("tech/" + tech, DeriveModificationsFromTech(template), this.entity); } if (template.replaces && template.replaces.length > 0) { for (let i of template.replaces) { if (!i || this.IsTechnologyResearched(i)) continue; this.researchedTechs.add(i); // Change the EntityLimit if any. let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined) { let playerID = cmpPlayer.GetPlayerID(); let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(i); } } } this.UpdateAutoResearch(); let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined) return; let playerID = cmpPlayer.GetPlayerID(); // Change the EntityLimit if any. let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(tech); // Always send research finished message. Engine.PostMessage(this.entity, MT_ResearchFinished, { "player": playerID, "tech": tech }); if (tech.startsWith("phase") && !template.autoResearch) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [playerID], "phaseName": tech, "phaseState": "completed" }); } }; /** * Marks a technology as being queued for research at the given entityID. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher) { this.researchQueued.set(tech, researcher); }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function(tech, notification) { this.researchStarted.add(tech); if (notification && tech.startsWith("phase")) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "started" }); } }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. */ TechnologyManager.prototype.StoppedResearch = function(tech, notification) { if (notification && tech.startsWith("phase") && this.researchStarted.has(tech)) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "aborted" }); } this.researchQueued.delete(tech); this.researchStarted.delete(tech); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; /** * Returns the names of technologies that are currently being researched (non-queued). */ TechnologyManager.prototype.GetStartedTechs = function() { return this.researchStarted; }; /** * Gets the entity currently researching the technology. */ TechnologyManager.prototype.GetResearcher = function(tech) { return this.researchQueued.get(tech); }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js (revision 25087) @@ -1,355 +1,355 @@ function Trigger() {} Trigger.prototype.Schema = ""; /** * Events we're able to receive and call handlers for. */ Trigger.prototype.eventNames = [ "CinemaPathEnded", "CinemaQueueEnded", "ConstructionStarted", "DiplomacyChanged", "Deserialized", "InitGame", "Interval", "EntityRenamed", "OwnershipChanged", "PlayerCommand", "PlayerDefeated", "PlayerWon", "Range", "ResearchFinished", "ResearchQueued", "StructureBuilt", "TrainingFinished", "TrainingQueued", "TreasureCollected" ]; Trigger.prototype.Init = function() { // Difficulty used by trigger scripts (as defined in data/settings/trigger_difficulties.json). this.difficulty = undefined; this.triggerPoints = {}; // Each event has its own set of actions determined by the map maker. for (let eventName of this.eventNames) this["On" + eventName + "Actions"] = {}; }; Trigger.prototype.RegisterTriggerPoint = function(ref, ent) { if (!this.triggerPoints[ref]) this.triggerPoints[ref] = []; this.triggerPoints[ref].push(ent); }; Trigger.prototype.RemoveRegisteredTriggerPoint = function(ref, ent) { if (!this.triggerPoints[ref]) { warn("no trigger points found with ref "+ref); return; } let i = this.triggerPoints[ref].indexOf(ent); if (i == -1) { warn("entity " + ent + " wasn't found under the trigger points with ref "+ref); return; } this.triggerPoints[ref].splice(i, 1); }; Trigger.prototype.GetTriggerPoints = function(ref) { return this.triggerPoints[ref] || []; }; /** * Binds a function to the specified event. * * @param {string} event - One of eventNames * @param {string} action - Name of a function available to this object * @param {Object} data - f.e. enabled or not, delay for timers, range for range triggers * * @example * data = { enabled: true, interval: 1000, delay: 500 } * * Range trigger: * data.entities = [id1, id2] * Ids of the source * data.players = [1,2,3,...] * list of player ids * data.minRange = 0 * Minimum range for the query * data.maxRange = -1 * Maximum range for the query (-1 = no maximum) * data.requiredComponent = 0 * Required component id the entities will have * data.enabled = false * If the query is enabled by default */ Trigger.prototype.RegisterTrigger = function(event, action, data) { let eventString = event + "Actions"; if (!this[eventString]) { warn("Trigger.js: Invalid trigger event \"" + event + "\"."); return; } if (this[eventString][action]) { warn("Trigger.js: Trigger \"" + action + "\" has been registered before. Aborting..."); return; } // clone the data to be sure it's only modified locally // We could run into triggers overwriting each other's data otherwise. // F.e. getting the wrong timer tag data = clone(data) || { "enabled": false }; this[eventString][action] = data; // setup range query if (event == "OnRange") { if (!data.entities) { warn("Trigger.js: Range triggers should carry extra data"); return; } data.queries = []; for (let ent of data.entities) { let cmpTriggerPoint = Engine.QueryInterface(ent, IID_TriggerPoint); if (!cmpTriggerPoint) { warn("Trigger.js: Range triggers must be defined on trigger points"); continue; } data.queries.push(cmpTriggerPoint.RegisterRangeTrigger(action, data)); } } if (data.enabled) this.EnableTrigger(event, action); }; Trigger.prototype.DisableTrigger = function(event, action) { let eventString = event + "Actions"; if (!this[eventString][action]) { warn("Trigger.js: Disabling unknown trigger"); return; } let data = this[eventString][action]; // special casing interval and range triggers for performance if (event == "OnInterval") { if (!data.timer) // don't disable it a second time return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(data.timer); data.timer = null; } else if (event == "OnRange") { if (!data.queries) { warn("Trigger.js: Range query wasn't set up before trying to disable it."); return; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let query of data.queries) cmpRangeManager.DisableActiveQuery(query); } data.enabled = false; }; Trigger.prototype.EnableTrigger = function(event, action) { let eventString = event + "Actions"; if (!this[eventString][action]) { warn("Trigger.js: Enabling unknown trigger"); return; } let data = this[eventString][action]; // special casing interval and range triggers for performance if (event == "OnInterval") { if (data.timer) // don't enable it a second time return; if (!data.interval) { warn("Trigger.js: An interval trigger should have an intervel in its data"); return; } let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); data.timer = cmpTimer.SetInterval(this.entity, IID_Trigger, "DoAction", - data.delay || 0, data.interval, { "action" : action }); + data.delay || 0, data.interval, { "action": action }); } else if (event == "OnRange") { if (!data.queries) { warn("Trigger.js: Range query wasn't set up before"); return; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let query of data.queries) cmpRangeManager.EnableActiveQuery(query); } data.enabled = true; }; /** * This function executes the actions bound to the events. * It's either called directlty from other simulation scripts, * or from message listeners in this file * * @param {string} event - One of eventNames * @param {Object} data - will be passed to the actions */ Trigger.prototype.CallEvent = function(event, data) { let eventString = "On" + event + "Actions"; if (!this[eventString]) { warn("Trigger.js: Unknown trigger event called:\"" + event + "\"."); return; } for (let action in this[eventString]) if (this[eventString][action].enabled) - this.DoAction({ "action": action, "data":data }); + this.DoAction({ "action": action, "data": data }); }; Trigger.prototype.OnGlobalInitGame = function(msg) { this.CallEvent("InitGame", {}); }; Trigger.prototype.OnGlobalConstructionFinished = function(msg) { this.CallEvent("StructureBuilt", { "building": msg.newentity, "foundation": msg.entity }); }; Trigger.prototype.OnGlobalTrainingFinished = function(msg) { this.CallEvent("TrainingFinished", msg); // The data for this one is {"entities": createdEnts, // "owner": cmpOwnership.GetOwner(), // "metadata": metadata} // See function "SpawnUnits" in ProductionQueue for more details }; Trigger.prototype.OnGlobalResearchFinished = function(msg) { this.CallEvent("ResearchFinished", msg); // The data for this one is { "player": playerID, "tech": tech } }; Trigger.prototype.OnGlobalCinemaPathEnded = function(msg) { this.CallEvent("CinemaPathEnded", msg); }; Trigger.prototype.OnGlobalCinemaQueueEnded = function(msg) { this.CallEvent("CinemaQueueEnded", msg); }; Trigger.prototype.OnGlobalDeserialized = function(msg) { this.CallEvent("Deserialized", msg); }; Trigger.prototype.OnGlobalEntityRenamed = function(msg) { this.CallEvent("EntityRenamed", msg); }; Trigger.prototype.OnGlobalOwnershipChanged = function(msg) { this.CallEvent("OwnershipChanged", msg); // data is {"entity": ent, "from": playerId, "to": playerId} }; Trigger.prototype.OnGlobalPlayerDefeated = function(msg) { this.CallEvent("PlayerDefeated", msg); }; Trigger.prototype.OnGlobalPlayerWon = function(msg) { this.CallEvent("PlayerWon", msg); }; Trigger.prototype.OnGlobalDiplomacyChanged = function(msg) { this.CallEvent("DiplomacyChanged", msg); }; /** * Execute a function after a certain delay. * * @param {number} time - Delay in milliseconds. * @param {string} action - Name of the action function. * @param {Object} data - Arbitrary object that will be passed to the action function. * @return {number} The ID of the timer, so it can be stopped later. */ Trigger.prototype.DoAfterDelay = function(time, action, data) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); return cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "DoAction", time, { "action": action, "data": data }); }; /** * Execute a function each time a certain delay has passed. * * @param {number} interval - Interval in milleseconds between consecutive calls. * @param {string} action - Name of the action function. * @param {Object} data - Arbitrary object that will be passed to the action function. * @param {number} [start] - Optional initial delay in milleseconds before starting the calls. * If not given, interval will be used. * @return {number} the ID of the timer, so it can be stopped later. */ Trigger.prototype.DoRepeatedly = function(time, action, data, start) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); return cmpTimer.SetInterval(SYSTEM_ENTITY, IID_Trigger, "DoAction", start !== undefined ? start : time, time, { "action": action, "data": data }); }; /** * Called by the trigger listeners to exucute the actual action. Including sanity checks. */ Trigger.prototype.DoAction = function(msg) { if (this[msg.action]) this[msg.action](msg.data || null); else warn("Trigger.js: called a trigger action '" + msg.action + "' that wasn't found"); }; /** * Level of difficulty used by trigger scripts. */ Trigger.prototype.GetDifficulty = function() { return this.difficulty; }; Trigger.prototype.SetDifficulty = function(diff) { this.difficulty = diff; }; Engine.RegisterSystemComponentType(IID_Trigger, "Trigger", Trigger); Index: ps/trunk/binaries/data/mods/public/simulation/components/TriggerPoint.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TriggerPoint.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/TriggerPoint.js (revision 25087) @@ -1,79 +1,79 @@ function TriggerPoint() {} TriggerPoint.prototype.Schema = "" + "" + "" + "" + ""; TriggerPoint.prototype.Init = function() { if (this.template && this.template.Reference) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.RegisterTriggerPoint(this.template.Reference, this.entity); } this.currentCollections = {}; this.actions = {}; }; TriggerPoint.prototype.OnDestroy = function() { if (this.template && this.template.EntityReference) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.RemoveRegisteredTriggerPoint(this.template.Reference, this.entity); } }; /** * @param action Name of the action function defined under Trigger * @param data The data is an object containing information for the range query * Some of the data has sendible defaults (mentionned next to the object) * data.players = [1,2,3,...] * list of player ids * data.minRange = 0 * Minimum range for the query * data.maxRange = -1 * Maximum range for the query (-1 = no maximum) * data.requiredComponent = -1 * Required component id the entities will have * data.enabled = false * If the query is enabled by default */ TriggerPoint.prototype.RegisterRangeTrigger = function(action, data) { var players = data.players || Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); var minRange = data.minRange || 0; var maxRange = data.maxRange || -1; var cid = data.requiredComponent || -1; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var tag = cmpRangeManager.CreateActiveQuery(this.entity, minRange, maxRange, players, cid, cmpRangeManager.GetEntityFlagMask("normal"), true); this.currentCollections[tag] = []; this.actions[tag] = action; return tag; }; TriggerPoint.prototype.OnRangeUpdate = function(msg) { var collection = this.currentCollections[msg.tag]; if (!collection) return; for (var ent of msg.removed) { var index = collection.indexOf(ent); if (index > -1) collection.splice(index, 1); } for (var entity of msg.added) collection.push(entity); - var r = {"currentCollection": collection.slice()}; + var r = { "currentCollection": collection.slice() }; r.added = msg.added; r.removed = msg.removed; var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); - cmpTrigger.DoAction({"action":this.actions[msg.tag], "data": r}); + cmpTrigger.DoAction({ "action": this.actions[msg.tag], "data": r }); }; Engine.RegisterComponentType(IID_TriggerPoint, "TriggerPoint", TriggerPoint); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 25087) @@ -1,6633 +1,6635 @@ 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; // To reject an order, use 'return this.FinishOrder();' const ACCEPT_ORDER = true; // 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 this.FinishOrder(); 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 this.FinishOrder(); 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 this.FinishOrder(); msg.data.min = g_LeaveFoundationRange; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, // Individual orders: "Order.LeaveFormation": function() { if (!this.IsFormationMember()) return this.FinishOrder(); 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 this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } this.SetHeldPosition(msg.data.x, msg.data.z); msg.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKING"); return ACCEPT_ORDER; }, "Order.WalkAndFight": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } this.SetHeldPosition(msg.data.x, msg.data.z); msg.data.relaxed = true; this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING"); return ACCEPT_ORDER; }, "Order.WalkToTarget": function(msg) { if (!this.AbleToMove()) return this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } if (this.CheckRange(msg.data)) return this.FinishOrder(); msg.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 this.FinishOrder(); let range = cmpGarrisonHolder.GetLoadingRange(); msg.data.min = range.min; msg.data.max = range.max; if (this.CheckRange(msg.data)) return this.FinishOrder(); // 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(msg.data.target, IID_UnitMotion); if (cmpPassengerMotion && cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) && PositionHelper.DistanceBetweenEntities(this.entity, msg.data.target) < 200) this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); else this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING"); return ACCEPT_ORDER; }, "Order.Guard": function(msg) { if (!this.AddGuard(msg.data.target)) return this.FinishOrder(); 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(msg.data.target, msg.data.allowCapture); if (!type) return this.FinishOrder(); msg.data.attackType = type; this.RememberTargetPosition(); if (msg.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather") this.RememberTargetPosition(this.orderQueue[1].data); if (this.CheckTargetAttackRange(msg.data.target, msg.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 && !msg.data.force && !msg.data.hunting || !this.AbleToMove()) return this.FinishOrder(); 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 this.FinishOrder(); if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } msg.data.relaxed = true; this.SetNextState("INDIVIDUAL.PATROL.PATROLLING"); return ACCEPT_ORDER; }, "Order.Heal": function(msg) { if (!this.TargetIsAlive(msg.data.target)) return this.FinishOrder(); // Healers can't heal themselves. if (msg.data.target == this.entity) return this.FinishOrder(); if (this.CheckTargetRange(msg.data.target, IID_Heal)) { this.SetNextState("INDIVIDUAL.HEAL.HEALING"); return ACCEPT_ORDER; } if (this.GetStance().respondStandGround && !msg.data.force) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); return ACCEPT_ORDER; }, "Order.Gather": function(msg) { if (!this.CanGather(msg.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. let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (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(msg.data.target)) { // Make sure we can attack the target, else we'll get very stuck if (!this.GetBestAttackAgainst(msg.data.target, false)) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed return this.FinishOrder(); } // The target was visible when this order was issued, // but could now be invisible again. 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 if (!this.FinishOrder()) this.PushOrderFront("GatherNearPosition", { "x": msg.data.lastPos.x, "z": msg.data.lastPos.z, "type": msg.data.type, "template": msg.data.template }); return ACCEPT_ORDER; } this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "allowCapture": false }); return ACCEPT_ORDER; } this.RememberTargetPosition(); if (!msg.data.initPos) msg.data.initPos = msg.data.lastPos; if (this.CheckTargetRange(msg.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"); msg.data.initPos = { 'x': msg.data.x, 'z': msg.data.z }; msg.data.relaxed = true; return ACCEPT_ORDER; }, "Order.ReturnResource": function(msg) { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (this.CheckTargetRange(msg.data.target, IID_ResourceGatherer) && this.CanReturnResource(msg.data.target, true, cmpResourceGatherer)) { cmpResourceGatherer.CommitResources(msg.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 this.FinishOrder(); this.waypoints = []; this.SetNextState("TRADE.APPROACHINGMARKET"); return ACCEPT_ORDER; }, "Order.Repair": function(msg) { if (this.CheckTargetRange(msg.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()) return this.FinishOrder(); // Also pack when we are in range. if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return ACCEPT_ORDER; } if (this.CheckGarrisonRange(msg.data.target)) this.SetNextState("INDIVIDUAL.GARRISON.GARRISONING"); else this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); return ACCEPT_ORDER; }, "Order.Ungarrison": function(msg) { // Note that this order MUST succeed, or we break // the assumptions done in garrisonable/garrisonHolder, // especially in Unloading in the latter. (For user feedback.) // ToDo: This can be fixed by not making that assumption :) this.FinishOrder(); return ACCEPT_ORDER; }, "Order.Cheer": function(msg) { return this.FinishOrder(); }, "Order.Pack": function(msg) { if (!this.CanPack()) return this.FinishOrder(); this.SetNextState("INDIVIDUAL.PACKING"); return ACCEPT_ORDER; }, "Order.Unpack": function(msg) { if (!this.CanUnpack()) return this.FinishOrder(); 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 this.FinishOrder(); }, "Order.CollectTreasure": function(msg) { let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter); if (!cmpTreasureCollecter || !cmpTreasureCollecter.CanCollect(msg.data.target)) return this.FinishOrder(); this.SetNextState("COLLECTTREASURE"); return ACCEPT_ORDER; }, "Order.CollectTreasureNearPosition": function(msg) { let nearbyTreasure = this.FindNearbyTreasure(msg.data.x, msg.data.z); if (nearbyTreasure) this.CollectTreasure(nearbyTreasure, oldData.autocontinue, true); else this.SetNextState("COLLECTTREASURE"); return ACCEPT_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(msg.data)) return this.FinishOrder(); this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkToTarget": function(msg) { if (this.CheckRange(msg.data)) return this.FinishOrder(); this.SetNextState("WALKING"); return ACCEPT_ORDER; }, "Order.WalkToPointRange": function(msg) { if (this.CheckRange(msg.data)) return this.FinishOrder(); 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 this.FinishOrder(); } 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 this.FinishOrder(); if (!this.CheckGarrisonRange(msg.data.target)) { if (!this.CheckTargetVisible(msg.data.target)) return this.FinishOrder(); 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 this.FinishOrder(); // 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 this.FinishOrder(); } 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 this.FinishOrder(); if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return this.FinishOrder(); } 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 this.FinishOrder(); if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return this.FinishOrder(); } 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 this.FinishOrder(); if (!msg.data.secondTry) { msg.data.secondTry = true; this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return ACCEPT_ORDER; } return this.FinishOrder(); } 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 this.FinishOrder(); msg.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 this.FinishOrder(); 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 this.FinishOrder(); }, "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") + 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 this.FinishOrder(); 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 this.FinishOrder(); msg.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". // If we can't move, assume we'll fail any subsequent order // and finish the order entirely to avoid an infinite loop. if (!this.AbleToMove()) { this.FinishOrder(); return true; } 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.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; } let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); // 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); let status = cmpResourceGatherer.PerformGather(this.gatheringTarget); 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 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return true; let filter = (ent, type, template) => { if (previousTarget == ent) return false; // Don't switch to a different type of huntable animal. return type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template); }; // Current position is often next to a dropsite. let pos = cmpPosition.GetPosition(); let nearbyResource = this.FindNearbyResource(Vector2D.from3D(pos), filter); // If there is an initPos, search there as well when we haven't found anything. // Otherwise set initPos to our current pos. if (!initPos) initPos = { 'x': pos.X, 'z': pos.Z }; else if (!nearbyResource) nearbyResource = this.FindNearbyResource(new Vector2D(initPos.X, initPos.Z), filter); 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.FinishOrder(); 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(); }, }, }, "COLLECTTREASURE": { "enter": function() { let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter); if (!cmpTreasureCollecter || !cmpTreasureCollecter.CanCollect(this.order.data.target)) { this.SetNextState("FINDINGNEWTARGET"); return true; } if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollecter)) this.SetNextState("COLLECTING"); else this.SetNextState("APPROACHING"); return true; }, "leave": function() { }, "APPROACHING": { "enter": function() { // If we can't move, assume we'll fail any subsequent order // and finish the order entirely to avoid an infinite loop. if (!this.AbleToMove()) { this.FinishOrder(); return true; } if (!this.MoveToTargetRange(this.order.data.target, IID_TreasureCollecter)) { this.SetNextState("FINDINGNEWTARGET"); return true; } return false; }, "leave": function() { this.StopMoving(); }, "MovementUpdate": function(msg) { if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollecter)) this.SetNextState("COLLECTING"); else if (msg.likelyFailure) this.SetNextState("FINDINGNEWTARGET"); }, }, "COLLECTING": { "enter": function() { let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter); if (!cmpTreasureCollecter.StartCollecting(this.order.data.target, IID_UnitAI)) { this.ProcessMessage("TargetInvalidated"); return true; } this.FaceTowardsTarget(this.order.data.target); this.SelectAnimation("collecting_treasure"); return false; }, "leave": function() { let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter); if (cmpTreasureCollecter) cmpTreasureCollecter.StopCollecting(); this.ResetAnimation(); }, "OutOfRange": function(msg) { this.SetNextState("APPROACHING"); }, "TargetInvalidated": function(msg) { this.SetNextState("FINDINGNEWTARGET"); }, }, "FINDINGNEWTARGET": { "enter": function() { let oldData = this.order.data; // Switch to the next order (if any). if (this.FinishOrder()) return true; // If autocontinue explicitly disabled (e.g. by AI) // then do nothing automatically. if (!oldData.autocontinue) return false; let nearbyTreasure = this.FindNearbyTreasure(this.TargetPosOrEntPos(oldData.target)); if (nearbyTreasure) this.CollectTreasure(nearbyTreasure, oldData.autocontinue, true); return true; }, }, }, "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": { "APPROACHING": { "enter": function() { if (!this.CanGarrison(this.order.data.target)) { this.FinishOrder(); return true; } 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; Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity }); } return false; }, "leave": function() { if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } this.StopMoving(); }, "MovementUpdate": function(msg) { if (!msg.likelyFailure && !msg.likelySuccess) return; if (this.CheckGarrisonRange(this.order.data.target)) this.SetNextState("GARRISONING"); else { // Unable to reach the target, try again (or follow if it is a moving target) // except if the target does not exist anymore or its orders have changed. if (this.pickup) { let cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI); if (!cmpUnitAI || (!cmpUnitAI.HasPickupOrder(this.entity) && !cmpUnitAI.IsIdle())) this.FinishOrder(); } } }, }, "GARRISONING": { "enter": function() { let target = this.order.data.target; let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable); if (!cmpGarrisonable || !cmpGarrisonable.Garrison(target)) { this.FinishOrder(); return true; } if (this.formationController) { let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { let 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(); } 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.isIdle = false; 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.SetGarrisoned = function() { // UnitAI caches its own garrisoned state for performance. this.isGarrisoned = true; this.SetImmobile(); }; UnitAI.prototype.UnsetGarrisoned = function() { delete this.isGarrisoned; this.SetMobile(); }; 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() { if (this.isImmobile) return; this.isImmobile = true; Engine.PostMessage(this.entity, MT_UnitAbleToMoveChanged, { "entity": this.entity, "ableToMove": this.AbleToMove() }); }; UnitAI.prototype.SetMobile = function() { if (!this.isImmobile) return; delete this.isImmobile; 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 (un)packing, in which case we only clear the order queue. if (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.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}); + 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 //// +// FSM linkage functions // Setting the next state to the current state will leave/re-enter the top-most substate. // Must be called from inside the FSM. UnitAI.prototype.SetNextState = function(state) { this.UnitFsm.SetNextState(this, state); }; // Must be called from inside the FSM. 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). * Must be called from inside the FSM. */ 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 } - ); + let ret = this.UnitFsm.ProcessMessage(this, { + "type": "Order."+this.order.type, + "data": this.order.data + }); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); return ret; } 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; - this.UnitFsm.ProcessMessage(this, - { "type": "Order."+this.order.type, "data": this.order.data } - ); + this.UnitFsm.ProcessMessage(this, { + "type": "Order."+this.order.type, + "data": this.order.data + }); } 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; - this.UnitFsm.ProcessMessage(this, - { "type": "Order."+this.order.type, "data": this.order.data } - ); + this.UnitFsm.ProcessMessage(this, { + "type": "Order."+this.order.type, + "data": this.order.data + }); } 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}); + 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); } // 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); } 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 cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable); if (!cmpGarrisonable || !cmpGarrisonable.UnGarrison(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}); + 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}); + this.UnitFsm.ProcessMessage(this, { "type": "Attacked", "data": msg }); }; UnitAI.prototype.OnGuardedAttacked = function(msg) { - this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data}); + 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}); + this.UnitFsm.ProcessMessage(this, { "type": "PackFinished", "packed": msg.packed }); }; /** * A general function to process messages sent from components. * @param {string} type - The type of message to process. * @param {Object} msg - Optionally extra data to use. */ UnitAI.prototype.ProcessMessage = function(type, msg) { this.UnitFsm.ProcessMessage(this, { "type": type, "data": msg }); }; -//// Helper functions to be called by the FSM //// +// 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()); }; /** * Returns the entity ID of the nearest treasure. * "Nearest" is nearest from @param position. */ UnitAI.prototype.FindNearbyTreasure = function(position) { if (!position) return undefined; let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter); if (!cmpTreasureCollecter) return undefined; let players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); 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_Treasure, false); return nearby.find(ent => cmpTreasureCollecter.CanCollect(ent)); }; /** * 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)) + 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; + 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 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 //// +// 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, pushFront) { if (this.expectedRoute) this.expectedRoute = undefined; if (pushFront) this.PushOrderFront(type, data); else 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. // ToDo: Fix that by checking for the ability to move on orders that need that. 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, pushFront) { 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.RemoveGuard(); } this.AddOrder("Guard", { "target": target, "force": false }, queued, pushFront); }; /** * @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, pushFront) { if (!pushFront && this.expectedRoute && queued) this.expectedRoute.push({ "x": x, "z": z }); else this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued, pushFront); }; /** * Adds walk to point range order to queue, forced by the player. */ UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued, pushFront) { this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued, pushFront); }; /** * Adds stop order to queue, forced by the player. */ UnitAI.prototype.Stop = function(queued, pushFront) { this.AddOrder("Stop", { "force": true }, queued, pushFront); }; /** * 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, pushFront) { this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued, pushFront); }; /** * 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, pushFront = false) { this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront); }; UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) { if (!this.CanPatrol()) { this.Walk(x, z, queued); return; } this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued, pushFront); }; /** * 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, pushFront = 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, pushFront); 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, pushFront); }; /** * Adds garrison order to the queue, forced by the player. */ UnitAI.prototype.Garrison = function(target, queued, pushFront) { if (target == this.entity) return; if (!this.CanGarrison(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", { "target": target, "force": true }, queued, pushFront); }; /** * Adds ungarrison order to the queue. */ UnitAI.prototype.Ungarrison = function() { if (!this.isGarrisoned) return; this.AddOrder("Ungarrison", null, false); }; /** * Adds gather order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Gather = function(target, queued, pushFront) { this.PerformGather(target, queued, true, pushFront); }; /** * Internal function to abstract the force parameter. */ UnitAI.prototype.PerformGather = function(target, queued, force, pushFront = false) { 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, pushFront); }; /** * 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, pushFront) { 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, pushFront); else this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued, pushFront); }; /** * Adds heal order to the queue, forced by the player. */ UnitAI.prototype.Heal = function(target, queued, pushFront) { 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, pushFront); }; /** * Adds return resource order to the queue, forced by the player. */ UnitAI.prototype.ReturnResource = function(target, queued, pushFront) { if (!this.CanReturnResource(target, true)) { this.WalkToTarget(target, queued); return; } this.AddOrder("ReturnResource", { "target": target, "force": true }, queued, pushFront); }; /** * Adds order to collect a treasure to queue, forced by the player. */ UnitAI.prototype.CollectTreasure = function(target, autocontinue, queued) { this.AddOrder("CollectTreasure", { "target": target, "autocontinue": autocontinue, "force": true }, queued); }; /** * Adds order to collect a treasure to queue, forced by the player. */ UnitAI.prototype.CollectTreasureNearPosition = function(posX, posZ, autocontinue, queued) { this.AddOrder("CollectTreasureNearPosition", { "x": posX, "z": posZ, "target": target, "autocontinue": autocontinue, "force": false }, 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, pushFront) { 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, pushFront); } else { if (this.IsFormationController()) this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued, pushFront]); else this.WalkToTarget(cmpTrader.GetFirstMarket(), queued, pushFront); 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, pushFront) { 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, pushFront); }; /** * Adds flee order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.Flee = function(target, queued, pushFront) { this.AddOrder("Flee", { "target": target, "force": false }, queued, pushFront); }; UnitAI.prototype.Cheer = function() { this.PushOrderFront("Cheer", { "force": false }); }; UnitAI.prototype.Pack = function(queued, pushFront) { if (this.CanPack()) this.AddOrder("Pack", { "force": true }, queued, pushFront); }; UnitAI.prototype.Unpack = function(queued, pushFront) { if (this.CanUnpack()) this.AddOrder("Unpack", { "force": true }, queued, pushFront); }; UnitAI.prototype.CancelPack = function(queued, pushFront) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) this.AddOrder("CancelPack", { "force": true }, queued, pushFront); }; UnitAI.prototype.CancelUnpack = function(queued, pushFront) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) this.AddOrder("CancelUnpack", { "force": true }, queued, pushFront); }; 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)) + if (targetClasses.attack && cmpIdentity && + !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) continue; - if (targetClasses.avoid && cmpIdentity - && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) + 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)) + if (cmpIdentity && targetClasses.attack && + !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) continue; - if (cmpIdentity && targetClasses.avoid - && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) + 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}; + 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, false); return true; } return false; }; -//// Helper functions //// +// 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 //// +// Formation specific functions UnitAI.prototype.IsAttackingAsFormation = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - return cmpAttack && cmpAttack.CanAttackAsFormation() - && this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING"; + 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_Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 25087) @@ -1,162 +1,162 @@ Engine.LoadHelperScript("MultiKeyMap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/RangeOverlayManager.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("Auras.js"); Engine.LoadComponentScript("ModifiersManager.js"); var playerID = [0, 1, 2]; var playerEnt = [10, 11, 12]; var playerState = ["active", "active", "active"]; var sourceEnt = 20; var targetEnt = 30; var auraRange = 40; var template = { "Identity": { "Classes": { "_string": "CorrectClass OtherClass" } } }; global.AuraTemplates = { "Get": name => { let template = { "type": name, "affectedPlayers": ["Ally"], "affects": ["CorrectClass"], "modifications": [{ "value": "Component/Value", "add": 10 }], "auraName": "name", "auraDescription": "description" }; if (name == "range") template.radius = auraRange; return template; } }; function testAuras(name, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": idx => playerEnt[idx], "GetNumPlayers": () => 3, "GetAllPlayers": () => playerID }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "CreateActiveQuery": (ent, minRange, maxRange, players, iid, flags) => 1, "EnableActiveQuery": id => {}, "ResetActiveQuery": id => {}, "DisableActiveQuery": id => {}, "DestroyActiveQuery": id => {}, "GetEntityFlagMask": identifier => {}, "GetEntitiesByPlayer": id => [30, 31, 32] }); AddMock(playerEnt[1], IID_Player, { "IsAlly": id => id == playerID[1] || id == playerID[2], "IsEnemy": id => id != playerID[1] || id != playerID[2], "GetPlayerID": () => playerID[1], "GetState": () => playerState[1] }); AddMock(playerEnt[2], IID_Player, { "IsAlly": id => id == playerID[1] || id == playerID[2], "IsEnemy": id => id != playerID[1] || id != playerID[2], "GetPlayerID": () => playerID[2], "GetState": () => playerState[2] }); AddMock(targetEnt, IID_Identity, { "GetClassesList": () => ["CorrectClass", "OtherClass"] }); AddMock(sourceEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); if (name != "player" || playerEnt.indexOf(targetEnt) == -1) { AddMock(targetEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); AddMock(targetEnt, IID_Ownership, { "GetOwner": () => playerID[1] }); } if (playerEnt.indexOf(sourceEnt) == -1) AddMock(sourceEnt, IID_Ownership, { "GetOwner": () => playerID[1] }); let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {}); - cmpModifiersManager.OnGlobalPlayerEntityChanged({ player: playerID[1], from: -1, to: playerEnt[1] }); - cmpModifiersManager.OnGlobalPlayerEntityChanged({ player: playerID[2], from: -1, to: playerEnt[2] }); + cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": playerID[1], "from": -1, "to": playerEnt[1] }); + cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": playerID[2], "from": -1, "to": playerEnt[2] }); let cmpAuras = ConstructComponent(sourceEnt, "Auras", { "_string": name }); test_function(name, cmpAuras); } targetEnt = playerEnt[playerID[2]]; testAuras("player", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); }); targetEnt = 30; // Test the case when the aura source is a player entity. sourceEnt = 11; testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); }); sourceEnt = 20; testAuras("range", (name, cmpAuras) => { cmpAuras.OnRangeUpdate({ "tag": 1, "added": [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); cmpAuras.OnRangeUpdate({ "tag": 1, "added": [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("garrisonedUnits", (name, cmpAuras) => { cmpAuras.OnGarrisonedUnitsChanged({ "added": [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.OnGarrisonedUnitsChanged({ "added": [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("garrison", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasGarrisonAura(), true); cmpAuras.ApplyGarrisonAura(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.RemoveGarrisonAura(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("formation", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasFormationAura(), true); cmpAuras.ApplyFormationAura([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.RemoveFormationAura([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 15); AddMock(sourceEnt, IID_Ownership, { "GetOwner": () => -1 }); cmpAuras.OnOwnershipChanged({ "from": sourceEnt, "to": -1 }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 5); }); playerState[1] = "defeated"; testAuras("global", (name, cmpAuras) => { cmpAuras.OnGlobalPlayerDefeated({ "playerId": playerID[1] }); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[2], template), 5); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 25087) @@ -1,199 +1,199 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Capturable.js"); var testData = { "structure": 20, "playerID": 1, "regenRate": 2, "garrisonedEntities": [30, 31, 32, 33], "garrisonRegenRate": 5, "decay": false, "decayRate": 30, "maxCapturePoints": 3000, "neighbours": [20, 0, 20, 10] }; function testCapturable(testData, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": (ent, iid, funcname, time, repeattime, data) => {}, "CancelTimer": timer => {} }); AddMock(testData.structure, IID_Ownership, { "GetOwner": () => testData.playerID, "SetOwner": id => {} }); AddMock(testData.structure, IID_GarrisonHolder, { "GetEntities": () => testData.garrisonedEntities }); AddMock(testData.structure, IID_Fogging, { "Activate": () => {} }); AddMock(10, IID_Player, { "IsEnemy": id => id != 0 }); AddMock(11, IID_Player, { "IsEnemy": id => id != 1 && id != 2 }); AddMock(12, IID_Player, { "IsEnemy": id => id != 1 && id != 2 }); AddMock(13, IID_Player, { "IsEnemy": id => id != 3 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": () => 4, "GetPlayerByID": id => 10 + id }); AddMock(testData.structure, IID_StatisticsTracker, { "LostEntity": () => {}, "CapturedBuilding": () => {} }); let cmpCapturable = ConstructComponent(testData.structure, "Capturable", { "CapturePoints": testData.maxCapturePoints, "RegenRate": testData.regenRate, "GarrisonRegenRate": testData.garrisonRegenRate }); AddMock(testData.structure, IID_TerritoryDecay, { "IsDecaying": () => testData.decay, "GetDecayRate": () => testData.decayRate, "GetConnectedNeighbours": () => testData.neighbours }); TS_ASSERT_EQUALS(cmpCapturable.GetRegenRate(), testData.regenRate + testData.garrisonRegenRate * testData.garrisonedEntities.length); test_function(cmpCapturable); Engine.PostMessage = (ent, iid, message) => {}; } // Tests initialisation of the capture points when the entity is created testCapturable(testData, cmpCapturable => { Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate(), "territoryDecay": 0 }); }; cmpCapturable.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": testData.playerID }); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 3000, 0, 0]); }); // Tests if the message is sent when capture points change testCapturable(testData, cmpCapturable => { - cmpCapturable.SetCapturePoints([0, 2000, 0 , 1000]); + cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000, 0, 1000]); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000, 0, 1000] }); }; cmpCapturable.RegisterCapturePointsChanged(); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CapturePointsChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000 - 100, 0, 1000 + 100] }); if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate(), "territoryDecay": 0 }); }; TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(100, 3), 100); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000 - 100, 0, 1000 + 100]); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_EQUALS(cmpCapturable.Reduce(2500, 3), 2000); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 0, 0, 3000]); }); function testRegen(testData, capturePointsIn, capturePointsOut, regenerating) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(capturePointsIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.regenerating, regenerating); }; cmpCapturable.TimerTick(capturePointsIn); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), capturePointsOut); }); } // With our testData, the total regen rate is 22. That should be taken from the ennemies testRegen(testData, [12, 2950, 2, 36], [1, 2972, 2, 25], true); testRegen(testData, [0, 2994, 2, 4], [0, 2998, 2, 0], true); testRegen(testData, [0, 2998, 2, 0], [0, 2998, 2, 0], false); // If the regeneration rate becomes negative, capture points are given in favour of gaia testData.regenRate = -32; // With our testData, the total regen rate is -12. That should be taken from all players to gaia testRegen(testData, [100, 2800, 50, 50], [112, 2796, 46, 46], true); testData.regenRate = 2; function testDecay(testData, capturePointsIn, capturePointsOut) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(capturePointsIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.territoryDecay, testData.decayRate); }; cmpCapturable.TimerTick(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), capturePointsOut); }); } testData.decay = true; // With our testData, the decay rate is 30, that should be given to all neighbours with weights [20/50, 0, 20/50, 10/50], then it regens. testDecay(testData, [2900, 35, 10, 55], [2901, 27, 22, 50]); testData.decay = false; // Tests Reduce function testReduce(testData, amount, player, taken) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0, 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(amount, player), taken); }); } testReduce(testData, 50, 3, 50); testReduce(testData, 50, 2, 50); testReduce(testData, 50, 1, 50); testReduce(testData, -50, 3, 0); testReduce(testData, 50, 0, 50); testReduce(testData, 0, 3, 0); testReduce(testData, 1500, 3, 1500); testReduce(testData, 2000, 3, 2000); testReduce(testData, 3000, 3, 2000); // Test defeated player testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([500, 1000, 0, 250]); cmpCapturable.OnGlobalPlayerDefeated({ "playerId": 3 }); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [750, 1000, 0, 0]); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js (revision 25087) @@ -1,170 +1,170 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Heal.js"); const entity = 60; const player = 1; -const otherPlayer = 2 +const otherPlayer = 2; let template = { "Range": 20, "RangeOverlay": { "LineTexture": "heal_overlay_range.png", "LineTextureMask": "heal_overlay_range_mask.png", "LineThickness": 0.35 }, "Health": 5, "Interval": 2000, "UnhealableClasses": { "_string": "Cavalry" }, "HealableClasses": { "_string": "Support Infantry" }, }; AddMock(entity, IID_Ownership, { "GetOwner": () => player }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => player }); AddMock(player, IID_Player, { "IsAlly": () => true }); AddMock(otherPlayer, IID_Player, { "IsAlly": () => false }); ApplyValueModificationsToEntity = function(value, stat, ent) { if (ent != entity) return stat; switch (value) { case "Heal/Health": return stat + 100; case "Heal/Interval": return stat + 200; case "Heal/Range": return stat + 300; default: return stat; } }; let cmpHeal = ConstructComponent(60, "Heal", template); // Test Getters TS_ASSERT_EQUALS(cmpHeal.GetInterval(), 2000 + 200); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetTimers(), { "prepare": 1000, "repeat": 2000 + 200 }); TS_ASSERT_EQUALS(cmpHeal.GetHealth(), 5 + 100); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRange(), { "min": 0, "max": 20 + 300 }); TS_ASSERT_EQUALS(cmpHeal.GetHealableClasses(), "Support Infantry"); TS_ASSERT_EQUALS(cmpHeal.GetUnhealableClasses(), "Cavalry"); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.GetRangeOverlays(), [{ "radius": 20 + 300, "texture": "heal_overlay_range.png", "textureMask": "heal_overlay_range_mask.png", "thickness": 0.35 }]); // Test PerformHeal let target = 70; AddMock(target, IID_Ownership, { "GetOwner": () => player }); let targetClasses; AddMock(target, IID_Identity, { "GetClassesList": () => targetClasses }); let increased; let unhealable = false; AddMock(target, IID_Health, { "GetMaxHitpoints": () => 700, "Increase": amount => { increased = true; TS_ASSERT_EQUALS(amount, 5 + 100); return { "old": 600, "new": 600 + 5 + 100 }; }, "IsUnhealable": () => unhealable }); cmpHeal.PerformHeal(target); TS_ASSERT(increased); let looted; AddMock(target, IID_Loot, { "GetXp": () => { looted = true; return 80; } }); let promoted; AddMock(entity, IID_Promotion, { "IncreaseXp": amount => { promoted = true; TS_ASSERT_EQUALS(amount, (5 + 100) * 80 / 700); } }); increased = false; cmpHeal.PerformHeal(target); TS_ASSERT(increased && looted && promoted); // Test OnValueModification let updated; AddMock(entity, IID_UnitAI, { "UpdateRangeQueries": () => { updated = true; } }); cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/Health"] }); TS_ASSERT(!updated); cmpHeal.OnValueModification({ "component": "Heal", "valueNames": ["Heal/Range"] }); TS_ASSERT(updated); // Test CanHeal. targetClasses = ["Infantry", "Hero"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), true); targetClasses = ["Hero"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); targetClasses = ["Infantry", "Cavalry"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); targetClasses = ["Cavalry"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); targetClasses = ["Infantry"]; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), true); unhealable = true; TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(target), false); let otherTarget = 71; AddMock(otherTarget, IID_Ownership, { "GetOwner": () => player }); AddMock(otherTarget, IID_Health, { "IsUnhealable": () => false }); TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(otherTarget), false); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Math.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Math.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Math.js (revision 25087) @@ -1,144 +1,144 @@ /** * Tests for consistent and correct math results */ - // +0 is different than -0, but standard equality won't test that +// +0 is different than -0, but standard equality won't test that function isNegativeZero(z) { return z === 0 && 1/z === -Infinity; } function isPositiveZero(z) { return z === 0 && 1/z === Infinity; } // rounding TS_ASSERT_EQUALS(0.1+0.2, 0.30000000000000004); TS_ASSERT_EQUALS(0.1+0.7+0.3, 1.0999999999999999); // cos TS_ASSERT_EQUALS(Math.cos(Math.PI/2), 0); TS_ASSERT_UNEVAL_EQUALS(Math.cos(NaN), NaN); TS_ASSERT_EQUALS(Math.cos(0), 1); TS_ASSERT_EQUALS(Math.cos(-0), 1); TS_ASSERT_UNEVAL_EQUALS(Math.cos(Infinity), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.cos(-Infinity), NaN); // sin TS_ASSERT_EQUALS(Math.sin(Math.PI), 0); TS_ASSERT_UNEVAL_EQUALS(Math.sin(NaN), NaN); TS_ASSERT(isPositiveZero(Math.sin(0))); // TS_ASSERT(isNegativeZero(Math.sin(-0))); TODO: doesn't match spec TS_ASSERT_UNEVAL_EQUALS(Math.sin(Infinity), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.sin(-Infinity), NaN); TS_ASSERT_EQUALS(Math.sin(1e-15), 7.771561172376096e-16); // atan TS_ASSERT_UNEVAL_EQUALS(Math.atan(NaN), NaN); TS_ASSERT(isPositiveZero(Math.atan(0))); TS_ASSERT(isNegativeZero(Math.atan(-0))); TS_ASSERT_EQUALS(Math.atan(Infinity), Math.PI/2); TS_ASSERT_EQUALS(Math.atan(-Infinity), -Math.PI/2); TS_ASSERT_EQUALS(Math.atan(1e-310), 1.00000000003903e-310); TS_ASSERT_EQUALS(Math.atan(100), 1.5607966601078411); // atan2 TS_ASSERT_UNEVAL_EQUALS(Math.atan2(NaN, 1), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.atan2(1, NaN), NaN); TS_ASSERT_EQUALS(Math.atan2(1, 0), Math.PI/2); TS_ASSERT_EQUALS(Math.atan2(1, -0), Math.PI/2); TS_ASSERT(isPositiveZero(Math.atan2(0, 1))); TS_ASSERT(isPositiveZero(Math.atan2(0, 0))); TS_ASSERT_EQUALS(Math.atan2(0, -0), Math.PI); TS_ASSERT_EQUALS(Math.atan2(0, -1), Math.PI); TS_ASSERT(isNegativeZero(Math.atan2(-0, 1))); TS_ASSERT(isNegativeZero(Math.atan2(-0, 0))); TS_ASSERT_EQUALS(Math.atan2(-0, -0), -Math.PI); TS_ASSERT_EQUALS(Math.atan2(-0, -1), -Math.PI); TS_ASSERT_EQUALS(Math.atan2(-1, 0), -Math.PI/2); TS_ASSERT_EQUALS(Math.atan2(-1, -0), -Math.PI/2); TS_ASSERT(isPositiveZero(Math.atan2(1.7e308, Infinity))); TS_ASSERT_EQUALS(Math.atan2(1.7e308, -Infinity), Math.PI); TS_ASSERT(isNegativeZero(Math.atan2(-1.7e308, Infinity))); TS_ASSERT_EQUALS(Math.atan2(-1.7e308, -Infinity), -Math.PI); TS_ASSERT_EQUALS(Math.atan2(Infinity, -1.7e308), Math.PI/2); TS_ASSERT_EQUALS(Math.atan2(-Infinity, 1.7e308), -Math.PI/2); TS_ASSERT_EQUALS(Math.atan2(Infinity, Infinity), Math.PI/4); TS_ASSERT_EQUALS(Math.atan2(Infinity, -Infinity), 3*Math.PI/4); TS_ASSERT_EQUALS(Math.atan2(-Infinity, Infinity), -Math.PI/4); TS_ASSERT_EQUALS(Math.atan2(-Infinity, -Infinity), -3*Math.PI/4); TS_ASSERT_EQUALS(Math.atan2(1e-310, 2), 5.0000000001954e-311); // exp TS_ASSERT_UNEVAL_EQUALS(Math.exp(NaN), NaN); TS_ASSERT_EQUALS(Math.exp(0), 1); TS_ASSERT_EQUALS(Math.exp(-0), 1); TS_ASSERT_EQUALS(Math.exp(Infinity), Infinity); TS_ASSERT(isPositiveZero(Math.exp(-Infinity))); TS_ASSERT_EQUALS(Math.exp(10), 22026.465794806707); // log TS_ASSERT_UNEVAL_EQUALS(Math.log("NaN"), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.log(-1), NaN); TS_ASSERT_EQUALS(Math.log(0), -Infinity); TS_ASSERT_EQUALS(Math.log(-0), -Infinity); TS_ASSERT(isPositiveZero(Math.log(1))); TS_ASSERT_EQUALS(Math.log(Infinity), Infinity); TS_ASSERT_EQUALS(Math.log(Math.E), 0.9999999999999991); TS_ASSERT_EQUALS(Math.log(Math.E*Math.E*Math.E), 2.999999999999999); // pow TS_ASSERT_EQUALS(Math.pow(NaN, 0), 1); TS_ASSERT_EQUALS(Math.pow(NaN, -0), 1); TS_ASSERT_UNEVAL_EQUALS(Math.pow(NaN, 100), NaN); TS_ASSERT_EQUALS(Math.pow(1.7e308, Infinity), Infinity); TS_ASSERT_EQUALS(Math.pow(-1.7e308, Infinity), Infinity); TS_ASSERT(isPositiveZero(Math.pow(1.7e308, -Infinity))); TS_ASSERT(isPositiveZero(Math.pow(-1.7e308, -Infinity))); TS_ASSERT_UNEVAL_EQUALS(Math.pow(1, Infinity), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.pow(-1, Infinity), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.pow(1, -Infinity), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.pow(-1, -Infinity), NaN); TS_ASSERT(isPositiveZero(Math.pow(1e-310, Infinity))); TS_ASSERT(isPositiveZero(Math.pow(-1e-310, Infinity))); TS_ASSERT_EQUALS(Math.pow(1e-310, -Infinity), Infinity); TS_ASSERT_EQUALS(Math.pow(-1e-310, -Infinity), Infinity); TS_ASSERT_EQUALS(Math.pow(Infinity, 1e-310), Infinity); TS_ASSERT(isPositiveZero(Math.pow(Infinity, -1e-310))); TS_ASSERT_EQUALS(Math.pow(-Infinity, 101), -Infinity); TS_ASSERT_EQUALS(Math.pow(-Infinity, 1.7e308), Infinity); TS_ASSERT(isNegativeZero(Math.pow(-Infinity, -101))); TS_ASSERT(isPositiveZero(Math.pow(-Infinity, -1.7e308))); TS_ASSERT(isPositiveZero(Math.pow(0, 1e-310))); TS_ASSERT_EQUALS(Math.pow(0, -1e-310), Infinity); TS_ASSERT(isNegativeZero(Math.pow(-0, 101))); TS_ASSERT(isPositiveZero(Math.pow(-0, 1e-310))); TS_ASSERT_EQUALS(Math.pow(-0, -101), -Infinity); TS_ASSERT_EQUALS(Math.pow(-0, -1e-310), Infinity); TS_ASSERT_UNEVAL_EQUALS(Math.pow(-1.7e308, 1e-310), NaN); TS_ASSERT_EQUALS(Math.pow(Math.PI, -100), 1.9275814160560185e-50); // sqrt TS_ASSERT_UNEVAL_EQUALS(Math.sqrt(NaN), NaN); TS_ASSERT_UNEVAL_EQUALS(Math.sqrt(-1e-323), NaN); TS_ASSERT(isPositiveZero(Math.sqrt(0))); TS_ASSERT(isNegativeZero(Math.sqrt(-0))); TS_ASSERT_EQUALS(Math.sqrt(Infinity), Infinity); TS_ASSERT_EQUALS(Math.sqrt(1e-323), 3.1434555694052576e-162); // square TS_ASSERT_UNEVAL_EQUALS(Math.square(NaN), NaN); TS_ASSERT(isPositiveZero(Math.square(0))); TS_ASSERT(isPositiveZero(Math.square(-0))); TS_ASSERT_EQUALS(Math.square(Infinity), Infinity); -TS_ASSERT_EQUALS(Math.square(1.772979291871526e-81),3.143455569405258e-162); +TS_ASSERT_EQUALS(Math.square(1.772979291871526e-81), 3.143455569405258e-162); TS_ASSERT_EQUALS(Math.square(1e+155), Infinity); TS_ASSERT_UNEVAL_EQUALS(Math.square(1), 1); TS_ASSERT_UNEVAL_EQUALS(Math.square(20), 400); TS_ASSERT_UNEVAL_EQUALS(Math.square(300), 90000); TS_ASSERT_UNEVAL_EQUALS(Math.square(4000), 16000000); TS_ASSERT_UNEVAL_EQUALS(Math.square(50000), 2500000000); TS_ASSERT_UNEVAL_EQUALS(Math.square(-3), 9); TS_ASSERT_UNEVAL_EQUALS(Math.square(-8), 64); TS_ASSERT_UNEVAL_EQUALS(Math.square(0.123), 0.015129); // euclid distance TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance2D(0, 0, 3, 4), 5); TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance2D(-4, -4, -5, -4), 1); TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance2D(1e+140, 1e+140, 0, 0), 1.414213562373095e+140); TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance3D(0, 0, 0, 20, 48, 165), 173); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Population.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Population.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Population.js (revision 25087) @@ -1,111 +1,111 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Population.js"); Engine.LoadComponentScript("Population.js"); const player = 1; const entity = 11; let entPopBonus = 5; Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => currentValue ); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => player }); let cmpPopulation = ConstructComponent(entity, "Population", { "Bonus": entPopBonus }); // Test ownership change adds bonus. let cmpPlayer = AddMock(player, IID_Player, { "AddPopulationBonuses": bonus => TS_ASSERT_EQUALS(bonus, entPopBonus) }); let spy = new Spy(cmpPlayer, "AddPopulationBonuses"); cmpPopulation.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": player }); TS_ASSERT_EQUALS(spy._called, 1); // Test ownership change removes bonus. cmpPlayer = AddMock(player, IID_Player, { "AddPopulationBonuses": bonus => TS_ASSERT_EQUALS(bonus, -entPopBonus) }); spy = new Spy(cmpPlayer, "AddPopulationBonuses"); cmpPopulation.OnOwnershipChanged({ "from": player, "to": INVALID_PLAYER }); TS_ASSERT_EQUALS(spy._called, 1); // Test value modifications. // Test no change. Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => currentValue ); cmpPlayer = AddMock(player, IID_Player, { "AddPopulationBonuses": () => TS_ASSERT(false) }); cmpPopulation.OnValueModification({ "component": "bogus" }); cmpPopulation.OnValueModification({ "component": "Population" }); // Test changes. AddMock(entity, IID_Ownership, { "GetOwner": () => player }); let difference = 3; Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => currentValue + difference ); cmpPlayer = AddMock(player, IID_Player, { "AddPopulationBonuses": bonus => TS_ASSERT_EQUALS(bonus, difference) }); spy = new Spy(cmpPlayer, "AddPopulationBonuses"); // Foundations don't count yet. AddMock(entity, IID_Foundation, {}); cmpPopulation.OnValueModification({ "component": "Population" }); TS_ASSERT_EQUALS(spy._called, 0); DeleteMock(entity, IID_Foundation); cmpPopulation.OnValueModification({ "component": "Population" }); TS_ASSERT_EQUALS(spy._called, 1); // Reset to no bonus. cmpPlayer = AddMock(player, IID_Player, { "AddPopulationBonuses": bonus => TS_ASSERT_EQUALS(bonus, -3) }); -difference = 0 +difference = 0; Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => currentValue + difference ); spy = new Spy(cmpPlayer, "AddPopulationBonuses"); cmpPopulation.OnValueModification({ "component": "Population" }); TS_ASSERT_EQUALS(spy._called, 1); // Test negative change. difference = -2; Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => currentValue + difference ); cmpPlayer = AddMock(player, IID_Player, { "AddPopulationBonuses": bonus => TS_ASSERT_EQUALS(bonus, difference) }); spy = new Spy(cmpPlayer, "AddPopulationBonuses"); cmpPopulation.OnValueModification({ "component": "Population" }); TS_ASSERT_EQUALS(spy._called, 1); // Test newly created entities also get affected by modifications. difference = 3; Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => currentValue + difference ); cmpPlayer = AddMock(player, IID_Player, { "AddPopulationBonuses": bonus => TS_ASSERT_EQUALS(bonus, entPopBonus + difference) }); spy = new Spy(cmpPlayer, "AddPopulationBonuses"); cmpPopulation.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": player }); TS_ASSERT_EQUALS(spy._called, 1); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js (revision 25087) @@ -1,18 +1,18 @@ Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("TechnologyManager.js"); global.TechnologyTemplates = { "GetAll": () => [] }; let cmpTechnologyManager = ConstructComponent(SYSTEM_ENTITY, "TechnologyManager", {}); // Test CheckTechnologyRequirements let template = { "requirements": { "all": [{ "entity": { "class": "Village", "number": 5 } }, { "civ": "athen" }] } }; -cmpTechnologyManager.classCounts["Village"] = 2; +cmpTechnologyManager.classCounts.Village = 2; TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen")), false); TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen"), true), true); TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "maur"), true), false); -cmpTechnologyManager.classCounts["Village"] = 6; +cmpTechnologyManager.classCounts.Village = 6; TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen")), true); TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "maur")), false); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Timer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Timer.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Timer.js (revision 25087) @@ -1,97 +1,97 @@ Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Timer.js"); Engine.RegisterInterface("Test"); var cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); var fired = []; AddMock(10, IID_Test, { - Callback: function(data, lateness) { + "Callback": function(data, lateness) { fired.push([data, lateness]); } }); var cancelId; AddMock(20, IID_Test, { - Callback: function(data, lateness) { + "Callback": function(data, lateness) { fired.push([data, lateness]); cmpTimer.CancelTimer(cancelId); } }); TS_ASSERT_EQUALS(cmpTimer.GetTime(), 0); cmpTimer.OnUpdate({ "turnLength": 1/3 }); TS_ASSERT_EQUALS(cmpTimer.GetTime(), 333); cmpTimer.SetTimeout(10, IID_Test, "Callback", 1000, "a"); cmpTimer.SetTimeout(10, IID_Test, "Callback", 1200, "b"); cmpTimer.OnUpdate({ "turnLength": 0.5 }); TS_ASSERT_UNEVAL_EQUALS(fired, []); cmpTimer.OnUpdate({ "turnLength": 0.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["a",0]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["a", 0]]); cmpTimer.OnUpdate({ "turnLength": 0.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["a",0], ["b",300]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["a", 0], ["b", 300]]); cmpTimer.OnUpdate({ "turnLength": 0.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["a",0], ["b",300]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["a", 0], ["b", 300]]); fired = []; var c = cmpTimer.SetTimeout(10, IID_Test, "Callback", 1000, "c"); var d = cmpTimer.SetTimeout(10, IID_Test, "Callback", 1000, "d"); var e = cmpTimer.SetTimeout(10, IID_Test, "Callback", 1000, "e"); cmpTimer.CancelTimer(d); cmpTimer.OnUpdate({ "turnLength": 1.0 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["c",0], ["e",0]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["c", 0], ["e", 0]]); fired = []; var r = cmpTimer.SetInterval(10, IID_Test, "Callback", 500, 1000, "r"); cmpTimer.OnUpdate({ "turnLength": 0.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["r",0]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["r", 0]]); cmpTimer.OnUpdate({ "turnLength": 0.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["r",0]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["r", 0]]); cmpTimer.OnUpdate({ "turnLength": 0.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["r",0], ["r",0]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["r", 0], ["r", 0]]); cmpTimer.OnUpdate({ "turnLength": 3.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["r",0], ["r",0], ["r",2500], ["r",1500], ["r",500]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["r", 0], ["r", 0], ["r", 2500], ["r", 1500], ["r", 500]]); cmpTimer.CancelTimer(r); cmpTimer.OnUpdate({ "turnLength": 3.5 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["r",0], ["r",0], ["r",2500], ["r",1500], ["r",500]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["r", 0], ["r", 0], ["r", 2500], ["r", 1500], ["r", 500]]); fired = []; cancelId = cmpTimer.SetInterval(20, IID_Test, "Callback", 500, 1000, "s"); cmpTimer.OnUpdate({ "turnLength": 3.0 }); -TS_ASSERT_UNEVAL_EQUALS(fired, [["s",2500]]); +TS_ASSERT_UNEVAL_EQUALS(fired, [["s", 2500]]); fired = []; let f = cmpTimer.SetInterval(10, IID_Test, "Callback", 1000, 1000, "f"); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_UNEVAL_EQUALS(fired, [["f", 0]]); cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT_UNEVAL_EQUALS(fired, [["f", 0], ["f", 0]]); cmpTimer.UpdateRepeatTime(f, 500); cmpTimer.OnUpdate({ "turnLength": 1.5 }); // Interval updated at next updated, so expecting latency here. TS_ASSERT_UNEVAL_EQUALS(fired, [["f", 0], ["f", 0], ["f", 500], ["f", 0]]); cmpTimer.OnUpdate({ "turnLength": 0.5 }); TS_ASSERT_UNEVAL_EQUALS(fired, [["f", 0], ["f", 0], ["f", 500], ["f", 0], ["f", 0]]); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 25087) @@ -1,450 +1,451 @@ Engine.LoadHelperScript("FSM.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Position.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Formation.js"); Engine.LoadComponentScript("UnitAI.js"); /** * Fairly straightforward test that entity renaming is handled * by unitAI states. These ought to be augmented with integration tests, ideally. */ function TestTargetEntityRenaming(init_state, post_state, setup) { ResetState(); const player_ent = 5; const target_ent = 6; AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": () => {}, "SetTimeout": () => {} }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": () => false }); let unitAI = ConstructComponent(player_ent, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive", "FleeDistance": 10 }); unitAI.OnCreate(); setup(unitAI, player_ent, target_ent); TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state); unitAI.OnGlobalEntityRenamed({ "entity": target_ent, "newentity": target_ent + 1 }); TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state); } TestTargetEntityRenaming( "INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE", (unitAI, player_ent, target_ent) => { unitAI.CanGarrison = (target) => target == target_ent; unitAI.MoveToGarrisonRange = (target) => target == target_ent; unitAI.AbleToMove = () => true; AddMock(target_ent, IID_GarrisonHolder, { "GetLoadingRange": () => ({ "max": 100, "min": 0 }), "CanPickup": () => false }); unitAI.Garrison(target_ent, false); } ); TestTargetEntityRenaming( "INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.IDLE", (unitAI, player_ent, target_ent) => { QueryBuilderListInterface = () => {}; unitAI.CheckTargetRange = () => true; unitAI.CanRepair = (target) => target == target_ent; unitAI.Repair(target_ent, false, false); } ); TestTargetEntityRenaming( "INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING", (unitAI, player_ent, target_ent) => { PositionHelper.DistanceBetweenEntities = () => 10; unitAI.CheckTargetRangeExplicit = () => false; AddMock(player_ent, IID_UnitMotion, { "MoveToTargetRange": () => true, "GetRunMultiplier": () => 1, "SetSpeedMultiplier": () => {}, "StopMoving": () => {} }); unitAI.Flee(target_ent, false); } ); /* Regression test. * Tests the FSM behaviour of a unit when walking as part of a formation, * then exiting the formation. * mode == 0: There is no enemy unit nearby. * mode == 1: There is a live enemy unit nearby. * mode == 2: There is a dead enemy unit nearby. */ function TestFormationExiting(mode) { ResetState(); var playerEntity = 5; var unit = 10; var enemy = 20; var controller = 30; AddMock(SYSTEM_ENTITY, IID_Timer, { - SetInterval: function() { }, - SetTimeout: function() { }, + "SetInterval": function() { }, + "SetTimeout": function() { }, }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { - CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags, accountForSize) { + "CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) { return 1; }, - EnableActiveQuery: function(id) { }, - ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; }, - DisableActiveQuery: function(id) { }, - GetEntityFlagMask: function(identifier) { }, + "EnableActiveQuery": function(id) { }, + "ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; }, + "DisableActiveQuery": function(id) { }, + "GetEntityFlagMask": function(identifier) { }, }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - GetCurrentTemplateName: function(ent) { return "special/formations/line_closed"; }, + "GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; }, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: function(id) { return playerEntity; }, - GetNumPlayers: function() { return 2; }, + "GetPlayerByID": function(id) { return playerEntity; }, + "GetNumPlayers": function() { return 2; }, }); AddMock(playerEntity, IID_Player, { - IsAlly: function() { return false; }, - IsEnemy: function() { return true; }, - GetEnemies: function() { return [2]; }, + "IsAlly": function() { return false; }, + "IsEnemy": function() { return true; }, + "GetEnemies": function() { return [2]; }, }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": () => true, "IsInPointRange": () => true }); var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); AddMock(unit, IID_Identity, { - GetClassesList: function() { return []; }, + "GetClassesList": function() { return []; }, }); AddMock(unit, IID_Ownership, { - GetOwner: function() { return 1; }, + "GetOwner": function() { return 1; }, }); AddMock(unit, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(); }, - GetPosition2D: function() { return new Vector2D(); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, + "GetTurretParent": function() { return INVALID_ENTITY; }, + "GetPosition": function() { return new Vector3D(); }, + "GetPosition2D": function() { return new Vector2D(); }, + "GetRotation": function() { return { "y": 0 }; }, + "IsInWorld": function() { return true; }, }); AddMock(unit, IID_UnitMotion, { "GetWalkSpeed": () => 1, "MoveToFormationOffset": (target, x, z) => {}, "MoveToTargetRange": (target, min, max) => true, "StopMoving": () => {}, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); AddMock(unit, IID_Vision, { - GetRange: function() { return 10; }, + "GetRange": function() { return 10; }, }); AddMock(unit, IID_Attack, { - GetRange: function() { return { "max": 10, "min": 0}; }, - GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, - GetBestAttackAgainst: function(t) { return "melee"; }, - GetPreference: function(t) { return 0; }, - GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, - CanAttack: function(v) { return true; }, - CompareEntitiesByPreference: function(a, b) { return 0; }, + "GetRange": function() { return { "max": 10, "min": 0 }; }, + "GetFullAttackRange": function() { return { "max": 40, "min": 0 }; }, + "GetBestAttackAgainst": function(t) { return "melee"; }, + "GetPreference": function(t) { return 0; }, + "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; }, + "CanAttack": function(v) { return true; }, + "CompareEntitiesByPreference": function(a, b) { return 0; }, }); unitAI.OnCreate(); unitAI.SetupAttackRangeQuery(1); if (mode == 1) { AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 10; }, + "GetHitpoints": function() { return 10; }, }); AddMock(enemy, IID_UnitAI, { "IsAnimal": () => "false", "IsDangerousAnimal": () => "false" }); } else if (mode == 2) AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 0; }, + "GetHitpoints": function() { return 0; }, }); let controllerFormation = ConstructComponent(controller, "Formation", { "FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppiness": 0 }); let controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); AddMock(controller, IID_Position, { - JumpTo: function(x, z) { this.x = x; this.z = z; }, - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(this.x, 0, this.z); }, - GetPosition2D: function() { return new Vector2D(this.x, this.z); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, - MoveOutOfWorld: () => {} + "JumpTo": function(x, z) { this.x = x; this.z = z; }, + "GetTurretParent": function() { return INVALID_ENTITY; }, + "GetPosition": function() { return new Vector3D(this.x, 0, this.z); }, + "GetPosition2D": function() { return new Vector2D(this.x, this.z); }, + "GetRotation": function() { return { "y": 0 }; }, + "IsInWorld": function() { return true; }, + "MoveOutOfWorld": () => {} }); AddMock(controller, IID_UnitMotion, { "GetWalkSpeed": () => 1, "StopMoving": () => {}, "SetSpeedMultiplier": () => {}, "MoveToPointRange": () => true, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); controllerAI.OnCreate(); TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE"); TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); controllerFormation.SetMembers([unit]); controllerAI.Walk(100, 100, false); TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING"); TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING"); controllerFormation.Disband(); unitAI.UnitFsm.ProcessMessage(unitAI, { "type": "Timer" }); if (mode == 0) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); else if (mode == 1) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); else if (mode == 2) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); else TS_FAIL("invalid mode"); } function TestMoveIntoFormationWhileAttacking() { ResetState(); var playerEntity = 5; var controller = 10; var enemy = 20; var unit = 30; var units = []; var unitCount = 8; var unitAIs = []; AddMock(SYSTEM_ENTITY, IID_Timer, { - SetInterval: function() { }, - SetTimeout: function() { }, + "SetInterval": function() { }, + "SetTimeout": function() { }, }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { - CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags, accountForSize) { + "CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) { return 1; }, - EnableActiveQuery: function(id) { }, - ResetActiveQuery: function(id) { return [enemy]; }, - DisableActiveQuery: function(id) { }, - GetEntityFlagMask: function(identifier) { }, + "EnableActiveQuery": function(id) { }, + "ResetActiveQuery": function(id) { return [enemy]; }, + "DisableActiveQuery": function(id) { }, + "GetEntityFlagMask": function(identifier) { }, }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - GetCurrentTemplateName: function(ent) { return "special/formations/line_closed"; }, + "GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; }, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: function(id) { return playerEntity; }, - GetNumPlayers: function() { return 2; }, + "GetPlayerByID": function(id) { return playerEntity; }, + "GetNumPlayers": function() { return 2; }, }); AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { "IsInTargetRange": (ent, target, min, max) => true }); AddMock(playerEntity, IID_Player, { - IsAlly: function() { return false; }, - IsEnemy: function() { return true; }, - GetEnemies: function() { return [2]; }, + "IsAlly": function() { return false; }, + "IsEnemy": function() { return true; }, + "GetEnemies": function() { return [2]; }, }); // create units - for (var i = 0; i < unitCount; i++) { + for (var i = 0; i < unitCount; i++) + { units.push(unit + i); var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); AddMock(unit + i, IID_Identity, { - GetClassesList: function() { return []; }, + "GetClassesList": function() { return []; }, }); AddMock(unit + i, IID_Ownership, { - GetOwner: function() { return 1; }, + "GetOwner": function() { return 1; }, }); AddMock(unit + i, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(); }, - GetPosition2D: function() { return new Vector2D(); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, + "GetTurretParent": function() { return INVALID_ENTITY; }, + "GetPosition": function() { return new Vector3D(); }, + "GetPosition2D": function() { return new Vector2D(); }, + "GetRotation": function() { return { "y": 0 }; }, + "IsInWorld": function() { return true; }, }); AddMock(unit + i, IID_UnitMotion, { "GetWalkSpeed": () => 1, "MoveToFormationOffset": (target, x, z) => {}, "MoveToTargetRange": (target, min, max) => true, "StopMoving": () => {}, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); AddMock(unit + i, IID_Vision, { - GetRange: function() { return 10; }, + "GetRange": function() { return 10; }, }); AddMock(unit + i, IID_Attack, { - GetRange: function() { return {"max":10, "min": 0}; }, - GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, - GetBestAttackAgainst: function(t) { return "melee"; }, - GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, - CanAttack: function(v) { return true; }, - CompareEntitiesByPreference: function(a, b) { return 0; }, + "GetRange": function() { return { "max": 10, "min": 0 }; }, + "GetFullAttackRange": function() { return { "max": 40, "min": 0 }; }, + "GetBestAttackAgainst": function(t) { return "melee"; }, + "GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; }, + "CanAttack": function(v) { return true; }, + "CompareEntitiesByPreference": function(a, b) { return 0; }, }); unitAI.OnCreate(); unitAI.SetupAttackRangeQuery(1); unitAIs.push(unitAI); } // create enemy AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 40; }, + "GetHitpoints": function() { return 40; }, }); let controllerFormation = ConstructComponent(controller, "Formation", { "FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppiness": 0 }); let controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); AddMock(controller, IID_Position, { "GetTurretParent": () => INVALID_ENTITY, "JumpTo": function(x, z) { this.x = x; this.z = z; }, "GetPosition": function(){ return new Vector3D(this.x, 0, this.z); }, "GetPosition2D": function(){ return new Vector2D(this.x, this.z); }, "GetRotation": () => ({ "y": 0 }), "IsInWorld": () => true, "MoveOutOfWorld": () => {}, }); AddMock(controller, IID_UnitMotion, { "GetWalkSpeed": () => 1, "SetSpeedMultiplier": (speed) => {}, "MoveToPointRange": (x, z, minRange, maxRange) => {}, "StopMoving": () => {}, "SetFacePointAfterMove": () => {}, "GetFacePointAfterMove": () => true, "GetPassabilityClassName": () => "default" }); AddMock(controller, IID_Attack, { - GetRange: function() { return {"max":10, "min": 0}; }, - CanAttackAsFormation: function() { return false; }, + "GetRange": function() { return { "max": 10, "min": 0 }; }, + "CanAttackAsFormation": function() { return false; }, }); controllerAI.OnCreate(); controllerFormation.SetMembers(units); controllerAI.Attack(enemy, []); for (let ent of unitAIs) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); - controllerAI.MoveIntoFormation({"name": "Circle"}); + controllerAI.MoveIntoFormation({ "name": "Circle" }); // let all units be in position for (let ent of unitAIs) controllerFormation.SetWaitingOnController(ent); for (let ent of unitAIs) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); controllerFormation.Disband(); } TestFormationExiting(0); TestFormationExiting(1); TestFormationExiting(2); TestMoveIntoFormationWhileAttacking(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitMotionFlying.js (revision 25087) @@ -1,143 +1,143 @@ Engine.LoadComponentScript("UnitMotionFlying.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); let entity = 1; let target = 2; let height = 5; AddMock(SYSTEM_ENTITY, IID_Pathfinder, { - GetPassabilityClass: (name) => 1 << 8 + "GetPassabilityClass": (name) => 1 << 8 }); let cmpUnitMotionFlying = ConstructComponent(entity, "UnitMotionFlying", { "MaxSpeed": 1.0, "TakeoffSpeed": 0.5, "LandingSpeed": 0.5, "AccelRate": 0.0005, "SlowingRate": 0.001, "BrakingRate": 0.0005, "TurnRate": 0.1, "OvershootTime": 10, "FlyingHeight": 100, "ClimbRate": 0.1, "DiesInWater": false, "PassabilityClass": "unrestricted" }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 0); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetRunMultiplier(), 1); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); cmpUnitMotionFlying.SetSpeedMultiplier(2); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 0); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetRunMultiplier(), 1); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClassName(), "unrestricted"); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetPassabilityClass(), 1 << 8); AddMock(entity, IID_Position, { "IsInWorld": () => true, "GetPosition2D": () => { return { "x": 50, "y": 100 }; }, "GetPosition": () => { return { "x": 50, "y": height, "z": 100 }; }, "GetRotation": () => { return { "y": 3.14 }; }, "SetHeightFixed": (y) => height = y, "TurnTo": () => {}, "SetXZRotation": () => {}, "MoveTo": () => {} }); AddMock(target, IID_Position, { "IsInWorld": () => true, "GetPosition2D": () => { return { "x": 100, "y": 200 }; } }); AddMock(entity, IID_GarrisonHolder, { "AllowGarrisoning": () => {} }); AddMock(entity, IID_Health, { }); AddMock(entity, IID_RangeManager, { "GetLosCircular": () => true }); AddMock(entity, IID_Terrain, { "GetGroundLevel": () => 4, "GetMapSize": () => 20 }); AddMock(entity, IID_WaterManager, { "GetWaterLevel": () => 5 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 0); TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToTargetRange(target, 0, 10), true); TS_ASSERT_EQUALS(cmpUnitMotionFlying.MoveToPointRange(100, 200, 0, 20), true); // Take Off cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25); TS_ASSERT_EQUALS(height, 5); cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5); TS_ASSERT_EQUALS(height, 5); cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5); TS_ASSERT_EQUALS(height, 5); cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.75); TS_ASSERT_EQUALS(height, 55); cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetSpeedMultiplier(), 1); TS_ASSERT_EQUALS(height, 105); // Fly cmpUnitMotionFlying.OnUpdate({ "turnLength": 100 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); TS_ASSERT_EQUALS(height, 105); cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); TS_ASSERT_EQUALS(height, 105); cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); TS_ASSERT_EQUALS(height, 105); // Land cmpUnitMotionFlying.StopMoving(); cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 1); TS_ASSERT_EQUALS(height, 105); cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.5); TS_ASSERT_EQUALS(height, 5); // Slide cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25); TS_ASSERT_EQUALS(height, 5); cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0.25); TS_ASSERT_EQUALS(height, 5); cmpUnitMotionFlying.OnUpdate({ "turnLength": 500 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(height, 5); // Stay cmpUnitMotionFlying.OnUpdate({ "turnLength": 300 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(height, 5); cmpUnitMotionFlying.OnUpdate({ "turnLength": 0 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(height, 5); cmpUnitMotionFlying.OnUpdate({ "turnLength": 900 }); TS_ASSERT_EQUALS(cmpUnitMotionFlying.GetCurrentSpeed(), 0); TS_ASSERT_EQUALS(height, 5); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js (revision 25086) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js (revision 25087) @@ -1,189 +1,192 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Commands.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/VisionSharing.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("VisionSharing.js"); const ent = 170; let template = { "Bribable": "true" }; AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "GetTemplate": (name) => name == "special/spy" ? - ({ "Cost": { "Resources": { "wood": 1000 } }, - "VisionSharing": { "Duration": 15 } }) - : ({}) + "GetTemplate": (name) => { + return name == "special/spy" ? + { + "Cost": { "Resources": { "wood": 1000 } }, + "VisionSharing": { "Duration": 15 } } : + {}; + } }); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [] }); AddMock(ent, IID_Ownership, { "GetOwner": () => 1 }); let cmpVisionSharing = ConstructComponent(ent, "VisionSharing", template); // Add some entities AddMock(180, IID_Ownership, { "GetOwner": () => 2 }); AddMock(181, IID_Ownership, { "GetOwner": () => 1 }); AddMock(182, IID_Ownership, { "GetOwner": () => 8 }); AddMock(183, IID_Ownership, { "GetOwner": () => 2 }); TS_ASSERT_EQUALS(cmpVisionSharing.activated, false); // Test Activate cmpVisionSharing.activated = false; cmpVisionSharing.Activate(); TS_ASSERT_EQUALS(cmpVisionSharing.activated, true); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1])); // Test CheckVisionSharings cmpVisionSharing.activated = true; cmpVisionSharing.shared = new Set([1]); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [181] }); Engine.PostMessage = function(id, iid, message) { -TS_ASSERT(false); // One doesn't send message + TS_ASSERT(false); // One doesn't send message }; cmpVisionSharing.CheckVisionSharings(); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1])); cmpVisionSharing.shared = new Set([1, 2, 8]); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [180] }); Engine.PostMessage = function(id, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "entity": ent, "player": 8, "add": false }, message); }; cmpVisionSharing.CheckVisionSharings(); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2])); cmpVisionSharing.shared = new Set([1, 8]); AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [181, 182, 183] }); Engine.PostMessage = function(id, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "entity": ent, "player": 2, "add": true }, message); }; cmpVisionSharing.CheckVisionSharings(); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 8, 2])); // take care of order or sort them // Test IsBribable TS_ASSERT(cmpVisionSharing.IsBribable()); // Test RemoveSpy AddMock(ent, IID_GarrisonHolder, { "GetEntities": () => [] }); cmpVisionSharing.spies = new Map([[5, 2], [17, 5]]); cmpVisionSharing.shared = new Set([1, 2, 5]); Engine.PostMessage = function(id, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "entity": ent, "player": 2, "add": false }, message); }; cmpVisionSharing.RemoveSpy({ "id": 5 }); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 5])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[17, 5]])); Engine.PostMessage = function(id, iid, message) {}; // Test AddSpy cmpVisionSharing.spies = new Map([[5, 2], [17, 5]]); cmpVisionSharing.shared = new Set([1, 2, 5]); cmpVisionSharing.spyId = 20; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => 14 }); AddMock(14, IID_TechnologyManager, { "CanProduce": entity => false, }); AddMock(14, IID_ModifiersManager, { "ApplyTemplateModifiers": (valueName, curValue) => curValue }); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5]])); TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 20); AddMock(14, IID_TechnologyManager, { "CanProduce": entity => entity == "special/spy", }); AddMock(14, IID_ModifiersManager, { "ApplyTemplateModifiers": (valueName, curValue) => curValue }); AddMock(14, IID_Player, { "GetSpyCostMultiplier": () => 1, "TrySubtractResources": costs => false }); AddMock(4, IID_StatisticsTracker, { "IncreaseSuccessfulBribesCounter": () => {}, "IncreaseFailedBribesCounter": () => {} }); cmpVisionSharing.AddSpy(4, 25); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5]])); TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 20); AddMock(14, IID_Player, { "GetSpyCostMultiplier": () => 1, "TrySubtractResources": costs => true }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetTimeout": (ent, iid, funcname, time, data) => TS_ASSERT_EQUALS(time, 25 * 1000) }); cmpVisionSharing.AddSpy(4, 25); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5, 4])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5], [21, 4]])); TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 21); cmpVisionSharing.spies = new Map([[5, 2], [17, 5]]); cmpVisionSharing.shared = new Set([1, 2, 5]); cmpVisionSharing.spyId = 20; AddMock(ent, IID_Vision, { "GetRange": () => 48 }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetTimeout": (ent, iid, funcname, time, data) => TS_ASSERT_EQUALS(time, 15 * 1000 * 60 / 48) }); cmpVisionSharing.AddSpy(4); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.shared, new Set([1, 2, 5, 4])); TS_ASSERT_UNEVAL_EQUALS(cmpVisionSharing.spies, new Map([[5, 2], [17, 5], [21, 4]])); TS_ASSERT_EQUALS(cmpVisionSharing.spyId, 21); // Test ShareVisionWith cmpVisionSharing.activated = false; cmpVisionSharing.shared = undefined; TS_ASSERT(cmpVisionSharing.ShareVisionWith(1)); TS_ASSERT(!cmpVisionSharing.ShareVisionWith(2)); cmpVisionSharing.activated = true; cmpVisionSharing.shared = new Set([1, 2, 8]); TS_ASSERT(cmpVisionSharing.ShareVisionWith(1)); TS_ASSERT(cmpVisionSharing.ShareVisionWith(2)); TS_ASSERT(!cmpVisionSharing.ShareVisionWith(3)); TS_ASSERT(!cmpVisionSharing.ShareVisionWith(0));