diff --git a/binaries/data/mods/public/gui/session/unit_commands.js b/binaries/data/mods/public/gui/session/unit_commands.js
index a76abebdbf..d546de9413 100644
--- a/binaries/data/mods/public/gui/session/unit_commands.js
+++ b/binaries/data/mods/public/gui/session/unit_commands.js
@@ -274,7 +274,7 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
}
else if (guiName == "Stance")
{
- var stanceSelected = Engine.GuiInterfaceCall("StanceSelected", {
+ var stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": g_Selection.toList(),
"stance": item
});
@@ -385,13 +385,19 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
var formations = getEntityFormationsList(entState);
if (isUnit(entState) && !isAnimal(entState) && !entState.garrisonHolder && formations.length)
+ {
setupUnitPanel("Formation", usedPanels, entState, formations,
function (item) { performFormation(entState.id, item); } );
+ }
- var stances = ["violent","aggressive","passive","defensive","stand"];
+ // TODO: probably should load the stance list from a data file,
+ // and/or vary depending on what units are selected
+ var stances = ["violent", "aggressive", "passive", "defensive", "stand"];
if (isUnit(entState) && !isAnimal(entState) && !entState.garrisonHolder && stances.length)
+ {
setupUnitPanel("Stance", usedPanels, entState, stances,
function (item) { performStance(entState.id, item); } );
+ }
if (entState.buildEntities && entState.buildEntities.length)
{
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index cbe1c0af1a..3d678853ba 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -298,7 +298,7 @@ GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
return CanMoveEntsIntoFormation(data.ents, data.formationName);
};
-GuiInterface.prototype.StanceSelected = function(player, data)
+GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for each (var ent in data.ents)
{
@@ -567,7 +567,7 @@ var exposedFunctions = {
"GetNextNotification": 1,
"CanMoveEntsIntoFormation": 1,
- "StanceSelected": 1,
+ "IsStanceSelected": 1,
"SetSelectionHighlight": 1,
"SetStatusBars": 1,
diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index 0c1d030da6..703bc4d107 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -5,9 +5,11 @@ UnitAI.prototype.Schema =
"" +
"" +
"" +
+ "violent" +
"aggressive" +
"defensive" +
"passive" +
+ "stand" +
"" +
"" +
"" +
@@ -45,60 +47,64 @@ UnitAI.prototype.Schema =
"" +
"";
-// Very basic stance support (currently just for test maps where we don't want
-// everyone killing each other immediately after loading, and for female citizens)
+// Unit stances.
// There some targeting options:
// targetVisibleEnemies: anything in vision range is a viable target
// targetAttackers: anything that hurts us is a viable target
// There are some response options, triggered when targets are detected:
// respondFlee: run away
// respondChase: start chasing after the enemy
-// TODO: maybe add respondStandGround, respondHoldGround (allow chasing a short distance then return),
-// targetAggressiveEnemies (don't worry about lone scouts, do worry around armies slaughtering
-// the guy standing next to you), etc.
-// TODO: even this limited version isn't implemented properly yet (e.g. it can't handle
-// dynamic stance changes).
+// 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,
targetAttackers: true,
respondFlee: false,
respondChase: true,
- respondStandGround : false,
- respondHoldGround : false,
- respondChaseToHell : true,
+ respondChaseBeyondVision: true,
+ respondStandGround: false,
+ respondHoldGround: false,
},
"aggressive": {
targetVisibleEnemies: true,
targetAttackers: true,
respondFlee: false,
respondChase: true,
- respondStandGround : false,
- respondHoldGround : false,
+ respondChaseBeyondVision: false,
+ respondStandGround: false,
+ respondHoldGround: false,
},
"defensive": {
targetVisibleEnemies: true,
targetAttackers: true,
respondFlee: false,
respondChase: false,
- respondStandGround : false,
- respondHoldGround : true,
+ respondChaseBeyondVision: false,
+ respondStandGround: false,
+ respondHoldGround: true,
},
"passive": {
targetVisibleEnemies: false,
targetAttackers: true,
respondFlee: true,
respondChase: false,
- respondStandGround : false,
- respondHoldGround : false,
+ respondChaseBeyondVision: false,
+ respondStandGround: false,
+ respondHoldGround: false,
},
"stand": {
targetVisibleEnemies: true,
targetAttackers: true,
respondFlee: false,
respondChase: false,
- respondStandGround : true,
- respondHoldGround : false,
+ respondChaseBeyondVision: false,
+ respondStandGround: true,
+ respondHoldGround: false,
},
};
@@ -137,11 +143,6 @@ var UnitFsmSpec = {
// ignore
},
- "StanceChanged": function(msg) {
- if (this.StanceSpecificQuery(this.stance,true))
- return;
- },
-
// Formation handlers:
"FormationLeave": function(msg) {
@@ -246,27 +247,39 @@ var UnitFsmSpec = {
}
this.attackType = type;
+ // If we are already at the target, try attacking it from here
if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
{
- // We are already at the target
- // so try attacking it from here.
this.StopMoving();
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
+ return;
}
+
+ // If we can't reach the target, but are standing ground,
+ // then abandon this attack order
+ if (this.GetStance().respondStandGround && !this.order.data.force)
+ {
+ this.FinishOrder();
+ return;
+ }
+
// Try to move within attack range
- else if ((!this.GetStance().respondStandGround || this.order.data.force) && this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
+ if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
{
// We've started walking to the given point
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
+ return;
}
- else
- this.FinishOrder();
+
+ // We can't reach the target, and can't move towards it,
+ // so abandon this attack order
+ this.FinishOrder();
},
"Order.Gather": function(msg) {
@@ -514,8 +527,8 @@ var UnitFsmSpec = {
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
- if (this.StanceSpecificQuery(this.stance))
- return true;
+ if (this.FindNewTargets())
+ return true; // (abort the FSM transition since we may have already switched state)
// Nobody to attack - stay in idle
return false;
@@ -572,11 +585,6 @@ var UnitFsmSpec = {
this.FinishOrder();
}
},
-
- "StanceChanged": function(msg) {
- if (this.StanceSpecificQuery(this.stance))
- return;
- },
},
"WALKING": {
@@ -634,12 +642,12 @@ var UnitFsmSpec = {
},
"Timer": function(msg) {
- if (!this.ChaseTargetedEntity(this.order.data.target, this.order.data.force))
+ if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
{
this.StopMoving();
this.FinishOrder();
- // we return to our position
+ // Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
@@ -691,7 +699,8 @@ var UnitFsmSpec = {
return;
}
- if (this.ChaseTargetedEntity(this.order.data.target))
+ // Can't reach it - try to chase after it
+ if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
{
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
{
@@ -706,16 +715,13 @@ var UnitFsmSpec = {
if (this.FinishOrder())
return;
- // see if we can switch to a new nearby enemy
- if (this.StanceSpecificQuery(this.stance, true))
+ // See if we can switch to a new nearby enemy
+ if (this.FindNewTargets())
return;
- // we return to our position
+ // Return to our original position
if (this.GetStance().respondHoldGround)
- {
- if (this.WalkToHeldPosition())
- return;
- }
+ this.WalkToHeldPosition();
},
// TODO: respond to target deaths immediately, rather than waiting
@@ -733,12 +739,12 @@ var UnitFsmSpec = {
},
"Timer": function(msg) {
- if (!this.ChaseTargetedEntity(this.order.data.target))
+ if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force))
{
this.StopMoving();
this.FinishOrder();
- // we return to our position
+ // Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
@@ -1365,7 +1371,7 @@ UnitAI.prototype.OnCreate = function()
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
- this.SetupRangeQuery(msg.to);
+ this.SetupRangeQuery();
};
UnitAI.prototype.OnDestroy = function()
@@ -1382,8 +1388,11 @@ UnitAI.prototype.OnDestroy = function()
// Set up a range query for all enemy units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
-UnitAI.prototype.SetupRangeQuery = function(owner)
+UnitAI.prototype.SetupRangeQuery = function()
{
+ var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ var owner = cmpOwnership.GetOwner();
+
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
@@ -1410,11 +1419,11 @@ UnitAI.prototype.SetupRangeQuery = function(owner)
}
}
- var range = this.StanceSpecificRange();
+ var range = this.GetQueryRange();
- this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, 0, range.max, players, IID_DamageReceiver);
+ this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver);
rangeMan.EnableActiveQuery(this.losRangeQuery);
-}
+};
//// FSM linkage functions ////
@@ -1861,12 +1870,18 @@ UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
UnitAI.prototype.MoveToTarget = function(target)
{
+ if (!this.CheckTargetVisible(target))
+ return false;
+
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
};
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
+ if (!this.CheckTargetVisible(target))
+ return false;
+
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = cmpRanged.GetRange(type);
@@ -1876,6 +1891,9 @@ UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
+ if (!this.CheckTargetVisible(target))
+ return false;
+
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
@@ -1889,6 +1907,26 @@ UnitAI.prototype.CheckTargetRange = function(target, iid, type)
return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};
+/**
+ * Returns true if the target entity is visible through the FoW/SoD.
+ */
+UnitAI.prototype.CheckTargetVisible = function(target)
+{
+ var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership)
+ return false;
+
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ if (!cmpRangeManager)
+ return false;
+
+ if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden")
+ return false;
+
+ // Either visible directly, or visible in fog
+ return true;
+};
+
UnitAI.prototype.FaceTowardsTarget = function(target)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
@@ -1942,7 +1980,7 @@ UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
var distance = DistanceBetweenEntities(this.entity,target);
return distance < range;
-}
+};
UnitAI.prototype.GetBestAttack = function()
{
@@ -1971,8 +2009,8 @@ UnitAI.prototype.AttackVisibleEntity = function(ents)
};
/**
- * Try to find one of the given entities which can be attacked,
- * within zone.
+ * 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)
@@ -2017,38 +2055,45 @@ UnitAI.prototype.RespondToTargetedEntities = function(ents)
return false;
};
-/*
- * Try to chase targeted entity given our current stance
+/**
+ * Returns true if we should stop following the target entity.
*/
-UnitAI.prototype.ChaseTargetedEntity = function(ent, force)
+UnitAI.prototype.ShouldAbandonChase = function(target, force)
{
- if (this.GetStance().respondChase)
- {
- if (this.CheckTargetIsInVisionRange(ent))
- {
- return true;
- }
- }
+ // Stop if we're in hold-ground mode and it's too far from the holding point
+ if (this.GetStance().respondHoldGround)
+ {
+ if (!this.CheckTargetDistanceFromHeldPosition(target, this.attackType))
+ return true;
+ }
- if (this.GetStance().respondChaseToHell || force)
- {
- //if ( TODO : Check ent is not in Shroud of Darkness)
- {
- return true;
- }
- }
+ // Stop if it's left our vision range, unless we're especially persistent
+ if (!force && !this.GetStance().respondChaseBeyondVision)
+ {
+ if (!this.CheckTargetIsInVisionRange(target))
+ return true;
+ }
- if (this.GetStance().respondHoldGround)
- {
- if (this.CheckTargetDistanceFromHeldPosition(ent, this.attackType))
- {
- return true;
- }
- }
+ // (Note that CCmpUnitMotion will detect if the target is lost in FoW,
+ // and will continue moving to its last seen position and then stop)
- return false;
+ return false;
+};
-}
+/*
+ * Returns whether we should chase the targeted entity,
+ * given our current stance.
+ */
+UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
+{
+ if (this.GetStance().respondChase)
+ return true;
+
+ if (force)
+ return true;
+
+ return false;
+};
//// External interface functions ////
@@ -2289,32 +2334,29 @@ UnitAI.prototype.SwitchToStance = function(stance)
if (stance == "stand" || stance == "defensive" || stance == "passive")
this.StopMoving();
- UnitFsm.ProcessMessage(this, {"type": "StanceChanged", "stance": stance});
+ // Reset the range query, since the range depends on stance
+ this.SetupRangeQuery();
}
-UnitAI.prototype.StanceSpecificQuery = function(stance, disable)
+/**
+ * Resets losRangeQuery, and if there are some targets in range that we can
+ * attack then we start attacking and this returns true; otherwise, returns false.
+ */
+UnitAI.prototype.FindNewTargets = function()
{
- if (!g_Stances[stance])
- {
- error("UnitAI: Setting to invalid stance '"+stance+"'");
- return false;
- }
-
if (!this.losRangeQuery)
return false;
- var range = this.StanceSpecificRange();
-
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
- var ents = rangeMan.ModifyRangeActiveQuery(this.losRangeQuery, range.min, range.max);
+ var ents = rangeMan.ResetActiveQuery(this.losRangeQuery);
- if (disable && !this.IsAnimal())
- rangeMan.DisableActiveQuery(this.losRangeQuery);
+ if (!this.GetStance().targetVisibleEnemies)
+ return false;
return this.RespondToTargetedEntities(ents);
};
-UnitAI.prototype.StanceSpecificRange = function()
+UnitAI.prototype.GetQueryRange = function()
{
var ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp
index 7146cc7312..01e8573ff8 100644
--- a/source/simulation2/components/CCmpRangeManager.cpp
+++ b/source/simulation2/components/CCmpRangeManager.cpp
@@ -459,23 +459,6 @@ public:
return (tag_t)id;
}
- virtual std::vector ModifyRangeActiveQuery(tag_t tag,
- entity_pos_t minRange, entity_pos_t maxRange)
- {
- std::map::iterator it = m_Queries.find(tag);
- if (it == m_Queries.end())
- {
- LOGERROR(L"CCmpRangeManager: ModifyRangeActiveQuery called with invalid tag %d", tag);
- std::vector r;
- return r;
- }
-
- Query& q = it->second;
- q.minRange = minRange;
- q.maxRange = maxRange;
- return ResetActiveQuery(tag);
- }
-
virtual void DestroyActiveQuery(tag_t tag)
{
if (m_Queries.find(tag) == m_Queries.end())
diff --git a/source/simulation2/components/CCmpUnitMotion.cpp b/source/simulation2/components/CCmpUnitMotion.cpp
index f1b82a6d9f..059fa65e26 100644
--- a/source/simulation2/components/CCmpUnitMotion.cpp
+++ b/source/simulation2/components/CCmpUnitMotion.cpp
@@ -22,8 +22,10 @@
#include "ICmpObstruction.h"
#include "ICmpObstructionManager.h"
+#include "ICmpOwnership.h"
#include "ICmpPosition.h"
#include "ICmpPathfinder.h"
+#include "ICmpRangeManager.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
@@ -1003,6 +1005,20 @@ bool CCmpUnitMotion::CheckTargetMovement(CFixedVector2D from, entity_pos_t minDe
if (!PathIsShort(m_LongPath, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST))
return false;
+ // Fail if the target is no longer visible to this entity's owner
+ // (in which case we'll continue moving to its last known location,
+ // unless it comes back into view before we reach that location)
+ CmpPtr cmpOwnership(GetSimContext(), GetEntityId());
+ if (!cmpOwnership.null())
+ {
+ CmpPtr cmpRangeManager(GetSimContext(), SYSTEM_ENTITY);
+ if (!cmpRangeManager.null())
+ {
+ if (cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN)
+ return false;
+ }
+ }
+
// The target moved and we need to update our current path;
// change the goal here and expect our caller to start the path request
m_FinalGoal.x = targetPos.X;
diff --git a/source/simulation2/components/ICmpRangeManager.cpp b/source/simulation2/components/ICmpRangeManager.cpp
index ee1992b1b1..a0dbc9801a 100644
--- a/source/simulation2/components/ICmpRangeManager.cpp
+++ b/source/simulation2/components/ICmpRangeManager.cpp
@@ -36,7 +36,6 @@ std::string ICmpRangeManager::GetLosVisibility_wrapper(entity_id_t ent, int play
BEGIN_INTERFACE_WRAPPER(RangeManager)
DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int)
DEFINE_INTERFACE_METHOD_5("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int)
-DEFINE_INTERFACE_METHOD_3("ModifyRangeActiveQuery", std::vector, ICmpRangeManager, ModifyRangeActiveQuery, ICmpRangeManager::tag_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t)
diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h
index fd47807ca7..a5e317e29b 100644
--- a/source/simulation2/components/ICmpRangeManager.h
+++ b/source/simulation2/components/ICmpRangeManager.h
@@ -101,16 +101,6 @@ public:
virtual tag_t CreateActiveQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange, std::vector owners, int requiredInterface) = 0;
- /**
- * Modify an active query's range parameters and execute a ResetActiveQuery.
- * @param tag identifier of query.
- * @param minRange non-negative minimum distance in metres (inclusive).
- * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance.
- * @return list of entities matching the query, ordered by increasing distance from the source entity.
- */
- virtual std::vector ModifyRangeActiveQuery(tag_t tag,
- entity_pos_t minRange, entity_pos_t maxRange) = 0;
-
/**
* Destroy a query and clean up resources. This must be called when an entity no longer needs its
* query (e.g. when the entity is destroyed).