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));