Index: ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js (nonexistent)
@@ -1,127 +0,0 @@
-function TreasureCollecter() {}
-
-TreasureCollecter.prototype.Schema =
- "Defines the treasure collecting abilities." +
- "" +
- "2.0" +
- "" +
- "" +
- "" +
- "";
-
-TreasureCollecter.prototype.Init = function()
-{
-};
-
-/**
- * @return {Object} - Min/Max range at which this entity can claim a treasure.
- */
-TreasureCollecter.prototype.GetRange = function()
-{
- return { "min": 0, "max": +this.template.MaxDistance };
-};
-
-/**
- * @param {number} target - Entity ID of the target.
- * @return {boolean} - Whether we can collect from the target.
- */
-TreasureCollecter.prototype.CanCollect = function(target)
-{
- let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
- return cmpTreasure && cmpTreasure.IsAvailable();
-};
-
-/**
- * @param {number} target - The target to collect.
- * @param {number} callerIID - The IID to notify on specific events.
- *
- * @return {boolean} - Whether we started collecting.
- */
-TreasureCollecter.prototype.StartCollecting = function(target, callerIID)
-{
- if (this.target)
- this.StopCollecting();
-
- let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
- if (!cmpTreasure || !cmpTreasure.IsAvailable())
- return false;
-
- let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
- if (cmpVisual)
- cmpVisual.SelectAnimation("collecting_treasure", false, 1.0);
-
- this.target = target;
- this.callerIID = callerIID;
-
- // ToDo: Implement rate modifiers.
- let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- this.timer = cmpTimer.SetTimeout(this.entity, IID_TreasureCollecter, "CollectTreasure", cmpTreasure.CollectionTime(), null);
-
- return true;
-};
-
-/**
- * @param {string} reason - The reason why we stopped collecting, used to notify the caller.
- */
-TreasureCollecter.prototype.StopCollecting = function(reason)
-{
- if (!this.target)
- return;
-
- let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
- cmpTimer.CancelTimer(this.timer);
- delete this.timer;
-
- delete this.target;
-
- let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
- if (cmpVisual)
- cmpVisual.SelectAnimation("idle", false, 1.0);
-
- // The callerIID component may start again,
- // replacing the callerIID, hence save that.
- let callerIID = this.callerIID;
- delete this.callerIID;
-
- if (reason && callerIID)
- {
- let component = Engine.QueryInterface(this.entity, callerIID);
- if (component)
- component.ProcessMessage(reason, null);
- }
-};
-
-/**
- * @params - Data and lateness are unused.
- */
-TreasureCollecter.prototype.CollectTreasure = function(data, lateness)
-{
- let cmpTreasure = Engine.QueryInterface(this.target, IID_Treasure);
- if (!cmpTreasure || !cmpTreasure.IsAvailable())
- {
- this.StopCollecting("TargetInvalidated");
- return;
- }
-
- if (!this.IsTargetInRange(this.target))
- {
- this.StopCollecting("OutOfRange");
- return;
- }
-
- cmpTreasure.Reward(this.entity);
- this.StopCollecting("TargetInvalidated");
-};
-
-/**
- * @param {number} - The entity ID of the target to check.
- * @return {boolean} - Whether this entity is in range of its target.
- */
-TreasureCollecter.prototype.IsTargetInRange = function(target)
-{
- let range = this.GetRange();
- let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
- return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
-};
-
-Engine.RegisterComponentType(IID_TreasureCollecter, "TreasureCollecter", TreasureCollecter);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollecter.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Deleted: svn:mime-type
## -1 +0,0 ##
-text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js (nonexistent)
@@ -1 +0,0 @@
-Engine.RegisterInterface("TreasureCollecter");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollecter.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Deleted: svn:mime-type
## -1 +0,0 ##
-text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js (nonexistent)
@@ -1,47 +0,0 @@
-Engine.LoadHelperScript("Player.js");
-Engine.LoadComponentScript("interfaces/Timer.js");
-Engine.LoadComponentScript("interfaces/Treasure.js");
-Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
-Engine.LoadComponentScript("interfaces/UnitAI.js");
-Engine.LoadComponentScript("Timer.js");
-Engine.LoadComponentScript("TreasureCollecter.js");
-
-AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
- "IsInTargetRange": () => true
-});
-
-const entity = 11;
-let treasure = 12;
-let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
-
-let cmpTreasurer = ConstructComponent(entity, "TreasureCollecter", {
- "MaxDistance": "2.0"
-});
-
-TS_ASSERT(!cmpTreasurer.StartCollecting(treasure));
-
-let cmpTreasure = AddMock(treasure, IID_Treasure, {
- "Reward": (ent) => true,
- "CollectionTime": () => 1000,
- "IsAvailable": () => true
-});
-let spyTreasure = new Spy(cmpTreasure, "Reward");
-TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
-cmpTimer.OnUpdate({ "turnLength": 1 });
-TS_ASSERT_EQUALS(spyTreasure._called, 1);
-
-// Test that starting to collect twice merely collects once.
-spyTreasure._called = 0;
-TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
-TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
-cmpTimer.OnUpdate({ "turnLength": 1 });
-TS_ASSERT_EQUALS(spyTreasure._called, 1);
-
-// Test callback is called.
-let cmpUnitAI = AddMock(entity, IID_UnitAI, {
- "ProcessMessage": (type, data) => TS_ASSERT_EQUALS(type, "TargetInvalidated")
-});
-let spyUnitAI = new Spy(cmpUnitAI, "ProcessMessage");
-TS_ASSERT(cmpTreasurer.StartCollecting(treasure, IID_UnitAI));
-cmpTimer.OnUpdate({ "turnLength": 1 });
-TS_ASSERT_EQUALS(spyUnitAI._called, 1);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollecter.js
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Deleted: svn:mime-type
## -1 +0,0 ##
-text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js (revision 25461)
@@ -1,1872 +1,1872 @@
/**
* Specifies which template should indicate the target location of a player command,
* given a command type.
*/
var g_TargetMarker = {
"move": "special/target_marker"
};
/**
* Which enemy entity types will be attacked on sight when patroling.
*/
var g_PatrolTargets = ["Unit"];
const g_DisabledTags = { "color": "255 140 0" };
/**
* List of different actions units can execute,
* this is mostly used to determine which actions can be executed
*
* "execute" is meant to send the command to the engine
*
* The next functions will always return false
* in case you have to continue to seek
* (i.e. look at the next entity for getActionInfo, the next
* possible action for the actionCheck ...)
* They will return an object when the searching is finished
*
* "getActionInfo" is used to determine if the action is possible,
* and also give visual feedback to the user (tooltips, cursors, ...)
*
* "preSelectedActionCheck" is used to select actions when the gui buttons
* were used to set them, but still require a target (like the guard button)
*
* "hotkeyActionCheck" is used to check the possibility of actions when
* a hotkey is pressed
*
* "actionCheck" is used to check the possibilty of actions without specific
* command. For that, the specificness variable is used
*
* "specificness" is used to determine how specific an action is,
* The lower the number, the more specific an action is, and the bigger
* the chance of selecting that action when multiple actions are possible
*/
var g_UnitActions =
{
"move":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "walk",
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.move") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("move", target, selection);
return actionInfo.possible && {
"type": "move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 12,
},
"attack-move":
{
"execute": function(target, action, selection, queued, pushFront)
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
Engine.PostNetworkCommand({
"type": "attack-walk",
"entities": selection,
"x": target.x,
"z": target.z,
"targetClasses": targetClasses,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return isAttackMovePressed() &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack-move", target, selection);
return actionInfo.possible && {
"type": "attack-move",
"cursor": "action-attack-move",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 30,
},
"capture":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"allowCapture": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.capturePoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["Capture"]
})
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("capture", target, selection);
return actionInfo.possible && {
"type": "capture",
"cursor": "action-capture",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 9,
},
"attack":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "attack",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"allowCapture": false,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_attack",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.attack || !targetState || !targetState.hitpoints)
return false;
return {
"possible": Engine.GuiInterfaceCall("CanAttack", {
"entity": entState.id,
"target": targetState.id,
"types": ["!Capture"]
})
};
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.attack") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("attack", target, selection);
return actionInfo.possible && {
"type": "attack",
"cursor": "action-attack",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 10,
},
"patrol":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "patrol",
"entities": selection,
"x": target.x,
"z": target.z,
"target": action.target,
"targetClasses": { "attack": g_PatrolTargets },
"queued": queued,
"allowCapture": false,
"formation": g_AutoFormation.getDefault()
});
DrawTargetMarker(target);
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_patrol",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.canPatrol)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.patrol") &&
this.actionCheck(target, selection);
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_PATROL &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("patrol", target, selection);
return actionInfo.possible && {
"type": "patrol",
"cursor": "action-patrol",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 37,
},
"heal":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "heal",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_heal",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.heal || !targetState ||
!hasClass(targetState, "Unit") || !targetState.needsHeal ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
entState.id == targetState.id) // Healers can't heal themselves.
return false;
let unhealableClasses = entState.heal.unhealableClasses;
if (MatchesClassList(targetState.identity.classes, unhealableClasses))
return false;
let healableClasses = entState.heal.healableClasses;
if (!MatchesClassList(targetState.identity.classes, healableClasses))
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("heal", target, selection);
return actionInfo.possible && {
"type": "heal",
"cursor": "action-heal",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 7,
},
// "Fake" action to check if an entity can be ordered to "construct"
// which is handled differently from repair as the target does not exist.
"construct":
{
"preSelectedActionCheck": function(target, selection)
{
let state = GetEntityState(selection[0]);
if (state && state.builder &&
target && target.constructor && target.constructor.name == "PlacementSupport")
return { "type": "construct" };
return false;
},
"specificness": 0,
},
"repair":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "repair",
"entities": selection,
"target": action.target,
"autocontinue": true,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": action.foundation ? "order_build" : "order_repair",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.builder || !targetState ||
!targetState.needsRepair && !targetState.foundation ||
!playerCheck(entState, targetState, ["Player", "Ally"]))
return false;
return {
"possible": true,
"foundation": targetState.foundation
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_REPAIR && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-repair-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.repair") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("repair", target, selection);
return actionInfo.possible && {
"type": "repair",
"cursor": "action-repair",
"target": target,
"foundation": actionInfo.foundation,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
"gather":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "gather",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.resourceGatherRates ||
!targetState || !targetState.resourceSupply)
return false;
let resource;
if (entState.resourceGatherRates[targetState.resourceSupply.type.generic + "." + targetState.resourceSupply.type.specific])
resource = targetState.resourceSupply.type.specific;
else if (entState.resourceGatherRates[targetState.resourceSupply.type.generic])
resource = targetState.resourceSupply.type.generic;
if (!resource)
return false;
return {
"possible": true,
"cursor": "action-gather-" + resource
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("gather", target, selection);
return actionInfo.possible && {
"type": "gather",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"returnresource":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "returnresource",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_gather",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.resourceDropsite)
return false;
let playerState = GetSimState().players[entState.player];
if (playerState.hasSharedDropsites && targetState.resourceDropsite.shared)
{
if (!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
}
else if (!playerCheck(entState, targetState, ["Player"]))
return false;
if (!entState.resourceCarrying || !entState.resourceCarrying.length)
return false;
let carriedType = entState.resourceCarrying[0].type;
if (targetState.resourceDropsite.types.indexOf(carriedType) == -1)
return false;
return {
"possible": true,
"cursor": "action-return-" + carriedType
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("returnresource", target, selection);
return actionInfo.possible && {
"type": "returnresource",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"cancel-setup-trade-route":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "cancel-setup-trade-route",
"entities": selection,
"target": action.target,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails || !tradingDetails.type)
return false;
if (tradingDetails.type == "is first" && !tradingDetails.hasBothMarkets)
return {
"possible": true,
"tooltip": translate("This is the origin trade market.\nRight-click to cancel trade route.")
};
return false;
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("cancel-setup-trade-route", target, selection);
return actionInfo.possible && {
"type": "cancel-setup-trade-route",
"cursor": "action-cancel-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 2,
},
"setup-trade-route":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "setup-trade-route",
"entities": selection,
"target": action.target,
"source": null,
"route": null,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_trade",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || targetState.foundation || !entState.trader || !targetState.market ||
playerCheck(entState, targetState, ["Enemy"]) ||
!(targetState.market.land && hasClass(entState, "Organic") ||
targetState.market.naval && hasClass(entState, "Ship")))
return false;
let tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", {
"trader": entState.id,
"target": targetState.id
});
if (!tradingDetails)
return false;
let tooltip;
switch (tradingDetails.type)
{
case "is first":
tooltip = translate("Origin trade market.") + "\n";
if (tradingDetails.hasBothMarkets)
tooltip += sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
else
return false;
break;
case "is second":
tooltip = translate("Destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
case "set first":
tooltip = translate("Right-click to set as origin trade market");
break;
case "set second":
if (tradingDetails.gain.traderGain == 0)
return {
"possible": true,
"tooltip": setStringTags(translate("This market is too close to the origin market."), g_DisabledTags),
"disabled": true
};
tooltip = translate("Right-click to set as destination trade market.") + "\n" +
sprintf(translate("Gain: %(gain)s"), {
"gain": getTradingTooltip(tradingDetails.gain)
});
break;
}
return {
"possible": true,
"tooltip": tooltip
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("setup-trade-route", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": "action-setup-trade-route-disabled",
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "setup-trade-route",
"cursor": "action-setup-trade-route",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 0,
},
"occupy-turret":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "occupy-turret",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.turretable || !targetState || !targetState.turretHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
if (!targetState.turretHolder.turretPoints.find(point =>
!point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
return false;
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_OCCUPY_TURRET && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-occupy-turret-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.occupyturret") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("occupy-turret", target, selection);
return actionInfo.possible && {
"type": "occupy-turret",
"cursor": "action-occupy-turret",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 21,
},
"garrison":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "garrison",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.garrisonable || !targetState || !targetState.garrisonHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
let extraCount = entState.garrisonable.size;
if (entState.garrisonHolder)
extraCount += entState.garrisonHolder.occupiedSlots;
if (targetState.garrisonHolder.occupiedSlots + extraCount > targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses))
return false;
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GARRISON && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-garrison-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.garrison") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("garrison", target, selection);
return actionInfo.possible && {
"type": "garrison",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 20,
},
"guard":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "guard",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState || !targetState.guard || entState.id == targetState.id ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
!entState.unitAI || !entState.unitAI.canGuard)
return false;
return { "possible": true };
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GUARD && (this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-guard-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("guard", target, selection);
return actionInfo.possible && {
"type": "guard",
"cursor": "action-guard",
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 40,
},
"collect-treasure":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "collect-treasure",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_collect_treasure",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
- if (!entState.treasureCollecter ||
+ if (!entState.treasureCollector ||
!targetState || !targetState.treasure)
return false;
return {
"possible": true,
"cursor": "action-collect-treasure"
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("collect-treasure", target, selection);
return actionInfo.possible && {
"type": "collect-treasure",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"remove-guard":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "remove-guard",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_guard",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.unitAI || !entState.unitAI.isGuarding)
return false;
return { "possible": true };
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.guard") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("remove-guard", target, selection);
return actionInfo.possible && {
"type": "remove-guard",
"cursor": "action-remove-guard",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 41,
},
"set-rallypoint":
{
"execute": function(target, action, selection, queued, pushFront)
{
// if there is a position set in the action then use this so that when setting a
// rally point on an entity it is centered on that entity
if (action.position)
target = action.position;
Engine.PostNetworkCommand({
"type": "set-rallypoint",
"entities": selection,
"x": target.x,
"z": target.z,
"data": action.data,
"queued": queued
});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z,
"queued": queued
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.rallyPoint)
return false;
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities.
if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") ||
!targetState.production ||
!targetState.production.entities.length))
for (const ent of g_Selection.toList())
if (targetState.id == ent)
return false;
let tooltip;
let disabled = false;
// default to walking there (or attack-walking if hotkey pressed)
let data = { "command": "walk" };
let cursor = "";
if (isAttackMovePressed())
{
let targetClasses;
if (Engine.HotkeyIsPressed("session.attackmoveUnit"))
targetClasses = { "attack": ["Unit"] };
else
targetClasses = { "attack": ["Unit", "Structure"] };
data.command = "attack-walk";
data.targetClasses = targetClasses;
cursor = "action-attack-move";
}
if (Engine.HotkeyIsPressed("session.repair") && targetState &&
(targetState.needsRepair || targetState.foundation) &&
playerCheck(entState, targetState, ["Player", "Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && targetState.garrisonHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "garrison";
data.target = targetState.id;
cursor = "action-garrison";
tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), {
"garrisoned": targetState.garrisonHolder.occupiedSlots,
"capacity": targetState.garrisonHolder.capacity
});
if (targetState.garrisonHolder.occupiedSlots >=
targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.turretHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "occupy-turret";
data.target = targetState.id;
cursor = "action-garrison";
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
cursor = "action-gather-" + resourceType.specific;
data.command = "gather-near-position";
data.resourceType = resourceType;
data.resourceTemplate = targetState.template;
if (!targetState.speed)
{
data.command = "gather";
data.target = targetState.id;
}
}
else if (targetState && targetState.treasure)
{
cursor = "action-collect-treasure";
data.command = "collect-treasure-near-position";
if (!targetState.speed)
{
data.command = "collect-treasure";
data.target = targetState.id;
}
}
else if (entState.market && targetState && targetState.market &&
entState.id != targetState.id &&
(!entState.market.naval || targetState.market.naval) &&
!playerCheck(entState, targetState, ["Enemy"]))
{
// Find a trader (if any) that this structure can train.
let trader;
if (entState.production && entState.production.entities.length)
for (let i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
break;
let traderData = {
"firstMarket": entState.id,
"secondMarket": targetState.id,
"template": trader
};
let gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData);
if (gain)
{
data.command = "trade";
data.target = traderData.secondMarket;
data.source = traderData.firstMarket;
cursor = "action-setup-trade-route";
if (gain.traderGain)
tooltip = translate("Right-click to establish a default route for new traders.") + "\n" +
sprintf(
trader ?
translate("Gain: %(gain)s") :
translate("Expected gain: %(gain)s"),
{ "gain": getTradingTooltip(gain) });
else
{
disabled = true;
tooltip = setStringTags(translate("This market is too close to the origin market."), g_DisabledTags);
cursor = "action-setup-trade-route-disabled";
}
}
}
else if (targetState && (targetState.needsRepair || targetState.foundation) && playerCheck(entState, targetState, ["Ally"]))
{
data.command = "repair";
data.target = targetState.id;
cursor = "action-repair";
}
else if (targetState && playerCheck(entState, targetState, ["Enemy"]))
{
data.target = targetState.id;
data.command = "attack";
cursor = "action-attack";
}
return {
"possible": true,
"data": data,
"position": targetState && targetState.position,
"cursor": cursor,
"disabled": disabled,
"tooltip": tooltip
};
},
"hotkeyActionCheck": function(target, selection)
{
// Hotkeys are checked in the actionInfo.
return this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
// We want commands to units take precedence.
if (selection.some(ent => {
let entState = GetEntityState(ent);
return entState && !!entState.unitAI;
}))
return false;
let actionInfo = getActionInfo("set-rallypoint", target, selection);
if (actionInfo.disabled)
return {
"type": "none",
"cursor": actionInfo.cursor,
"target": null,
"tooltip": actionInfo.tooltip
};
return actionInfo.possible && {
"type": "set-rallypoint",
"cursor": actionInfo.cursor,
"data": actionInfo.data,
"tooltip": actionInfo.tooltip,
"position": actionInfo.position,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 6,
},
"unset-rallypoint":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "unset-rallypoint",
"entities": selection
});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!targetState ||
entState.id != targetState.id || entState.unitAI ||
!entState.rallyPoint || !entState.rallyPoint.position)
return false;
return { "possible": true };
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("unset-rallypoint", target, selection);
return actionInfo.possible && {
"type": "unset-rallypoint",
"cursor": "action-unset-rally",
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 11,
},
// This is a "fake" action to show a failure cursor
// when only uncontrollable entities are selected.
"uncontrollable":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"actionCheck": function(target, selection)
{
// Only show this action if all entities are marked uncontrollable.
let playerState = g_SimState.players[g_ViewedPlayer];
if (playerState && playerState.controlsAll || selection.some(ent => {
let entState = GetEntityState(ent);
return entState && entState.identity && entState.identity.controllable;
}))
return false;
return {
"type": "none",
"cursor": "cursor-no",
"tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length)
};
},
"specificness": 100,
},
"none":
{
"execute": function(target, action, selection, queued)
{
return true;
},
"specificness": 100,
},
};
var g_UnitActionsSortedKeys = Object.keys(g_UnitActions).sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness);
/**
* Info and actions for the entity commands
* Currently displayed in the bottom of the central panel
*/
var g_EntityCommands =
{
"unload-all": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.garrisonHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.garrisonHolder.entities.length;
else
for (let entity of entState.garrisonHolder.entities)
if (allowedPlayersCheck([GetEntityState(entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unload") +
translate("Unload All."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAll();
},
"allowedPlayers": ["Player", "Ally"]
},
"unload-all-turrets": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.turretHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity && turretPoint.ejectable).length;
else
for (let turretPoint of entState.turretHolder.turretPoints)
if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") +
translate("Unload Turrets."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAllTurrets();
},
"allowedPlayers": ["Player", "Ally"]
},
"delete": {
"getInfo": function(entStates)
{
return entStates.some(entState => !isUndeletable(entState)) ?
{
"tooltip":
colorizeHotkey("%(hotkey)s" + " ", "session.kill") +
translate("Destroy the selected units or structures.") + "\n" +
colorizeHotkey(
translate("Use %(hotkey)s to avoid the confirmation dialog."),
"session.noconfirmation"
),
"icon": "kill_small.png",
"enabled": true
} :
{
// Get all delete reasons and remove duplications
"tooltip": entStates.map(entState => isUndeletable(entState))
.filter((reason, pos, self) =>
self.indexOf(reason) == pos && reason
).join("\n"),
"icon": "kill_small_disabled.png",
"enabled": false
};
},
"execute": function(entStates)
{
let entityIDs = entStates.reduce(
(ids, entState) => {
if (!isUndeletable(entState))
ids.push(entState.id);
return ids;
},
[]);
if (!entityIDs.length)
return;
let deleteSelection = () => Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": entityIDs
});
if (Engine.HotkeyIsPressed("session.noconfirmation"))
deleteSelection();
else
(new DeleteSelectionConfirmation(deleteSelection)).display();
},
"allowedPlayers": ["Player"]
},
"stop": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.stop") +
translate("Abort the current order."),
"icon": "stop.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
stopUnits(entStates.map(entState => entState.id));
},
"allowedPlayers": ["Player"]
},
"garrison": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.garrisonable ||
entState.garrisonable.holder != INVALID_ENTITY))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.garrison") +
translate("Order the selected units to garrison in a structure or unit."),
"icon": "garrison.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
},
"allowedPlayers": ["Player"]
},
"occupy-turret": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.turretable ||
entState.turretable.holder != INVALID_ENTITY))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.occupyturret") +
translate("Order the selected units to occupy a turret point."),
"icon": "occupy-turret.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_OCCUPY_TURRET;
},
"allowedPlayers": ["Player"]
},
"leave-turret": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.turretable ||
entState.turretable.holder == INVALID_ENTITY ||
!entState.turretable.ejectable))
return false;
return {
"tooltip": translate("Unload"),
"icon": "leave-turret.png",
"enabled": true
};
},
"execute": function(entStates)
{
leaveTurretPoints();
},
"allowedPlayers": ["Player"]
},
"repair": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.builder))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.repair") +
translate("Order the selected units to repair a structure, ship, or siege engine."),
"icon": "repair.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
},
"allowedPlayers": ["Player"]
},
"focus-rally": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.rallyPoint))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "camera.rallypointfocus") +
translate("Focus on Rally Point."),
"icon": "focus-rally.png",
"enabled": true
};
},
"execute": function(entStates)
{
// TODO: Would be nicer to cycle between the rallypoints of multiple entities instead of just using the first
let focusTarget;
for (let entState of entStates)
if (entState.rallyPoint && entState.rallyPoint.position)
{
focusTarget = entState.rallyPoint.position;
break;
}
if (!focusTarget)
for (let entState of entStates)
if (entState.position)
{
focusTarget = entState.position;
break;
}
if (focusTarget)
Engine.CameraMoveTo(focusTarget.x, focusTarget.z);
},
"allowedPlayers": ["Player", "Observer"]
},
"back-to-work": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.hasWorkOrders))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.backtowork") +
translate("Back to Work"),
"icon": "back-to-work.png",
"enabled": true
};
},
"execute": function()
{
backToWork();
},
"allowedPlayers": ["Player"]
},
"add-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState =>
!entState.unitAI || !entState.unitAI.canGuard || entState.unitAI.isGuarding))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.guard") +
translate("Order the selected units to guard a structure or unit."),
"icon": "add-guard.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GUARD;
},
"allowedPlayers": ["Player"]
},
"remove-guard": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.unitAI || !entState.unitAI.isGuarding))
return false;
return {
"tooltip": translate("Remove guard"),
"icon": "remove-guard.png",
"enabled": true
};
},
"execute": function()
{
removeGuard();
},
"allowedPlayers": ["Player"]
},
"select-trading-goods": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.market))
return false;
return {
"tooltip": translate("Barter & Trade"),
"icon": "economics.png",
"enabled": true
};
},
"execute": function()
{
g_TradeDialog.toggle();
},
"allowedPlayers": ["Player"]
},
"patrol": {
"getInfo": function(entStates)
{
if (!entStates.some(entState => entState.unitAI && entState.unitAI.canPatrol))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.patrol") +
translate("Patrol") + "\n" +
translate("Attack all encountered enemy units while avoiding structures."),
"icon": "patrol.png",
"enabled": true
};
},
"execute": function()
{
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_PATROL;
},
"allowedPlayers": ["Player"]
},
"share-dropsite": {
"getInfo": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!sharableEntities.length)
return false;
// Returns if none of the entities belong to a player with a mutual ally.
if (entStates.every(entState => !GetSimState().players[entState.player].isMutualAlly.some(
(isAlly, playerId) => isAlly && playerId != entState.player)))
return false;
return sharableEntities.some(entState => !entState.resourceDropsite.shared) ?
{
"tooltip": translate("Press to allow allies to use this dropsite"),
"icon": "locked_small.png",
"enabled": true
} :
{
"tooltip": translate("Press to prevent allies from using this dropsite"),
"icon": "unlocked_small.png",
"enabled": true
};
},
"execute": function(entStates)
{
let sharableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (sharableEntities)
Engine.PostNetworkCommand({
"type": "set-dropsite-sharing",
"entities": sharableEntities.map(entState => entState.id),
"shared": sharableEntities.some(entState => !entState.resourceDropsite.shared)
});
},
"allowedPlayers": ["Player"]
},
"is-dropsite-shared": {
"getInfo": function(entStates)
{
let shareableEntities = entStates.filter(
entState => entState.resourceDropsite && entState.resourceDropsite.sharable);
if (!shareableEntities.length)
return false;
let player = Engine.GetPlayerID();
let simState = GetSimState();
if (!g_IsObserver && !simState.players[player].hasSharedDropsites ||
shareableEntities.every(entState => controlsPlayer(entState.player)))
return false;
if (!shareableEntities.every(entState => entState.resourceDropsite.shared))
return {
"tooltip": translate("The use of this dropsite is prohibited"),
"icon": "locked_small.png",
"enabled": false
};
return {
"tooltip": g_IsObserver ? translate("Allies are allowed to use this dropsite.") :
translate("You are allowed to use this dropsite"),
"icon": "unlocked_small.png",
"enabled": false
};
},
"execute": function(entState)
{
// This command button is always disabled.
},
"allowedPlayers": ["Ally", "Observer"]
},
"autoqueue-on": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.production || !entState.production.entities.length || entState.production.autoqueue))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") +
translate("Activate auto-queue for selected structures."),
"icon": "autoqueue-on.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
turnAutoQueueOn();
},
"allowedPlayers": ["Player"]
},
"autoqueue-off": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.production || !entState.production.entities.length || !entState.production.autoqueue))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") +
translate("Deactivate auto-queue for selected structures."),
"icon": "autoqueue-off.png",
"enabled": true
};
},
"execute": function(entStates)
{
if (entStates.length)
turnAutoQueueOff();
},
"allowedPlayers": ["Player"]
},
};
function playerCheck(entState, targetState, validPlayers)
{
let playerState = GetSimState().players[entState.player];
for (let player of validPlayers)
if (player == "Gaia" && targetState.player == 0 ||
player == "Player" && targetState.player == entState.player ||
playerState["is" + player] && playerState["is" + player][targetState.player])
return true;
return false;
}
/**
* Checks whether the entities have the right diplomatic status
* with respect to the currently active player.
* Also "Observer" can be used.
*
* @param {Object[]} entStates - An array containing the entity states to check.
* @param {string[]} validPlayers - An array containing the diplomatic statuses.
*
* @return {boolean} - Whether the currently active player is allowed.
*/
function allowedPlayersCheck(entStates, validPlayers)
{
// Assume we can only select entities from one player,
// or it does not matter (e.g. observer).
let targetState = entStates[0];
let playerState = GetSimState().players[Engine.GetPlayerID()];
return validPlayers.some(player =>
player == "Observer" && g_IsObserver ||
player == "Player" && controlsPlayer(targetState.player) ||
playerState && playerState["is" + player] && playerState["is" + player][targetState.player]);
}
function hasClass(entState, className)
{
// note: use the functions in globalscripts/Templates.js for more versatile matching
return entState.identity && entState.identity.classes.indexOf(className) != -1;
}
/**
* Keep in sync with Commands.js.
*/
function isUndeletable(entState)
{
let playerState = g_SimState.players[entState.player];
if (playerState && playerState.controlsAll)
return false;
if (entState.resourceSupply && entState.resourceSupply.killBeforeGather)
return translate("The entity has to be killed before it can be gathered from");
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
return translate("You cannot destroy this entity as you own less than half the capture points");
if (!entState.identity.canDelete)
return translate("This entity is undeletable");
return false;
}
function DrawTargetMarker(target)
{
Engine.GuiInterfaceCall("AddTargetMarker", {
"template": g_TargetMarker.move,
"x": target.x,
"z": target.z
});
}
function getCommandInfo(command, entStates)
{
return entStates && g_EntityCommands[command] &&
allowedPlayersCheck(entStates, g_EntityCommands[command].allowedPlayers) &&
g_EntityCommands[command].getInfo(entStates);
}
function getActionInfo(action, target, selection)
{
if (!selection || !selection.length || !GetEntityState(selection[0]))
return { "possible": false };
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
let targetState = GetEntityState(target);
let simState = GetSimState();
let playerState = g_SimState.players[g_ViewedPlayer];
// Check if any entities in the selection can do some of the available actions.
for (let entityID of selection)
{
let entState = GetEntityState(entityID);
if (!entState)
continue;
if (playerState && !playerState.controlsAll && !entState.identity.controllable)
continue;
if (g_UnitActions[action] && g_UnitActions[action].getActionInfo)
{
let r = g_UnitActions[action].getActionInfo(entState, targetState, simState);
if (r && r.possible)
{
r.entity = entityID;
return r;
}
}
}
return { "possible": false };
}
Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 25461)
@@ -1,1025 +1,1025 @@
var API3 = function(m)
{
// defines a template.
m.Template = m.Class({
"_init": function(sharedAI, templateName, template)
{
this._templateName = templateName;
this._template = template;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
this._tpCache = new Map();
},
// Helper function to return a template value, adjusting for tech.
"get": function(string)
{
if (this._entityModif && this._entityModif.has(string))
return this._entityModif.get(string);
else if (this._templateModif)
{
let owner = this._entity ? this._entity.owner : PlayerID;
if (this._templateModif[owner] && this._templateModif[owner].has(string))
return this._templateModif[owner].get(string);
}
if (!this._tpCache.has(string))
{
let value = this._template;
let args = string.split("/");
for (let arg of args)
{
value = value[arg];
if (value == undefined)
break;
}
this._tpCache.set(string, value);
}
return this._tpCache.get(string);
},
"templateName": function() { return this._templateName; },
"genericName": function() { return this.get("Identity/GenericName"); },
"civ": function() { return this.get("Identity/Civ"); },
"matchLimit": function() {
if (!this.get("TrainingRestrictions"))
return undefined;
return this.get("TrainingRestrictions/MatchLimit");
},
"classes": function() {
let template = this.get("Identity");
if (!template)
return undefined;
return GetIdentityClasses(template);
},
"hasClass": function(name) {
if (!this._classes)
this._classes = this.classes();
return this._classes && this._classes.indexOf(name) != -1;
},
"hasClasses": function(array) {
if (!this._classes)
this._classes = this.classes();
if (!this._classes)
return false;
for (let cls of array)
if (this._classes.indexOf(cls) == -1)
return false;
return true;
},
"requiredTech": function() { return this.get("Identity/RequiredTechnology"); },
"available": function(gameState) {
let techRequired = this.requiredTech();
if (!techRequired)
return true;
return gameState.isResearched(techRequired);
},
// specifically
"phase": function() {
let techRequired = this.requiredTech();
if (!techRequired)
return 0;
if (techRequired == "phase_village")
return 1;
if (techRequired == "phase_town")
return 2;
if (techRequired == "phase_city")
return 3;
if (techRequired.startsWith("phase_"))
return 4;
return 0;
},
"cost": function(productionQueue) {
if (!this.get("Cost"))
return {};
let ret = {};
for (let type in this.get("Cost/Resources"))
ret[type] = +this.get("Cost/Resources/" + type);
return ret;
},
"costSum": function(productionQueue) {
let cost = this.cost(productionQueue);
if (!cost)
return 0;
let ret = 0;
for (let type in cost)
ret += cost[type];
return ret;
},
"techCostMultiplier": function(type) {
return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1);
},
/**
* Returns { "max": max, "min": min } or undefined if no obstruction.
* max: radius of the outer circle surrounding this entity's obstruction shape
* min: radius of the inner circle
*/
"obstructionRadius": function() {
if (!this.get("Obstruction"))
return undefined;
if (this.get("Obstruction/Static"))
{
let w = +this.get("Obstruction/Static/@width");
let h = +this.get("Obstruction/Static/@depth");
return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
}
if (this.get("Obstruction/Unit"))
{
let r = +this.get("Obstruction/Unit/@radius");
return { "max": r, "min": r };
}
let right = this.get("Obstruction/Obstructions/Right");
let left = this.get("Obstruction/Obstructions/Left");
if (left && right)
{
let w = +right["@x"] + right["@width"] / 2 - left["@x"] + left["@width"] / 2;
let h = Math.max(+right["@z"] + right["@depth"] / 2, +left["@z"] + left["@depth"] / 2) -
Math.min(+right["@z"] - right["@depth"] / 2, +left["@z"] - left["@depth"] / 2);
return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
}
return { "max": 0, "min": 0 }; // Units have currently no obstructions
},
/**
* Returns the radius of a circle surrounding this entity's footprint.
*/
"footprintRadius": function() {
if (!this.get("Footprint"))
return undefined;
if (this.get("Footprint/Square"))
{
let w = +this.get("Footprint/Square/@width");
let h = +this.get("Footprint/Square/@depth");
return Math.sqrt(w * w + h * h) / 2;
}
if (this.get("Footprint/Circle"))
return +this.get("Footprint/Circle/@radius");
return 0; // this should never happen
},
"maxHitpoints": function() { return +(this.get("Health/Max") || 0); },
"isHealable": function()
{
if (this.get("Health") !== undefined)
return this.get("Health/Unhealable") !== "true";
return false;
},
"isRepairable": function() { return this.get("Repairable") !== undefined; },
"getPopulationBonus": function() {
if (!this.get("Population"))
return 0;
return +this.get("Population/Bonus");
},
"resistanceStrengths": function() {
let resistanceTypes = this.get("Resistance");
if (!resistanceTypes || !resistanceTypes.Entity)
return undefined;
let resistance = {};
if (resistanceTypes.Entity.Capture)
resistance.Capture = +this.get("Resistance/Entity/Capture");
if (resistanceTypes.Entity.Damage)
{
resistance.Damage = {};
for (let damageType in resistanceTypes.Entity.Damage)
resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType);
}
// ToDo: Resistance to StatusEffects.
return resistance;
},
"attackTypes": function() {
let attack = this.get("Attack");
if (!attack)
return undefined;
let ret = [];
for (let type in attack)
ret.push(type);
return ret;
},
"attackRange": function(type) {
if (!this.get("Attack/" + type))
return undefined;
return {
"max": +this.get("Attack/" + type +"/MaxRange"),
"min": +(this.get("Attack/" + type +"/MinRange") || 0)
};
},
"attackStrengths": function(type) {
let attackDamageTypes = this.get("Attack/" + type + "/Damage");
if (!attackDamageTypes)
return undefined;
let damage = {};
for (let damageType in attackDamageTypes)
damage[damageType] = +attackDamageTypes[damageType];
return damage;
},
"captureStrength": function() {
if (!this.get("Attack/Capture"))
return undefined;
return +this.get("Attack/Capture/Capture") || 0;
},
"attackTimes": function(type) {
if (!this.get("Attack/" + type))
return undefined;
return {
"prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0),
"repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000)
};
},
// returns the classes this templates counters:
// Return type is [ [-neededClasses- , multiplier], … ].
"getCounteredClasses": function() {
let attack = this.get("Attack");
if (!attack)
return undefined;
let Classes = [];
for (let type in attack)
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]);
}
}
return Classes;
},
// returns true if the entity counters those classes.
// TODO: refine using the multiplier
"countersClasses": function(classes) {
let attack = this.get("Attack");
if (!attack)
return false;
let mcounter = [];
for (let type in attack)
{
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (!bonuses)
continue;
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (bonusClasses)
mcounter.concat(bonusClasses.split(" "));
}
}
for (let i in classes)
if (mcounter.indexOf(classes[i]) != -1)
return true;
return false;
},
// returns, if it exists, the multiplier from each attack against a given class
"getMultiplierAgainst": function(type, againstClass) {
if (!this.get("Attack/" + type +""))
return undefined;
let bonuses = this.get("Attack/" + type + "/Bonuses");
if (bonuses)
{
for (let b in bonuses)
{
let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
if (!bonusClasses)
continue;
for (let bcl of bonusClasses.split(" "))
if (bcl == againstClass)
return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
}
}
return 1;
},
"buildableEntities": function(civ) {
let templates = this.get("Builder/Entities/_string");
if (!templates)
return [];
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"trainableEntities": function(civ) {
let templates = this.get("ProductionQueue/Entities/_string");
if (!templates)
return undefined;
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"researchableTechs": function(gameState, civ) {
let templates = this.get("ProductionQueue/Technologies/_string");
if (!templates)
return undefined;
let techs = templates.split(/\s+/);
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let civTech = tech.replace("{civ}", civ);
techs[i] = TechnologyTemplates.Has(civTech) ?
civTech : tech.replace("{civ}", "generic");
}
return techs;
},
"resourceSupplyType": function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
},
"getResourceType": function() {
if (!this.get("ResourceSupply"))
return undefined;
return this.get("ResourceSupply/Type").split('.')[0];
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
"resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },
"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
"resourceGatherRates": function() {
if (!this.get("ResourceGatherer"))
return undefined;
let ret = {};
let baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
for (let r in this.get("ResourceGatherer/Rates"))
ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
return ret;
},
"resourceDropsiteTypes": function() {
if (!this.get("ResourceDropsite"))
return undefined;
let types = this.get("ResourceDropsite/Types");
return types ? types.split(/\s+/) : [];
},
"isTreasure": function() { return this.get("Treasure") !== undefined; },
"treasureResources": function() {
if (!this.get("Treasure"))
return undefined;
let ret = {};
for (let r in this.get("Treasure/Resources"))
ret[r] = +this.get("Treasure/Resources/" + r);
return ret;
},
"garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },
"garrisonMax": function() { return this.get("GarrisonHolder/Max"); },
"garrisonSize": function() { return this.get("Garrisonable/Size"); },
"garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); },
"getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); },
"getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); },
"getGarrisonArrowClasses": function() {
if (!this.get("BuildingAI"))
return undefined;
return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
},
"buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); },
"promotion": function() { return this.get("Promotion/Entity"); },
"isPackable": function() { return this.get("Pack") != undefined; },
"isHuntable": function() {
// Do not hunt retaliating animals (dead animals can be used).
// Assume entities which can attack, will attack.
return this.get("ResourceSupply/KillBeforeGather") &&
(!this.get("Health") || !this.get("Attack"));
},
"walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); },
"trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },
"buildTime": function(productionQueue) {
let time = +this.get("Cost/BuildTime");
if (productionQueue)
time *= productionQueue.techCostMultiplier("time");
return time;
},
"buildCategory": function() { return this.get("BuildRestrictions/Category"); },
"buildDistance": function() {
let distance = this.get("BuildRestrictions/Distance");
if (!distance)
return undefined;
let ret = {};
for (let key in distance)
ret[key] = this.get("BuildRestrictions/Distance/" + key);
return ret;
},
"buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); },
"buildTerritories": function() {
if (!this.get("BuildRestrictions"))
return undefined;
let territory = this.get("BuildRestrictions/Territory");
return !territory ? undefined : territory.split(/\s+/);
},
"hasBuildTerritory": function(territory) {
let territories = this.buildTerritories();
return territories && territories.indexOf(territory) != -1;
},
"hasTerritoryInfluence": function() {
return this.get("TerritoryInfluence") !== undefined;
},
"hasDefensiveFire": function() {
if (!this.get("Attack/Ranged"))
return false;
return this.getDefaultArrow() || this.getArrowMultiplier();
},
"territoryInfluenceRadius": function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Radius");
return -1;
},
"territoryInfluenceWeight": function() {
if (this.get("TerritoryInfluence") !== undefined)
return +this.get("TerritoryInfluence/Weight");
return -1;
},
"territoryDecayRate": function() {
return +(this.get("TerritoryDecay/DecayRate") || 0);
},
"defaultRegenRate": function() {
return +(this.get("Capturable/RegenRate") || 0);
},
"garrisonRegenRate": function() {
return +(this.get("Capturable/GarrisonRegenRate") || 0);
},
"visionRange": function() { return +this.get("Vision/Range"); },
"gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); },
"isBuilder": function() { return this.get("Builder") !== undefined; },
"isGatherer": function() { return this.get("ResourceGatherer") !== undefined; },
"canGather": function(type) {
let gatherRates = this.get("ResourceGatherer/Rates");
if (!gatherRates)
return false;
for (let r in gatherRates)
if (r.split('.')[0] === type)
return true;
return false;
},
"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; },
"isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },
/**
* returns true if the tempalte can capture the given target entity
* if no target is given, returns true if the template has the Capture attack
*/
"canCapture": function(target)
{
if (!this.get("Attack/Capture"))
return false;
if (!target)
return true;
if (!target.get("Capturable"))
return false;
let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses);
},
"isCapturable": function() { return this.get("Capturable") !== undefined; },
"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
"canGarrison": function() { return "Garrisonable" in this._template; },
"canOccupyTurret": function() { return "Turretable" in this._template; },
- "isTreasureCollecter": function() { return this.get("TreasureCollecter") !== undefined; },
+ "isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; },
});
// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
m.Entity = m.Class({
"_super": m.Template,
"_init": function(sharedAI, entity)
{
this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template));
this._entity = entity;
this._ai = sharedAI;
// save a reference to the template tech modifications
if (!sharedAI._templatesModifications[this._templateName])
sharedAI._templatesModifications[this._templateName] = {};
this._templateModif = sharedAI._templatesModifications[this._templateName];
// save a reference to the entity tech/aura modifications
if (!sharedAI._entitiesModifications.has(entity.id))
sharedAI._entitiesModifications.set(entity.id, new Map());
this._entityModif = sharedAI._entitiesModifications.get(entity.id);
},
"toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },
"id": function() { return this._entity.id; },
/**
* Returns extra data that the AI scripts have associated with this entity,
* for arbitrary local annotations.
* (This data should not be shared with any other AI scripts.)
*/
"getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); },
/**
* Sets extra data to be associated with this entity.
*/
"setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); },
"deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; },
"deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); },
"position": function() { return this._entity.position; },
"angle": function() { return this._entity.angle; },
"isIdle": function() { return this._entity.idle; },
"getStance": function() { return this._entity.stance; },
"unitAIState": function() { return this._entity.unitAIState; },
"unitAIOrderData": function() { return this._entity.unitAIOrderData; },
"hitpoints": function() { return this._entity.hitpoints; },
"isHurt": function() { return this.hitpoints() < this.maxHitpoints(); },
"healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); },
"needsHeal": function() { return this.isHurt() && this.isHealable(); },
"needsRepair": function() { return this.isHurt() && this.isRepairable(); },
"decaying": function() { return this._entity.decaying; },
"capturePoints": function() {return this._entity.capturePoints; },
"isInvulnerable": function() { return this._entity.invulnerability || false; },
"isSharedDropsite": function() { return this._entity.sharedDropsite === true; },
/**
* Returns the current training queue state, of the form
* [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
*/
"trainingQueue": function() {
return this._entity.trainingQueue;
},
"trainingQueueTime": function() {
let queue = this._entity.trainingQueue;
if (!queue)
return undefined;
let time = 0;
for (let item of queue)
time += item.timeRemaining;
return time / 1000;
},
"foundationProgress": function() {
return this._entity.foundationProgress;
},
"getBuilders": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return [];
return this._entity.foundationBuilders;
},
"getBuildersNb": function() {
if (this._entity.foundationProgress === undefined)
return undefined;
if (this._entity.foundationBuilders === undefined)
return 0;
return this._entity.foundationBuilders.length;
},
"owner": function() {
return this._entity.owner;
},
"isOwn": function(player) {
if (typeof this._entity.owner === "undefined")
return false;
return this._entity.owner === player;
},
"resourceSupplyAmount": function() {
return this._entity.resourceSupplyAmount;
},
"resourceSupplyNumGatherers": function()
{
return this._entity.resourceSupplyNumGatherers;
},
"isFull": function()
{
if (this._entity.resourceSupplyNumGatherers !== undefined)
return this.maxGatherers() === this._entity.resourceSupplyNumGatherers;
return undefined;
},
"resourceCarrying": function() {
return this._entity.resourceCarrying;
},
"currentGatherRate": function() {
// returns the gather rate for the current target if applicable.
if (!this.get("ResourceGatherer"))
return undefined;
if (this.unitAIOrderData().length &&
(this.unitAIState().split(".")[1] == "GATHER" || this.unitAIState().split(".")[1] == "RETURNRESOURCE"))
{
let res;
// this is an abuse of "_ai" but it works.
if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[0].target);
else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
res = this._ai._entities.get(this.unitAIOrderData()[1].target);
if (!res)
return 0;
let type = res.resourceSupplyType();
if (!type)
return 0;
let tstring = type.generic + "." + type.specific;
let rate = +this.get("ResourceGatherer/BaseSpeed");
rate *= +this.get("ResourceGatherer/Rates/" +tstring);
if (rate)
return rate;
return 0;
}
return undefined;
},
"garrisonHolderID": function() {
return this._entity.garrisonHolderID;
},
"garrisoned": function() { return this._entity.garrisoned; },
"garrisonedSlots": function() {
let count = 0;
if (this._entity.garrisoned)
for (let ent of this._entity.garrisoned)
count += +this._ai._entities.get(ent).garrisonSize();
return count;
},
"canGarrisonInside": function()
{
return this.garrisonedSlots() < this.garrisonMax();
},
/**
* returns true if the entity can attack (including capture) the given class.
*/
"canAttackClass": function(aClass)
{
let attack = this.get("Attack");
if (!attack)
return false;
for (let type in attack)
{
if (type == "Slaughter")
continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
return true;
}
return false;
},
/**
* Derived from Attack.js' similary named function.
* @return {boolean} - Whether an entity can attack a given target.
*/
"canAttackTarget": function(target, allowCapture)
{
let attackTypes = this.get("Attack");
if (!attackTypes)
return false;
let canCapture = allowCapture && this.canCapture(target);
let health = target.get("Health");
if (!health)
return canCapture;
for (let type in attackTypes)
{
if (type == "Capture" ? !canCapture : target.isInvulnerable())
continue;
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses))
return true;
}
return false;
},
"move": function(x, z, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront });
return this;
},
"moveToRange": function(x, z, min, max, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront });
return this;
},
"attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
// violent, aggressive, defensive, passive, standground
"setStance": function(stance, queued = false, pushFront = false) {
if (this.getStance() === undefined)
return undefined;
Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance, "queued": queued, "pushFront": pushFront });
return this;
},
"stopMoving": function() {
Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false });
},
"unload": function(id) {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] });
return this;
},
// Unloads all owned units, don't unload allies
"unloadAll": function() {
if (!this.get("GarrisonHolder"))
return undefined;
Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
return this;
},
"garrison": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"occupy-turret": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"attack": function(unitId, allowCapture = true, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
"collectTreasure": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, {
"type": "collect-treasure",
"entities": [this.id()],
"target": target.id(),
"queued": queued,
"pushFront": pushFront
});
return this;
},
// moveApart from a point in the opposite direction with a distance dist
"moveApart": function(point, dist) {
if (this.position() !== undefined)
{
let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
let norm = m.VectorDistance(point, this.position());
if (norm === 0)
direction = [1, 0];
else
{
direction[0] /= norm;
direction[1] /= norm;
}
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false, "pushFront": false });
}
return this;
},
// Flees from a unit in the opposite direction.
"flee": function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined)
{
let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
this.position()[1] - unitToFleeFrom.position()[1]];
let dist = m.VectorDistance(unitToFleeFrom.position(), this.position());
FleeDirection[0] = 40 * FleeDirection[0] / dist;
FleeDirection[1] = 40 * FleeDirection[1] / dist;
Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false, "pushFront": false });
}
return this;
},
"gather": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"repair": function(target, autocontinue = false, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront });
return this;
},
"returnResources": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"destroy": function() {
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
return this;
},
"barter": function(buyType, sellType, amount) {
Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
return this;
},
"tradeRoute": function(target, source) {
Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false });
return this;
},
"setRallyPoint": function(target, command) {
let data = { "command": command, "target": target.id() };
Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
return this;
},
"unsetRallyPoint": function() {
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] });
return this;
},
"train": function(civ, type, count, metadata, promotedTypes)
{
let trainable = this.trainableEntities(civ);
if (!trainable)
{
error("Called train("+type+", "+count+") on non-training entity "+this);
return this;
}
if (trainable.indexOf(type) == -1)
{
error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
return this;
}
Engine.PostCommand(PlayerID, {
"type": "train",
"entities": [this.id()],
"template": type,
"count": count,
"metadata": metadata,
"promoted": promotedTypes
});
return this;
},
"construct": function(template, x, z, angle, metadata) {
// TODO: verify this unit can construct this, just for internal
// sanity-checking and error reporting
Engine.PostCommand(PlayerID, {
"type": "construct",
"entities": [this.id()],
"template": template,
"x": x,
"z": z,
"angle": angle,
"autorepair": false,
"autocontinue": false,
"queued": false,
"pushFront": false,
"metadata": metadata // can be undefined
});
return this;
},
"research": function(template) {
Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template });
return this;
},
"stopProduction": function(id) {
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
return this;
},
"stopAllProduction": function(percentToStopAt) {
let queue = this._entity.trainingQueue;
if (!queue)
return true; // no queue, so technically we stopped all production.
for (let item of queue)
if (item.progress < percentToStopAt)
Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id });
return this;
},
"guard": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"removeGuard": function() {
Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
return this;
}
});
return m;
}(API3);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 25461)
@@ -1,436 +1,436 @@
/** returns true if this unit should be considered as a siege unit */
PETRA.isSiegeUnit = function(ent)
{
return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee");
};
/** returns true if this unit should be considered as "fast". */
PETRA.isFastMoving = function(ent)
{
// TODO: use clever logic based on walkspeed comparisons.
return ent.hasClass("FastMoving");
};
/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */
PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass)
{
let strength = 0;
let attackTypes = ent.attackTypes();
let damageTypes = Object.keys(DamageTypeImportance);
if (!attackTypes)
return strength;
for (let type of attackTypes)
{
if (type == "Slaughter")
continue;
let attackStrength = ent.attackStrengths(type);
for (let str in attackStrength)
{
let val = parseFloat(attackStrength[str]);
if (againstClass)
val *= ent.getMultiplierAgainst(type, againstClass);
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js).");
}
let attackRange = ent.attackRange(type);
if (attackRange)
strength += attackRange.max * 0.0125;
let attackTimes = ent.attackTimes(type);
for (let str in attackTimes)
{
let val = parseFloat(attackTimes[str]);
switch (str)
{
case "repeat":
strength += val / 100000;
break;
case "prepare":
strength -= val / 100000;
break;
default:
API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength");
}
}
}
let resistanceStrength = ent.resistanceStrengths();
if (resistanceStrength.Damage)
for (let str in resistanceStrength.Damage)
{
let val = +resistanceStrength.Damage[str];
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown resistanceStrength in getMaxStrength (please add " + str + " to config.js).");
}
// ToDo: Add support for StatusEffects and Capture.
return strength * ent.maxHitpoints() / 100.0;
};
/** Get access and cache it (except for units as it can change) in metadata if not already done */
PETRA.getLandAccess = function(gameState, ent)
{
if (ent.hasClass("Unit"))
{
let pos = ent.position();
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (holder)
return PETRA.getLandAccess(gameState, holder);
API3.warn("Petra error: entity without position, but not garrisoned");
PETRA.dumpEntity(ent);
return undefined;
}
return gameState.ai.accessibility.getAccessValue(pos);
}
let access = ent.getMetadata(PlayerID, "access");
if (!access)
{
access = gameState.ai.accessibility.getAccessValue(ent.position());
// Docks are sometimes not as expected
if (access < 2 && ent.buildPlacementType() == "shore")
{
let halfDepth = 0;
if (ent.get("Footprint/Square"))
halfDepth = +ent.get("Footprint/Square/@depth") / 2;
else if (ent.get("Footprint/Circle"))
halfDepth = +ent.get("Footprint/Circle/@radius");
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < halfDepth; d += 3)
{
let pos = [ entPos[0] - d * sina,
entPos[1] - d * cosa];
access = gameState.ai.accessibility.getAccessValue(pos);
if (access > 1)
break;
}
}
ent.setMetadata(PlayerID, "access", access);
}
return access;
};
/** Sea access always cached as it never changes */
PETRA.getSeaAccess = function(gameState, ent)
{
let sea = ent.getMetadata(PlayerID, "sea");
if (!sea)
{
sea = gameState.ai.accessibility.getAccessValue(ent.position(), true);
// Docks are sometimes not as expected
if (sea < 2 && ent.buildPlacementType() == "shore")
{
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < 15; d += 3)
{
let pos = [ entPos[0] + d * sina,
entPos[1] + d * cosa];
sea = gameState.ai.accessibility.getAccessValue(pos, true);
if (sea > 1)
break;
}
}
ent.setMetadata(PlayerID, "sea", sea);
}
return sea;
};
PETRA.setSeaAccess = function(gameState, ent)
{
PETRA.getSeaAccess(gameState, ent);
};
/** Decide if we should try to capture (returns true) or destroy (return false) */
PETRA.allowCapture = function(gameState, ent, target)
{
if (!target.isCapturable() || !ent.canCapture(target))
return false;
if (target.isInvulnerable())
return true;
// always try to recapture capture points from an allied, except if it's decaying
if (gameState.isPlayerAlly(target.owner()))
return !target.decaying();
let antiCapture = target.defaultRegenRate();
if (target.isGarrisonHolder() && target.garrisoned())
antiCapture += target.garrisonRegenRate() * target.garrisoned().length;
if (target.decaying())
antiCapture -= target.territoryDecayRate();
let capture;
let capturableTargets = gameState.ai.HQ.capturableTargets;
if (!capturableTargets.has(target.id()))
{
capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) });
}
else
{
let capturable = capturableTargets.get(target.id());
if (!capturable.ents.has(ent.id()))
{
capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturable.ents.add(ent.id());
}
capture = capturable.strength;
}
capture *= 1 / (0.1 + 0.9*target.healthLevel());
let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b);
if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned())
return capture > antiCapture + sumCapturePoints/50;
return capture > antiCapture + sumCapturePoints/80;
};
PETRA.getAttackBonus = function(ent, target, type)
{
let attackBonus = 1;
if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses"))
return attackBonus;
let bonuses = ent.get("Attack/" + type + "/Bonuses");
for (let key in bonuses)
{
let bonus = bonuses[key];
if (bonus.Civ && bonus.Civ !== target.civ())
continue;
if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls)))
continue;
attackBonus *= bonus.Multiplier;
}
return attackBonus;
};
/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */
PETRA.returnResources = function(gameState, ent)
{
if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position())
return false;
let resource = ent.resourceCarrying()[0].type;
let closestDropsite;
let distmin = Math.min();
let access = PETRA.getLandAccess(gameState, ent);
let dropsiteCollection = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource);
for (let dropsite of dropsiteCollection.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (PETRA.getLandAccess(gameState, dropsite) != access)
continue;
let dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
if (dist > distmin)
continue;
distmin = dist;
closestDropsite = dropsite;
}
if (!closestDropsite)
return false;
ent.returnResources(closestDropsite);
return true;
};
/** is supply full taking into account gatherers affected during this turn */
PETRA.IsSupplyFull = function(gameState, ent)
{
return ent.isFull() === true ||
ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers();
};
/**
* Get the best base (in terms of distance and accessIndex) for an entity.
* It should be on the same accessIndex for structures.
* If nothing found, return the base[0] for units and undefined for structures.
* If exclude is given, we exclude the base with ID = exclude.
*/
PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false)
{
let pos = ent.position();
let accessIndex;
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (!holder || !holder.position())
{
API3.warn("Petra error: entity without position, but not garrisoned");
PETRA.dumpEntity(ent);
return gameState.ai.HQ.baseManagers[0];
}
pos = holder.position();
accessIndex = PETRA.getLandAccess(gameState, holder);
}
else
accessIndex = PETRA.getLandAccess(gameState, ent);
let distmin = Math.min();
let dist;
let bestbase;
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude)
continue;
if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined))
continue;
if (ent.hasClass("Structure") && base.accessIndex != accessIndex)
continue;
if (base.anchor && base.anchor.position())
dist = API3.SquareVectorDistance(base.anchor.position(), pos);
else
{
let found = false;
for (let structure of base.buildings.values())
{
if (!structure.position())
continue;
dist = API3.SquareVectorDistance(structure.position(), pos);
found = true;
break;
}
if (!found)
continue;
}
if (base.accessIndex != accessIndex)
dist += 50000000;
if (!base.anchor)
dist += 50000000;
if (dist > distmin)
continue;
distmin = dist;
bestbase = base;
}
if (!bestbase && !ent.hasClass("Structure"))
bestbase = gameState.ai.HQ.baseManagers[0];
return bestbase;
};
PETRA.getHolder = function(gameState, ent)
{
for (let holder of gameState.getEntities().values())
{
if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1)
return holder;
}
return undefined;
};
/** return the template of the built foundation if a foundation, otherwise return the entity itself */
PETRA.getBuiltEntity = function(gameState, ent)
{
if (ent.foundationProgress() !== undefined)
return gameState.getBuiltTemplate(ent.templateName());
return ent;
};
/**
* return true if it is not worth finishing this building (it would surely decay)
* TODO implement the other conditions
*/
PETRA.isNotWorthBuilding = function(gameState, ent)
{
if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID)
{
let buildTerritories = ent.buildTerritories();
if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own"))
return true;
}
return false;
};
/**
* Check if the straight line between the two positions crosses an enemy territory
*/
PETRA.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70)
{
let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1;
let stepx = (pos2[0] - pos1[0]) / n;
let stepy = (pos2[1] - pos1[1]) / n;
for (let i = 1; i < n; ++i)
{
let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy];
let owner = gameState.ai.HQ.territoryMap.getOwner(pos);
if (owner && gameState.isPlayerEnemy(owner))
return true;
}
return false;
};
PETRA.gatherTreasure = function(gameState, ent, water = false)
{
if (!gameState.ai.HQ.treasures.hasEntities())
return false;
if (!ent || !ent.position())
return false;
- if (!ent.isTreasureCollecter())
+ if (!ent.isTreasureCollector())
return false;
let treasureFound;
let distmin = Math.min();
let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent);
for (let treasure of gameState.ai.HQ.treasures.values())
{
// let some time for the previous gatherer to reach the treasure before trying again
let lastGathered = treasure.getMetadata(PlayerID, "lastGathered");
if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20)
continue;
if (!water && access != PETRA.getLandAccess(gameState, treasure))
continue;
if (water && access != PETRA.getSeaAccess(gameState, treasure))
continue;
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner))
continue;
let dist = API3.SquareVectorDistance(ent.position(), treasure.position());
if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit
continue;
if (dist > distmin)
continue;
distmin = dist;
treasureFound = treasure;
}
if (!treasureFound)
return false;
treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime);
ent.collectTreasure(treasureFound);
ent.setMetadata(PlayerID, "treasure", treasureFound.id());
return true;
};
PETRA.dumpEntity = function(ent)
{
if (!ent)
return;
API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() +
" state " + ent.unitAIState());
API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") +
" subrole " + ent.getMetadata(PlayerID, "subrole"));
API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() +
" foundationProgress " + ent.foundationProgress());
API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") +
" garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") +
" plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport"));
API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") +
" gather-type " + ent.getMetadata(PlayerID, "gather-type") +
" target-foundation " + ent.getMetadata(PlayerID, "target-foundation") +
" PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy"));
};
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25461)
@@ -1,2140 +1,2140 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"resourceGatherers": cmpPlayer.GetResourceGatherers(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": cmpPlayer.CanBarter(),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
/**
* Called when the game ends if the current game is part of a campaign run.
*/
GuiInterface.prototype.GetCampaignGameEndData = function(player)
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (Trigger.prototype.OnCampaignGameEnd)
return Trigger.prototype.OnCampaignGameEnd();
return {};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
if (!ent)
return null;
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList(),
"controllable": cmpIdentity.IsControllable()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpPopulation = Engine.QueryInterface(ent, IID_Population);
if (cmpPopulation)
ret.population = {
"bonus": cmpPopulation.GetPopBonus()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo(),
"isUpgrading": cmpUpgrade.IsUpgrading()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue(),
"autoqueue": cmpProductionQueue.IsAutoQueueing()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (cmpTurretable)
ret.turretable = {
"ejectable": cmpTurretable.IsEjectable(),
"holder": cmpTurretable.HolderID()
};
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
ret.garrisonable = {
"holder": cmpGarrisonable.HolderID(),
"size": cmpGarrisonable.UnitSize()
};
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].attackName = cmpAttack.GetAttackName(type);
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = Engine.QueryInterface(ent, IID_Resistance);
if (cmpResistance)
ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"health": cmpHeal.GetHealth(),
"range": cmpHeal.GetRange().max,
"interval": cmpHeal.GetInterval(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetInterval(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
if (cmpTreasure)
ret.treasure = {
"collectTime": cmpTreasure.CollectionTime(),
"resources": cmpTreasure.Resources()
};
- let cmpTreasureCollecter = Engine.QueryInterface(ent, IID_TreasureCollecter);
- if (cmpTreasureCollecter)
- ret.treasureCollecter = true;
+ let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector);
+ if (cmpTreasureCollector)
+ ret.treasureCollector = true;
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep);
if (cmpUpkeep)
ret.upkeep = {
"interval": cmpUpkeep.GetInterval(),
"rates": cmpUpkeep.GetRates()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
{
let auraTemplate = AuraTemplates.Get(name);
if (!auraTemplate)
error("Template " + templateName + " has undefined aura " + name);
else
aurasTemplate[name] = auraTemplate;
}
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
if (!cmpAttackDetection)
return [];
return cmpAttackDetection.GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
if (!cmpPlayer)
return [];
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySoundForPlayer = function(player, data)
{
let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player);
let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound);
if (!cmpSound)
return;
let soundGroup = cmpSound.GetSoundGroup(data.name);
if (soundGroup)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player);
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle())
return { "idle": false };
let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable);
if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market);
return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return [];
return cmpPlayer.GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
"GetCampaignGameEndData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"PlaySoundForPlayer": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollector.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollector.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollector.js (revision 25461)
@@ -0,0 +1,127 @@
+function TreasureCollector() {}
+
+TreasureCollector.prototype.Schema =
+ "Defines the treasure collecting abilities." +
+ "" +
+ "2.0" +
+ "" +
+ "" +
+ "" +
+ "";
+
+TreasureCollector.prototype.Init = function()
+{
+};
+
+/**
+ * @return {Object} - Min/Max range at which this entity can claim a treasure.
+ */
+TreasureCollector.prototype.GetRange = function()
+{
+ return { "min": 0, "max": +this.template.MaxDistance };
+};
+
+/**
+ * @param {number} target - Entity ID of the target.
+ * @return {boolean} - Whether we can collect from the target.
+ */
+TreasureCollector.prototype.CanCollect = function(target)
+{
+ let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
+ return cmpTreasure && cmpTreasure.IsAvailable();
+};
+
+/**
+ * @param {number} target - The target to collect.
+ * @param {number} callerIID - The IID to notify on specific events.
+ *
+ * @return {boolean} - Whether we started collecting.
+ */
+TreasureCollector.prototype.StartCollecting = function(target, callerIID)
+{
+ if (this.target)
+ this.StopCollecting();
+
+ let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
+ if (!cmpTreasure || !cmpTreasure.IsAvailable())
+ return false;
+
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (cmpVisual)
+ cmpVisual.SelectAnimation("collecting_treasure", false, 1.0);
+
+ this.target = target;
+ this.callerIID = callerIID;
+
+ // ToDo: Implement rate modifiers.
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetTimeout(this.entity, IID_TreasureCollector, "CollectTreasure", cmpTreasure.CollectionTime(), null);
+
+ return true;
+};
+
+/**
+ * @param {string} reason - The reason why we stopped collecting, used to notify the caller.
+ */
+TreasureCollector.prototype.StopCollecting = function(reason)
+{
+ if (!this.target)
+ return;
+
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ delete this.timer;
+
+ delete this.target;
+
+ let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (cmpVisual)
+ cmpVisual.SelectAnimation("idle", false, 1.0);
+
+ // The callerIID component may start again,
+ // replacing the callerIID, hence save that.
+ let callerIID = this.callerIID;
+ delete this.callerIID;
+
+ if (reason && callerIID)
+ {
+ let component = Engine.QueryInterface(this.entity, callerIID);
+ if (component)
+ component.ProcessMessage(reason, null);
+ }
+};
+
+/**
+ * @params - Data and lateness are unused.
+ */
+TreasureCollector.prototype.CollectTreasure = function(data, lateness)
+{
+ let cmpTreasure = Engine.QueryInterface(this.target, IID_Treasure);
+ if (!cmpTreasure || !cmpTreasure.IsAvailable())
+ {
+ this.StopCollecting("TargetInvalidated");
+ return;
+ }
+
+ if (!this.IsTargetInRange(this.target))
+ {
+ this.StopCollecting("OutOfRange");
+ return;
+ }
+
+ cmpTreasure.Reward(this.entity);
+ this.StopCollecting("TargetInvalidated");
+};
+
+/**
+ * @param {number} - The entity ID of the target to check.
+ * @return {boolean} - Whether this entity is in range of its target.
+ */
+TreasureCollector.prototype.IsTargetInRange = function(target)
+{
+ let range = this.GetRange();
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
+};
+
+Engine.RegisterComponentType(IID_TreasureCollector, "TreasureCollector", TreasureCollector);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/TreasureCollector.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 25461)
@@ -1,6427 +1,6427 @@
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 cmpHolder = Engine.QueryInterface(this.entity, msg.data.iid);
if (!cmpHolder || cmpHolder.IsFull())
return this.FinishOrder();
let range = cmpHolder.LoadingRange();
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 if (this.AbleToMove())
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
else
return this.FinishOrder();
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.GUARDING");
else if (this.AbleToMove())
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
return this.FinishOrder();
return ACCEPT_ORDER;
},
"Order.Flee": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
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.AbleToMove())
return this.FinishOrder();
if (this.GetStance().respondStandGround && !msg.data.force)
return this.FinishOrder();
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
return ACCEPT_ORDER;
},
"Order.Gather": function(msg) {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return this.FinishOrder();
// We were given the order to gather while we were still gathering.
// This is needed because we don't re-enter the GATHER-state.
let lastGatheredType = cmpResourceGatherer.LastGatheredType();
if (lastGatheredType && msg.data.type.generic != lastGatheredType)
this.UnitFsm.SwitchToNextState(this, "INDIVIDUAL.GATHER");
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.
if (!cmpResourceGatherer.CanCarryMore(msg.data.type.generic))
{
this.SetNextState("INDIVIDUAL.GATHER.RETURNINGRESOURCE");
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 if (this.AbleToMove())
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
else
return this.FinishOrder();
return ACCEPT_ORDER;
},
"Order.GatherNearPosition": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
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) {
if (this.CheckTargetRange(msg.data.target, IID_ResourceGatherer))
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.DROPPINGRESOURCES");
else if (this.AbleToMove())
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
else
return this.FinishOrder();
return ACCEPT_ORDER;
},
"Order.Trade": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
// 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 if (this.AbleToMove())
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
else
return this.FinishOrder();
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.CheckTargetRange(msg.data.target, msg.data.garrison ? IID_Garrisonable : IID_Turretable))
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) {
- if (this.CheckTargetRange(msg.data.target, IID_TreasureCollecter))
+ if (this.CheckTargetRange(msg.data.target, IID_TreasureCollector))
this.SetNextState("INDIVIDUAL.COLLECTTREASURE.COLLECTING");
else if (this.AbleToMove())
this.SetNextState("INDIVIDUAL.COLLECTTREASURE.APPROACHING");
else
return this.FinishOrder();
return ACCEPT_ORDER;
},
"Order.CollectTreasureNearPosition": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
this.SetNextState("INDIVIDUAL.COLLECTTREASURE.WALKING");
msg.data.initPos = { 'x': msg.data.x, 'z': msg.data.z };
msg.data.relaxed = true;
return ACCEPT_ORDER;
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkAndFight": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKINGANDFIGHTING");
return ACCEPT_ORDER;
},
"Order.MoveIntoFormation": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
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();
if (!this.AbleToMove())
return this.FinishOrder();
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkToTarget": function(msg) {
if (this.CheckRange(msg.data))
return this.FinishOrder();
if (!this.AbleToMove())
return this.FinishOrder();
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.WalkToPointRange": function(msg) {
if (this.CheckRange(msg.data))
return this.FinishOrder();
if (!this.AbleToMove())
return this.FinishOrder();
this.SetNextState("WALKING");
return ACCEPT_ORDER;
},
"Order.Patrol": function(msg) {
if (!this.AbleToMove())
return this.FinishOrder();
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.AbleToMove() && 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,
msg.data.garrison ? IID_GarrisonHolder : IID_TurretHolder))
return this.FinishOrder();
if (this.CheckTargetRange(msg.data.target, msg.data.garrison ? IID_Garrisonable : IID_Turretable))
{
if (!this.AbleToMove() || !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) {
Engine.ProfileStart("FindWalkAndFightTargets");
this.FindWalkAndFightTargets();
Engine.ProfileStop();
},
"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() {
if (!this.MoveToTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
// If the holder should pickup, warn it so it can take needed action.
let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder);
if (cmpHolder && cmpHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity, "iid": this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder });
}
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(this.order.data.garrison ? "Garrison" : "OccupyTurret", [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;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
{
this.FinishOrder();
return true;
}
if (!this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return true;
}
this.ProcessMessage("OutOfRange");
return true;
}
if (!this.formationAnimationVariant)
this.SetAnimationVariant("combat");
this.FaceTowardsTarget(this.order.data.target);
this.RememberTargetPosition();
if (this.order.data.hunting && this.orderQueue.length > 1 && this.orderQueue[1].type === "Gather")
this.RememberTargetPosition(this.orderQueue[1].data);
if (!cmpAttack.StartAttacking(this.order.data.target, this.order.data.attackType, IID_UnitAI))
{
this.ProcessMessage("TargetInvalidated");
return true;
}
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);
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (cmpAttack)
cmpAttack.StopAttacking();
},
"OutOfRange": function() {
if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("CHASING");
return;
}
this.SetNextState("FINDINGNEWTARGET");
},
"TargetInvalidated": function() {
this.SetNextState("FINDINGNEWTARGET");
},
"Attacked": function(msg) {
if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) &&
this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture")
this.RespondToTargetedEntities([msg.data.attacker]);
},
},
"FINDINGNEWTARGET": {
"Order.Cheer": function() {
if (!this.cheeringTime)
return 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())
{
Engine.ProfileStart("FindWalkAndFightTargets");
this.FindWalkAndFightTargets();
Engine.ProfileStop();
}
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) || !this.AbleToMove())
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": {
"enter": function() {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
cmpResourceGatherer.AddToPlayerCounter(this.order.data.type.generic);
return false;
},
"leave": function() {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
cmpResourceGatherer.RemoveFromPlayerCounter();
// 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);
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;
let cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
},
},
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
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 (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.SetNextState("FINDINGNEWTARGET");
},
},
"GATHERING": {
"enter": function() {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
{
this.FinishOrder();
return true;
}
if (!this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
{
this.ProcessMessage("OutOfRange");
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;
if (!cmpResourceGatherer.StartGathering(this.order.data.target, IID_UnitAI))
{
this.ProcessMessage("TargetInvalidated");
return true;
}
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"leave": function() {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
cmpResourceGatherer.StopGathering();
},
"InventoryFilled": function(msg) {
this.SetNextState("RETURNINGRESOURCE");
},
"OutOfRange": function(msg) {
if (this.MoveToTargetRange(this.order.data.target, 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.order.data.target) && 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");
},
"TargetInvalidated": function(msg) {
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?).
// Using the default relaxed range check since that is used in the WALKING-state.
if (!this.CheckPointRangeExplicit(initPos.x, initPos.z, 0, this.DefaultRelaxedMaxRange))
{
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;
},
},
"RETURNINGRESOURCE": {
"enter": function() {
let nearestDropsite = this.FindNearestDropsite(this.order.data.type.generic);
if (!nearestDropsite)
{
// The player expects the unit to move upon failure.
let formerTarget = this.order.data.target;
if (!this.FinishOrder())
this.WalkToTarget(formerTarget);
return true;
}
this.order.data.formerTarget = this.order.data.target;
this.order.data.target = nearestDropsite;
this.SetDefaultAnimationVariant();
this.SetNextState("APPROACHING");
return true;
},
"leave": function() {
},
"APPROACHING": "INDIVIDUAL.RETURNRESOURCE.APPROACHING",
"DROPPINGRESOURCES": {
"enter": function() {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer))
{
cmpResourceGatherer.CommitResources(this.order.data.target);
this.SetNextState("GATHER.APPROACHING");
}
else
this.SetNextState("RETURNINGRESOURCE");
this.order.data.target = this.order.data.formerTarget;
return true;
},
"leave": function() {
},
},
},
},
"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() {
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (!cmpHeal)
{
this.FinishOrder();
return true;
}
if (!this.CheckRange(this.order.data, IID_Heal))
{
this.ProcessMessage("OutOfRange");
return true;
}
if (!cmpHeal.StartHealing(this.order.data.target, IID_UnitAI))
{
this.ProcessMessage("TargetInvalidated");
return true;
}
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"leave": function() {
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (cmpHeal)
cmpHeal.StopHealing();
},
"OutOfRange": function(msg) {
if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
{
if (this.CanPack())
this.PushOrderFront("Pack", { "force": true });
else
this.SetNextState("APPROACHING");
}
else
this.SetNextState("FINDINGNEWTARGET");
},
"TargetInvalidated": function(msg) {
this.SetNextState("FINDINGNEWTARGET");
},
},
"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) {
if (msg.likelyFailure || this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
this.SetNextState("DROPPINGRESOURCES");
},
},
"DROPPINGRESOURCES": {
"enter": function() {
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (this.CanReturnResource(this.order.data.target, true, cmpResourceGatherer))
{
cmpResourceGatherer.CommitResources(this.order.data.target);
// Stop showing the carried resource animation.
this.SetDefaultAnimationVariant();
this.FinishOrder();
return true;
}
let nearby = this.FindNearestDropsite(cmpResourceGatherer.GetMainCarryingType());
this.FinishOrder();
if (nearby)
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return true;
},
"leave": function() {
},
},
},
"COLLECTTREASURE": {
"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))
+ if (!this.MoveToTargetRange(this.order.data.target, IID_TreasureCollector))
{
this.SetNextState("FINDINGNEWTARGET");
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
- if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollecter))
+ if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollector))
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))
+ let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector);
+ if (!cmpTreasureCollector.StartCollecting(this.order.data.target, IID_UnitAI))
{
this.ProcessMessage("TargetInvalidated");
return true;
}
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"leave": function() {
- let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
- if (cmpTreasureCollecter)
- cmpTreasureCollecter.StopCollecting();
+ let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector);
+ if (cmpTreasureCollector)
+ cmpTreasureCollector.StopCollecting();
},
"OutOfRange": function(msg) {
this.SetNextState("APPROACHING");
},
"TargetInvalidated": function(msg) {
this.SetNextState("FINDINGNEWTARGET");
},
},
"FINDINGNEWTARGET": {
"enter": function() {
let oldTarget = this.order.data.target || INVALID_ENTITY;
// Switch to the next order (if any).
if (this.FinishOrder())
return true;
let nearbyTreasure = this.FindNearbyTreasure(this.TargetPosOrEntPos(oldTarget));
if (nearbyTreasure)
this.CollectTreasure(nearbyTreasure, true);
return true;
},
},
// Walking to a good place to collect treasures near, used by CollectTreasureNearPosition.
"WALKING": {
"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.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
this.CheckRange(this.order.data))
this.SetNextState("FINDINGNEWTARGET");
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
// TODO: Inform player
},
"leave": function() {
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader)
cmpTrader.StopTrading();
},
"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.FinishOrder();
}
else
this.SetNextState("TRADING");
},
},
"TRADING": {
"enter": function() {
if (!this.CanTrade(this.order.data.target))
{
this.FinishOrder();
return true;
}
if (!this.CheckTargetRange(this.order.data.target, IID_Trader))
{
this.SetNextState("APPROACHINGMARKET");
return true;
}
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let nextMarket = cmpTrader.PerformTrade(this.order.data.target);
let amount = cmpTrader.GetGoods().amount;
if (!nextMarket || !amount || !amount.traderGain)
{
this.FinishOrder();
return true;
}
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");
return true;
},
"leave": function() {
},
},
"TradingCanceled": function(msg) {
if (msg.market != this.order.data.target)
return;
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let otherMarket = cmpTrader && cmpTrader.GetFirstMarket();
if (otherMarket)
this.WalkToTarget(otherMarket);
else
this.FinishOrder();
},
},
"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() {
let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
{
this.FinishOrder();
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)
if (this.order.data.force)
this.order.data.autoharvest = true;
this.order.data.force = false;
if (!this.CheckTargetRange(this.order.data.target, IID_Builder))
{
this.ProcessMessage("OutOfRange");
return true;
}
let cmpHealth = Engine.QueryInterface(this.order.data.target, 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.order.data.target, "newentity": this.order.data.target });
return true;
}
if (!cmpBuilder.StartRepairing(this.order.data.target, IID_UnitAI))
{
this.ProcessMessage("TargetInvalidated");
return true;
}
this.FaceTowardsTarget(this.order.data.target);
return false;
},
"leave": function() {
let cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (cmpBuilder)
cmpBuilder.StopRepairing();
},
"OutOfRange": function(msg) {
this.SetNextState("APPROACHING");
},
"TargetInvalidated": function(msg) {
this.FinishOrder();
},
},
"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.order.data.garrison ? !this.CanGarrison(this.order.data.target) :
!this.CanOccupyTurret(this.order.data.target))
{
this.FinishOrder();
return true;
}
if (!this.MoveToTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable))
{
this.FinishOrder();
return true;
}
if (this.pickup)
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder);
if (cmpHolder && cmpHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target;
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity, "iid": this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder });
}
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.CheckTargetRange(this.order.data.target, this.order.data.garrison ? IID_Garrisonable : IID_Turretable))
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;
if (this.order.data.garrison)
{
let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
if (!cmpGarrisonable || !cmpGarrisonable.Garrison(target))
{
this.FinishOrder();
return true;
}
}
else
{
let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.OccupyTurret(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 cmpHolder = Engine.QueryInterface(this.entity, this.order.data.iid);
if (!cmpHolder || cmpHolder.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;
this.formationAnimationVariant = undefined;
this.cheeringTime = +(this.template.CheeringTime || 0);
this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsTurret = function()
{
if (!this.isGarrisoned)
return false;
let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
return cmpTurretable && cmpTurretable.HolderID() != 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.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.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, "iid": msg.iid });
};
UnitAI.prototype.OnPickupCanceled = function(msg)
{
for (let i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity)
continue;
if (i == 0)
this.UnitFsm.ProcessMessage(this, { "type": "PickupCanceled", "data": msg });
else
this.orderQueue.splice(i, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
break;
}
};
/**
* Wrapper function that sets up the LOS, healer and attack range queries.
* This should be called whenever our ownership changes.
*/
UnitAI.prototype.SetupRangeQueries = function()
{
if (this.GetStance().respondFleeOnSight)
this.SetupLOSRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
if (Engine.QueryInterface(this.entity, IID_Attack))
this.SetupAttackRangeQuery();
};
UnitAI.prototype.UpdateRangeQueries = function()
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
this.SetupLOSRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery));
if (this.losHealRangeQuery)
this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery));
if (this.losAttackRangeQuery)
this.SetupAttackRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losAttackRangeQuery));
};
/**
* Set up a range query for all enemy units within LOS range.
* @param {boolean} enable - Optional parameter whether to enable the query.
*/
UnitAI.prototype.SetupLOSRangeQuery = function(enable = true)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
this.losRangeQuery = undefined;
}
let cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner == -1), creating a range query is pointless.
if (!cmpPlayer)
return;
let players = cmpPlayer.GetEnemies();
if (!players.length)
return;
let range = this.GetQueryRange(IID_Vision);
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Identity,
cmpRangeManager.GetEntityFlagMask("normal"), false);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losRangeQuery);
};
/**
* Set up a range query for all own or ally units within LOS range
* which can be healed.
* @param {boolean} enable - Optional parameter whether to enable the query.
*/
UnitAI.prototype.SetupHealRangeQuery = function(enable = true)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losHealRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
this.losHealRangeQuery = undefined;
}
let cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner == -1), creating a range query is pointless.
if (!cmpPlayer)
return;
let players = cmpPlayer.GetAllies();
let range = this.GetQueryRange(IID_Heal);
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Health,
cmpRangeManager.GetEntityFlagMask("injured"), false);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery);
};
/**
* Set up a range query for all enemy and gaia units within range
* which can be attacked.
* @param {boolean} enable - Optional parameter whether to enable the query.
*/
UnitAI.prototype.SetupAttackRangeQuery = function(enable = true)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losAttackRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losAttackRangeQuery);
this.losAttackRangeQuery = undefined;
}
let cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner == -1), creating a range query is pointless.
if (!cmpPlayer)
return;
// TODO: How to handle neutral players - Special query to attack military only?
let players = cmpPlayer.GetEnemies();
if (!players.length)
return;
let range = this.GetQueryRange(IID_Attack);
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Resistance,
cmpRangeManager.GetEntityFlagMask("normal"), false);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
};
// FSM linkage functions
// Setting the next state to the current state will leave/re-enter the top-most substate.
// 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
});
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
});
}
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
});
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* Insert an order after the last forced order onto the queue
* and after the other orders of the same type
*/
UnitAI.prototype.PushOrderAfterForced = function(type, data)
{
if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type))
this.PushOrderFront(type, data);
else
{
for (let i = 1; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
continue;
if (this.orderQueue[i].type == type)
continue;
this.orderQueue.splice(i, 0, { "type": type, "data": data });
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
return;
}
this.PushOrder(type, data);
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* For a unit that is packing and trying to attack something,
* either cancel packing or continue with packing, as appropriate.
* Precondition: if the unit is packing/unpacking, then orderQueue
* should have the Attack order at index 0,
* and the Pack/Unpack order at index 1.
* This precondition holds because if we are packing while processing "Order.Attack",
* then we must have come from ReplaceOrder, which guarantees it.
*
* @param {boolean} requirePacked - true if the unit needs to be packed to continue attacking,
* false if it needs to be unpacked.
* @return {boolean} true if the unit can attack now, false if it must continue packing (or unpacking) first.
*/
UnitAI.prototype.EnsureCorrectPackStateForAttack = function(requirePacked)
{
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (!cmpPack ||
!cmpPack.IsPacking() ||
this.orderQueue.length != 2 ||
this.orderQueue[0].type != "Attack" ||
this.orderQueue[1].type != "Pack" &&
this.orderQueue[1].type != "Unpack")
return true;
if (cmpPack.IsPacked() == requirePacked)
{
// The unit is already in the packed/unpacked state we want.
// Delete the packing order.
this.orderQueue.splice(1, 1);
cmpPack.CancelPack();
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// Continue with the attack order.
return true;
}
// Move the attack order behind the unpacking order, to continue unpacking.
let tmp = this.orderQueue[0];
this.orderQueue[0] = this.orderQueue[1];
this.orderQueue[1] = tmp;
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
return false;
};
UnitAI.prototype.WillMoveFromFoundation = function(target, checkPacking = true)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (!IsOwnedByAllyOfEntity(this.entity, target) && cmpUnitAI && !cmpUnitAI.IsAnimal() &&
!Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() ||
checkPacking && this.IsPacking() || this.CanPack() || !this.AbleToMove())
return false;
return !this.CheckTargetRangeExplicit(target, g_LeaveFoundationRange, -1);
};
UnitAI.prototype.ReplaceOrder = function(type, data)
{
// Remember the previous work orders to be able to go back to them later if required
if (data && data.force)
{
if (this.IsFormationController())
this.CallMemberFunction("UpdateWorkOrders", [type]);
else
this.UpdateWorkOrders(type);
}
// 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)
{
if (this.IsTurret())
{
let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
return false;
}
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 });
};
/**
* Set up the UnitAI timer to run after 'offset' msecs, and then
* every 'repeat' msecs until StopTimer is called. A "Timer" message
* will be sent each time the timer runs.
*/
UnitAI.prototype.StartTimer = function(offset, repeat)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var data = { "timerRepeat": repeat };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (repeat === undefined)
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
else
this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
};
/**
* Stop the current UnitAI timer.
*/
UnitAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
UnitAI.prototype.OnMotionUpdate = function(msg)
{
if (msg.veryObstructed)
msg.obstructed = true;
this.UnitFsm.ProcessMessage(this, Object.assign({ "type": "MovementUpdate" }, msg));
};
/**
* Called directly by cmpFoundation and cmpRepairable to
* inform builders that repairing has finished.
* This not done by listening to a global message due to performance.
*/
UnitAI.prototype.ConstructionFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, { "type": "ConstructionFinished", "data": msg });
};
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
let changed = false;
let currentOrderChanged = false;
for (let i = 0; i < this.orderQueue.length; ++i)
{
let order = this.orderQueue[i];
if (order.data && order.data.target && order.data.target == msg.entity)
{
changed = true;
if (i == 0)
currentOrderChanged = true;
order.data.target = msg.newentity;
}
if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
{
changed = true;
if (i == 0)
currentOrderChanged = true;
order.data.formationTarget = msg.newentity;
}
}
if (!changed)
return;
if (currentOrderChanged)
this.UnitFsm.ProcessMessage(this, { "type": "OrderTargetRenamed", "data": msg });
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.OnAttacked = function(msg)
{
if (msg.fromStatusEffect)
return;
this.UnitFsm.ProcessMessage(this, { "type": "Attacked", "data": msg });
};
UnitAI.prototype.OnGuardedAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, { "type": "GuardedAttacked", "data": msg.data });
};
UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
this.UnitFsm.ProcessMessage(this, { "type": "LosRangeUpdate", "data": msg });
else if (msg.tag == this.losHealRangeQuery)
this.UnitFsm.ProcessMessage(this, { "type": "LosHealRangeUpdate", "data": msg });
else if (msg.tag == this.losAttackRangeQuery)
this.UnitFsm.ProcessMessage(this, { "type": "LosAttackRangeUpdate", "data": msg });
};
UnitAI.prototype.OnPackFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, { "type": "PackFinished", "packed": msg.packed });
};
/**
* 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
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 template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (template.indexOf("resource|") != -1)
template = template.slice(9);
let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
let type = cmpResourceSupply.GetType();
return 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)
+ let cmpTreasureCollector = Engine.QueryInterface(this.entity, IID_TreasureCollector);
+ if (!cmpTreasureCollector)
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));
+ return nearby.find(ent => cmpTreasureCollector.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, target);
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, target);
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);
};
/**
* 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, target);
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);
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.IsTargetInRange(target, type);
};
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);
};
/**
* 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, target);
if (!range)
return false;
let cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return false;
let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
let halfvision = cmpVision.GetRange() / 2;
let pos = cmpPosition.GetPosition();
let heldPosition = this.heldPosition;
if (heldPosition === undefined)
heldPosition = { "x": pos.x, "z": pos.z };
return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max;
};
UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
let range = cmpVision.GetRange();
let distance = PositionHelper.DistanceBetweenEntities(this.entity, target);
return distance < range;
};
UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttackAgainst(target, allowCapture);
};
/**
* Try to find one of the given entities which can be attacked,
* and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
var target = ents.find(target => this.CanAttack(target));
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to find one of the given entities which can be attacked
* and which is close to the hold position, and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackEntityInZone = function(ents)
{
var target = ents.find(target =>
this.CanAttack(target) &&
this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true)) &&
(this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
);
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to respond appropriately given our current stance,
* given a list of entities that match our stance's target criteria.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
if (!ents.length)
return false;
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents);
if (this.GetStance().respondFlee)
{
if (this.order && this.order.type == "Flee")
this.orderQueue.shift();
this.PushOrderFront("Flee", { "target": ents[0], "force": false });
return true;
}
return false;
};
/**
* @param {number} ents - An array of the IDs of the spotted entities.
* @return {boolean} - Whether we responded.
*/
UnitAI.prototype.RespondToSightedEntities = function(ents)
{
if (!ents || !ents.length)
return false;
if (this.GetStance().respondFleeOnSight)
{
this.Flee(ents[0], false);
return true;
}
return false;
};
/**
* Try to respond to healable entities.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToHealableEntities = function(ents)
{
let ent = ents.find(ent => this.CanHeal(ent));
if (!ent)
return false;
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
};
/**
* Returns true if we should stop following the target entity.
*/
UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type)
{
if (!this.CheckTargetVisible(target))
return true;
// Forced orders shouldn't be interrupted.
if (force)
return false;
// If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
let cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return false;
}
if (this.GetStance().respondHoldGround)
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type))
return true;
// Stop if it's left our vision range, unless we're especially persistent.
if (!this.GetStance().respondChaseBeyondVision)
if (!this.CheckTargetIsInVisionRange(target))
return true;
return false;
};
/*
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
if (!this.AbleToMove())
return false;
if (this.GetStance().respondChase)
return true;
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
let cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return true;
}
return force;
};
// External interface functions
/**
* Order a unit to leave the formation it is in.
* Used to handle queued no-formation orders for units in formation.
*/
UnitAI.prototype.LeaveFormation = function(queued = true)
{
// If queued, add the order even if we're not in formation,
// maybe we will be later.
if (!queued && !this.IsFormationMember())
return;
if (queued)
this.AddOrder("LeaveFormation", { "force": true }, queued);
else
this.PushOrderFront("LeaveFormation", { "force": true });
};
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.GetFormationTemplate = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || NULL_FORMATION;
};
UnitAI.prototype.MoveIntoFormation = function(cmd)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
for (var i = 0; i < this.orderQueue.length; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "WalkAndFight":
case "WalkToPointRange":
case "MoveIntoFormation":
case "GatherNearPosition":
case "Patrol":
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return targetPositions;
targetPositions.push(cmpTargetPosition.GetPosition2D());
return targetPositions;
case "Stop":
return [];
default:
error("GetTargetPositions: Unrecognised order type '"+order.type+"'");
return [];
}
}
return targetPositions;
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
* Intended for Formation to switch to column layout on long walks.
*/
UnitAI.prototype.ComputeWalkingDistance = function()
{
var distance = 0;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return 0;
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition2D();
var targetPositions = this.GetTargetPositions();
for (var i = 0; i < targetPositions.length; ++i)
{
distance += pos.distanceTo(targetPositions[i]);
// Remember this as the start position for the next order
pos = targetPositions[i];
}
return distance;
};
UnitAI.prototype.AddOrder = function(type, data, queued, pushFront)
{
if (this.expectedRoute)
this.expectedRoute = undefined;
if (pushFront)
this.PushOrderFront(type, data);
else if (queued)
this.PushOrder(type, data);
else
this.ReplaceOrder(type, data);
};
/**
* Adds guard/escort order to the queue, forced by the player.
*/
UnitAI.prototype.Guard = function(target, queued, 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;
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;
if (order.force)
this.orderQueue = [this.order];
return;
}
this.AddOrder("Attack", order, queued, pushFront);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued, pushFront)
{
// Not allowed to garrison when occupying a turret, at the moment.
if (this.isGarrisoned)
return;
if (target == this.entity)
return;
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true, "garrison": true }, queued, pushFront);
};
/**
* Adds ungarrison order to the queue.
*/
UnitAI.prototype.Ungarrison = function()
{
if (!this.isGarrisoned)
return;
this.AddOrder("Ungarrison", null, false);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.OccupyTurret = function(target, queued, pushFront)
{
if (target == this.entity)
return;
if (!this.CanOccupyTurret(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true, "garrison": false }, queued, pushFront);
};
/**
* 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;
if (order.force)
this.orderQueue = [this.order];
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;
this.orderQueue = [this.order];
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, queued, pushFront)
{
this.AddOrder("CollectTreasure", {
"target": target,
"force": true
}, queued, pushFront);
};
/**
* Adds order to collect a treasure to queue, forced by the player.
*/
UnitAI.prototype.CollectTreasureNearPosition = function(posX, posZ, queued, pushFront)
{
this.AddOrder("CollectTreasureNearPosition", {
"x": posX,
"z": posZ,
"force": true
}, queued, pushFront);
};
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.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 });
};
/**
* 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;
this.orderQueue = [this.order];
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())
{
let cmpUnitAI;
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
for (let ent of cmpFormation.members)
{
if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI)))
continue;
if (cmpUnitAI.FindWalkAndFightTargets())
return true;
}
return false;
}
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
let entities;
if (!this.losAttackRangeQuery || !this.GetStance().targetVisibleEnemies || !cmpAttack)
entities = [];
else
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery);
}
let attackfilter = e => {
if (this?.order?.data?.targetClasses)
{
let cmpIdentity = Engine.QueryInterface(e, IID_Identity);
let targetClasses = this.order.data.targetClasses;
if (cmpIdentity && targetClasses.attack &&
!MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
return false;
if (cmpIdentity && targetClasses.avoid &&
MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
return false;
// Only used by the AIs to prevent some choices of targets
if (targetClasses.vetoEntities && targetClasses.vetoEntities[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 prefs = {};
let bestPref;
let targets = [];
let pref;
for (let v of entities)
{
if (this.CanAttack(v) && attackfilter(v))
{
pref = cmpAttack.GetPreference(v);
if (pref === 0)
{
this.PushOrderFront("Attack", { "target": v, "force": false, "allowCapture": this?.order?.data?.allowCapture });
return true;
}
targets.push(v);
}
prefs[v] = pref;
if (pref !== undefined && (bestPref === undefined || pref < bestPref))
bestPref = pref;
}
for (let targ of targets)
{
if (prefs[targ] !== bestPref)
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.GetQueryRange = function(iid)
{
let ret = { "min": 0, "max": 0 };
let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
let visionRange = cmpVision.GetRange();
if (iid === IID_Vision)
{
ret.max = visionRange;
return ret;
}
if (this.GetStance().respondStandGround)
{
let range = this.GetRange(iid);
if (!range)
return ret;
ret.min = range.min;
ret.max = Math.min(range.max, visionRange);
}
else if (this.GetStance().respondChase)
ret.max = visionRange;
else if (this.GetStance().respondHoldGround)
{
let range = this.GetRange(iid);
if (!range)
return ret;
ret.max = Math.min(range.max + visionRange / 2, visionRange);
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
ret.max = visionRange;
return ret;
};
UnitAI.prototype.GetStance = function()
{
return g_Stances[this.stance];
};
UnitAI.prototype.GetSelectableStances = function()
{
if (this.IsTurret())
return [];
return Object.keys(g_Stances).filter(key => g_Stances[key].selectable);
};
UnitAI.prototype.GetStanceName = function()
{
return this.stance;
};
/*
* Make the unit walk at its normal pace.
*/
UnitAI.prototype.ResetSpeedMultiplier = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(1);
};
UnitAI.prototype.SetSpeedMultiplier = function(speed)
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(speed);
};
/**
* Try to match the targets current movement speed.
*
* @param {number} target - The entity ID of the target to match.
* @param {boolean} mayRun - Whether the entity is allowed to run to match the speed.
*/
UnitAI.prototype.TryMatchTargetSpeed = function(target, mayRun = true)
{
let cmpUnitMotionTarget = Engine.QueryInterface(target, IID_UnitMotion);
if (cmpUnitMotionTarget)
{
let targetSpeed = cmpUnitMotionTarget.GetCurrentSpeed();
if (targetSpeed)
this.SetSpeedMultiplier(Math.min(mayRun ? this.GetRunMultiplier() : 1, targetSpeed / this.GetWalkSpeed()));
}
};
/*
* Remember the position of the target (in lastPos), if any, in case it disappears later
* and we want to head to its last known position.
* @param orderData - The order data to set this on. Defaults to this.order.data
*/
UnitAI.prototype.RememberTargetPosition = function(orderData)
{
if (!orderData)
orderData = this.order.data;
let cmpPosition = Engine.QueryInterface(orderData.target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
orderData.lastPos = cmpPosition.GetPosition();
};
UnitAI.prototype.SetHeldPosition = function(x, z)
{
this.heldPosition = { "x": x, "z": z };
};
UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
};
UnitAI.prototype.GetHeldPosition = function()
{
return this.heldPosition;
};
UnitAI.prototype.WalkToHeldPosition = function()
{
if (this.heldPosition)
{
this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false, false);
return true;
}
return false;
};
// Helper functions
/**
* General getter for ranges.
*
* @param {number} iid
* @param {number} target - [Optional]
* @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, target)
{
let component = Engine.QueryInterface(this.entity, iid);
if (!component)
return undefined;
return component.GetRange(type, target);
};
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 cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
return cmpGarrisonable && cmpGarrisonable.CanGarrison(target);
};
UnitAI.prototype.CanGather = 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 cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
return cmpResourceGatherer && cmpResourceGatherer.CanGather(target);
};
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)
{
// 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);
return cmpResourceGatherer && cmpResourceGatherer.CanReturnResource(target, checkCarriedResource);
};
UnitAI.prototype.CanTrade = 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 cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
return cmpTrader && cmpTrader.CanTrade(target);
};
UnitAI.prototype.CanRepair = 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 cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
return cmpBuilder && cmpBuilder.CanRepair(target);
};
UnitAI.prototype.CanOccupyTurret = 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 cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
return cmpTurretable && cmpTurretable.CanOccupy(target);
};
UnitAI.prototype.CanPack = function()
{
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && cmpPack.CanPack();
};
UnitAI.prototype.CanUnpack = function()
{
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && cmpPack.CanUnpack();
};
UnitAI.prototype.IsPacking = function()
{
let cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && cmpPack.IsPacking();
};
// Formation specific functions
UnitAI.prototype.IsAttackingAsFormation = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttackAsFormation() &&
this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING";
};
UnitAI.prototype.MoveRandomly = function(distance)
{
// To minimize drift all across the map, describe circles
// approximated by polygons.
// And to avoid getting stuck in obstacles or narrow spaces, each side
// of the polygon is obtained by trying to go away from a point situated
// half a meter backwards of the current position, after rotation.
// We also add a fluctuation on the length of each side of the polygon (dist)
// which, in addition to making the move more random, helps escaping narrow spaces
// with bigger values of dist.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion)
return;
let pos = cmpPosition.GetPosition();
let ang = cmpPosition.GetRotation().y;
if (!this.roamAngle)
{
this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6;
ang -= this.roamAngle / 2;
this.startAngle = ang;
}
else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2))
this.roamAngle *= randBool() ? 1 : -1;
let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4);
// First half rotation to decrease the impression of immediate rotation
ang += halfDelta;
cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang));
// Then second half of the rotation
ang += halfDelta;
let dist = randFloat(0.5, 1.5) * distance;
cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1);
};
UnitAI.prototype.SetFacePointAfterMove = function(val)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion)
cmpMotion.SetFacePointAfterMove(val);
};
UnitAI.prototype.GetFacePointAfterMove = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion && cmpUnitMotion.GetFacePointAfterMove();
};
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
{
if (!ents.length)
return false;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
let attackfilter = function(e) {
if (!cmpAttack.CanAttack(e))
return false;
let cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
let entsByPreferences = {};
let preferences = [];
let entsWithoutPref = [];
for (let ent of ents)
{
if (!attackfilter(ent))
continue;
let pref = cmpAttack.GetPreference(ent);
if (pref === null || pref === undefined)
entsWithoutPref.push(ent);
else if (!entsByPreferences[pref])
{
preferences.push(pref);
entsByPreferences[pref] = [ent];
}
else
entsByPreferences[pref].push(ent);
}
if (preferences.length)
{
preferences.sort((a, b) => a - b);
for (let pref of preferences)
if (this.RespondToTargetedEntities(entsByPreferences[pref]))
return true;
}
return this.RespondToTargetedEntities(entsWithoutPref);
};
/**
* Call UnitAI.funcname(args) on all formation members.
* @param resetWaitingEntities - If true, call ResetWaitingEntities first.
* If the controller wants to wait on its members to finish their order,
* this needs to be reset before sending new orders (in case they instafail)
* so it makes sense to do it here.
* Only set this to false if you're sure it's safe.
*/
UnitAI.prototype.CallMemberFunction = function(funcname, args, resetWaitingEntities = true)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
if (resetWaitingEntities)
cmpFormation.ResetWaitingEntities();
cmpFormation.GetMembers().forEach(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
/**
* Call obj.funcname(args) on UnitAI components owned by player in given range.
*/
UnitAI.prototype.CallPlayerOwnedEntitiesFunctionInRange = function(funcname, args, range)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
let owner = cmpOwnership.GetOwner();
if (owner == INVALID_PLAYER)
return;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, [owner], IID_UnitAI, true);
for (let i = 0; i < nearby.length; ++i)
{
let cmpUnitAI = Engine.QueryInterface(nearby[i], IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
}
};
/**
* Call obj.functname(args) on UnitAI components of all formation members,
* and return true if all calls return true.
*/
UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
{
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
return cmpFormation && cmpFormation.GetMembers().every(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollector.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollector.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollector.js (revision 25461)
@@ -0,0 +1 @@
+Engine.RegisterInterface("TreasureCollector");
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TreasureCollector.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 25461)
@@ -1,616 +1,616 @@
Engine.LoadHelperScript("ObstructionSnap.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/Population.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
-Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
+Engine.LoadComponentScript("interfaces/TreasureCollector.js");
Engine.LoadComponentScript("interfaces/Turretable.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/Upkeep.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
"GetPrices": function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
}
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
"GetVictoryConditions": () => ["conquest", "wonder"],
"GetAlliedVictory": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetNumPlayers": function() { return 2; },
"GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; },
"GetMaxWorldPopulation": function() {}
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetLosVisibility": function(ent, player) { return "visible"; },
"GetLosCircular": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "example"; },
"GetTemplate": function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetTime": function() { return 0; },
"SetTimeout": function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
"GetName": function() { return "Player 1"; },
"GetCiv": function() { return "gaia"; },
"GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; },
"CanControlAllUnits": function() { return false; },
"GetPopulationCount": function() { return 10; },
"GetPopulationLimit": function() { return 20; },
"GetMaxPopulation": function() { return 200; },
"GetResourceCounts": function() { return { "food": 100 }; },
"GetResourceGatherers": function() { return { "food": 1 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() { return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return false; },
"IsMutualAlly": function() { return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return true; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"CanBarter": function() { return false; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(100, IID_EntityLimits, {
"GetLimits": function() { return { "Foo": 10 }; },
"GetCounts": function() { return { "Foo": 5 }; },
"GetLimitChangers": function() { return { "Foo": {} }; },
"GetMatchCounts": function() { return { "Bar": 0 }; }
});
AddMock(100, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(100, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
AddMock(101, IID_Player, {
"GetName": function() { return "Player 2"; },
"GetCiv": function() { return "mace"; },
"GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; },
"CanControlAllUnits": function() { return true; },
"GetPopulationCount": function() { return 40; },
"GetPopulationLimit": function() { return 30; },
"GetMaxPopulation": function() { return 300; },
"GetResourceCounts": function() { return { "food": 200 }; },
"GetResourceGatherers": function() { return { "food": 3 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() {return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return true; },
"IsMutualAlly": function() {return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return false; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"CanBarter": function() { return false; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(101, IID_EntityLimits, {
"GetLimits": function() { return { "Bar": 20 }; },
"GetCounts": function() { return { "Bar": 0 }; },
"GetLimitChangers": function() { return { "Bar": {} }; },
"GetMatchCounts": function() { return { "Foo": 0 }; }
});
AddMock(101, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(101, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"resourceGatherers": { "food": 1 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"resourceGatherers": { "food": 3 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"resourceGatherers": { "food": 1 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"matchEntityCounts": { "Bar": 0 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"resourceGatherers": { "food": 3 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"matchEntityCounts": { "Foo": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false,
"maxWorldPopulation": undefined
});
AddMock(10, IID_Builder, {
"GetEntitiesList": function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
"GetHitpoints": function() { return 50; },
"GetMaxHitpoints": function() { return 60; },
"IsRepairable": function() { return false; },
"IsUnhealable": function() { return false; }
});
AddMock(10, IID_Identity, {
"GetClassesList": function() { return ["class1", "class2"]; },
"GetRank": function() { return "foo"; },
"GetSelectionGroupName": function() { return "Selection Group Name"; },
"HasClass": function() { return true; },
"IsUndeletable": function() { return false; },
"IsControllable": function() { return true; },
"HasSomeFormation": function() { return false; },
"GetFormationsList": function() { return []; },
});
AddMock(10, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() {
return { "x": 1, "y": 2, "z": 3 };
},
"IsInWorld": function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetInterval": () => 1250,
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
"id": 10,
"player": INVALID_PLAYER,
"template": "example",
"identity": {
"rank": "foo",
"classes": ["class1", "class2"],
"selectionGroupName": "Selection Group Name",
"canDelete": true,
"hasSomeFormation": false,
"formations": [],
"controllable": true,
},
"position": { "x": 1, "y": 2, "z": 3 },
"hitpoints": 50,
"maxHitpoints": 60,
"needsRepair": false,
"needsHeal": true,
"builder": true,
"visibility": "visible",
"isBarterMarket": true,
"resourceTrickle": {
"interval": 1250,
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
}
});
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollector.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollector.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollector.js (revision 25461)
@@ -0,0 +1,47 @@
+Engine.LoadHelperScript("Player.js");
+Engine.LoadComponentScript("interfaces/Timer.js");
+Engine.LoadComponentScript("interfaces/Treasure.js");
+Engine.LoadComponentScript("interfaces/TreasureCollector.js");
+Engine.LoadComponentScript("interfaces/UnitAI.js");
+Engine.LoadComponentScript("Timer.js");
+Engine.LoadComponentScript("TreasureCollector.js");
+
+AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
+ "IsInTargetRange": () => true
+});
+
+const entity = 11;
+let treasure = 12;
+let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
+
+let cmpTreasurer = ConstructComponent(entity, "TreasureCollector", {
+ "MaxDistance": "2.0"
+});
+
+TS_ASSERT(!cmpTreasurer.StartCollecting(treasure));
+
+let cmpTreasure = AddMock(treasure, IID_Treasure, {
+ "Reward": (ent) => true,
+ "CollectionTime": () => 1000,
+ "IsAvailable": () => true
+});
+let spyTreasure = new Spy(cmpTreasure, "Reward");
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(spyTreasure._called, 1);
+
+// Test that starting to collect twice merely collects once.
+spyTreasure._called = 0;
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(spyTreasure._called, 1);
+
+// Test callback is called.
+let cmpUnitAI = AddMock(entity, IID_UnitAI, {
+ "ProcessMessage": (type, data) => TS_ASSERT_EQUALS(type, "TargetInvalidated")
+});
+let spyUnitAI = new Spy(cmpUnitAI, "ProcessMessage");
+TS_ASSERT(cmpTreasurer.StartCollecting(treasure, IID_UnitAI));
+cmpTimer.OnUpdate({ "turnLength": 1 });
+TS_ASSERT_EQUALS(spyUnitAI._called, 1);
Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TreasureCollector.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+text/plain
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasures.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasures.js (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Treasures.js (revision 25461)
@@ -1,82 +1,82 @@
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetTradableCodes": () => ["food", "metal", "stone", "wood"],
"GetBarterableCodes": () => ["food", "metal", "stone", "wood"],
"BuildSchema": () => {
let schema = "";
for (let res of ["food", "metal"])
{
for (let subtype in ["meat", "grain"])
schema += "" + res + "." + subtype + "";
schema += " treasure." + res + "";
}
return "" + schema + "";
},
"GetResource": (type) => {
return {
"subtypes": {
"meat": "meat",
"grain": "grain"
}
};
}
};
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Player.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
-Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
+Engine.LoadComponentScript("interfaces/TreasureCollector.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Player.js");
Engine.LoadComponentScript("Timer.js");
Engine.LoadComponentScript("Treasure.js");
-Engine.LoadComponentScript("TreasureCollecter.js");
+Engine.LoadComponentScript("TreasureCollector.js");
Engine.LoadComponentScript("Trigger.js");
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal);
ConstructComponent(SYSTEM_ENTITY, "Trigger", {});
const treasure = 11;
const treasurer = 12;
const owner = 1;
AddMock(treasurer, IID_Ownership, {
"GetOwner": () => owner
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": (id) => owner
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": (ent, target, min, max, invert) => true
});
let cmpPlayer = ConstructComponent(owner, "Player", {
"SpyCostMultiplier": 1,
"BarterMultiplier": {
"Buy": {},
"Sell": {}
}
});
let playerSpy = new Spy(cmpPlayer, "AddResources");
let cmpTreasure = ConstructComponent(treasure, "Treasure", {
"CollectTime": "1000",
"Resources": {
"Food": "10"
}
});
cmpTreasure.OnOwnershipChanged({ "to": 0 });
-let cmpTreasurer = ConstructComponent(treasurer, "TreasureCollecter", {
+let cmpTreasurer = ConstructComponent(treasurer, "TreasureCollector", {
"MaxDistance": "2.0"
});
TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(playerSpy._called, 1);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit.xml (revision 25461)
@@ -1,145 +1,145 @@
110000falsefalse30.00.010.04.01corpse10000falsegaiaUnit Organic ConquestCriticalUnit
special/formations/null
special/formations/box
special/formations/column_closed
special/formations/line_closed
special/formations/column_open
special/formations/line_open
special/formations/flank
special/formations/battle_line
malefalseunittruetruefalsefalsetruefalsefalsefalse0pitchfalse0.08111128x128/ellipse.png128x128/ellipse_mask.pnginterface/alarm/alarm_attackplayer.xmlinterface/alarm/alarm_attacked_gaia.xmlinterface/alarm/alarm_attackplayer.xmlinterface/alarm/alarm_attacked_gaia.xml2.00.3335.0
-
+ 2
-
+
aggressive12.0falsetruetrue12800falsedefault9.01.67falsefalsefalsefalse12falsetruefalsefalse
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_catafalque.xml (revision 25461)
@@ -1,59 +1,59 @@
2500102.0-Organic -ConquestCriticalRelicCatafalqueunits/catafalque.pngtemplate_unit_catafalqueA catafalque that holds the remains of a great leader.truepitch-roll128x256/cartouche.png128x256/cartouche_mask.pngactor/singlesteps/steps_grass_order.xmlactor/singlesteps/steps_grass.xmlactor/singlesteps/steps_grass.xml
-
+ standgroundfalselarge0.55units/global/catafalque.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 25461)
@@ -1,74 +1,74 @@
Fangs7235001000Structure Ship Siege1501001.5110War DogCannot attack Structures, Ships, or Siege Engines.Human FastMovingDog Melee10010128x256/ellipse.png128x256/ellipse_mask.pngvoice/global/civ_dog_move.xmlvoice/global/civ_dog_move.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/weapon/sword.xmlactor/fauna/death/death_animal_gen.xmlinterface/complete/building/complete_kennel.xml2.5WarDog
-
+ 1.5230
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_fauna.xml (revision 25461)
@@ -1,56 +1,56 @@
04.0-ConquestCritical Animalgaia/fauna_generic.pngfood4128x256/ellipse.png128x256/ellipse_mask.pngAnimal
-
+ passivefalsefalse8.024.02000800015000600000.7true60
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship.xml (revision 25461)
@@ -1,77 +1,77 @@
true0.00.57.010.01010FemaleCitizen Infantry Healer Dog0FemaleCitizen Infantry Healer Dog010truetrueShip-OrganicShipuprighttrue034.05105128x512/ellipse.png128x512/ellipse_mask.pnginterface/alarm/alarm_create_warship.xmlactor/ship/warship_move_01.xmlactor/ship/warship_move_01.xmlactor/ship/warship_move_01.xmlactor/ship/warship_death.xml6.00.56.0
-
+ ship
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_merchant.xml (revision 25461)
@@ -1,49 +1,49 @@
2010015Support Cavalry240Merchantmantemplate_unit_ship_merchantTrade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures.-ConquestCriticalTrader Bribablephase_town200.750.2
-
+ 12
-
+
passivefalsefalseship-small1.3550true
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml (revision 25460)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml (revision 25461)
@@ -1,63 +1,63 @@
0.501550-0.25trueSlavetemplate_unit_support_slaveGatherer with a finite life span. Bonused at mining and lumbering.Worker Slave1052.01.00.50.50.3511.051.051.010101010
-
+ resource/construction/con_wood.xmlresource/foraging/forage_leaves.xmlresource/farming/farm.xmlresource/lumbering/lumbering.xmlresource/mining/pickaxe.xmlresource/mining/mining.xmlinterface/alarm/alarm_invalid_building_placement.xmlvoice/{lang}/civ/civ_{phenotype}_build.xmlvoice/{lang}/civ/civ_{phenotype}_repair.xmlactor/singlesteps/steps_gravel_trained.xml