diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index a35a1316cb..6b444b4bbc 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -3,9 +3,19 @@ function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player." +
"" +
+ "" +
+ "" +
+ "aggressive" +
+ "holdfire" +
+ "noncombat" +
+ "" +
+ "" +
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
@@ -20,9 +30,6 @@ UnitAI.prototype.Schema =
"" +
"" +
"" +
- "" +
- "" +
- "" +
"" +
"" +
"" +
@@ -39,13 +46,36 @@ UnitAI.prototype.Schema =
"";
// Very basic stance support (currently just for test maps where we don't want
-// everyone killing each other immediately after loading)
+// everyone killing each other immediately after loading, and for female citizens)
+// 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).
var g_Stances = {
"aggressive": {
- attackOnSight: true,
+ targetVisibleEnemies: true,
+ targetAttackers: true,
+ respondFlee: false,
+ respondChase: true,
},
"holdfire": {
- attackOnSight: false,
+ targetVisibleEnemies: false,
+ targetAttackers: true,
+ respondFlee: false,
+ respondChase: true,
+ },
+ "noncombat": {
+ targetVisibleEnemies: false,
+ targetAttackers: true,
+ respondFlee: true,
+ respondChase: false,
},
};
@@ -145,6 +175,21 @@ var UnitFsmSpec = {
}
},
+ "Order.Flee": function(msg) {
+ // TODO: if we were attacked by a ranged unit, we need to flee much further away
+ var ok = this.MoveToTargetRangeExplicit(this.order.data.target, +this.template.FleeDistance, -1);
+ if (ok)
+ {
+ // We've started fleeing from the given target
+ this.SetNextState("INDIVIDUAL.FLEEING");
+ }
+ else
+ {
+ // We are already at the target, or can't move at all
+ this.FinishOrder();
+ }
+ },
+
"Order.Attack": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
@@ -368,14 +413,9 @@ var UnitFsmSpec = {
},
"Attacked": function(msg) {
- // Default behaviour: attack back at our attacker
- if (this.CanAttack(msg.data.attacker))
+ if (this.GetStance().targetAttackers)
{
- this.PushOrderFront("Attack", { "target": msg.data.attacker });
- }
- else
- {
- // TODO: If unit can't attack, run away
+ this.RespondToTargetedEntities([msg.data.attacker]);
}
},
@@ -399,8 +439,11 @@ var UnitFsmSpec = {
{
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = rangeMan.ResetActiveQuery(this.losRangeQuery);
- if (this.GetStance().attackOnSight && this.AttackVisibleEntity(ents))
- return true; // (abort the transition since we may have already switched state)
+ if (this.GetStance().targetVisibleEnemies)
+ {
+ if (this.RespondToTargetedEntities(ents))
+ return true; // (abort the FSM transition since we may have already switched state)
+ }
}
// Nobody to attack - stay in idle
@@ -421,10 +464,10 @@ var UnitFsmSpec = {
},
"LosRangeUpdate": function(msg) {
- if (this.GetStance().attackOnSight)
+ if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
- this.AttackVisibleEntity(msg.data.added);
+ this.RespondToTargetedEntities(msg.data.added);
}
},
@@ -466,6 +509,29 @@ var UnitFsmSpec = {
},
},
+ "FLEEING": {
+ "enter": function() {
+ this.PlaySound("panic");
+
+ // Run quickly
+ var speed = this.GetRunSpeed();
+ this.SelectAnimation("move");
+ this.SetMoveSpeed(speed);
+ },
+
+ "leave": function() {
+ // Reset normal speed
+ this.SetMoveSpeed(this.GetWalkSpeed());
+ },
+
+ "MoveCompleted": function() {
+ // When we've run far enough, stop fleeing
+ this.FinishOrder();
+ },
+
+ // TODO: what if we run into more enemies while fleeing?
+ },
+
"COMBAT": {
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else
@@ -924,7 +990,6 @@ var UnitFsmSpec = {
{
this.MoveToTargetRangeExplicit(msg.data.attacker, +this.template.FleeDistance, +this.template.FleeDistance);
this.SetNextState("FLEEING");
- this.PlaySound("panic");
}
else if (this.template.NaturalBehaviour == "violent" ||
this.template.NaturalBehaviour == "aggressive" ||
@@ -939,7 +1004,6 @@ var UnitFsmSpec = {
// Run away from the foundation
this.MoveToTargetRangeExplicit(msg.data.target, +this.template.FleeDistance, +this.template.FleeDistance);
this.SetNextState("FLEEING");
- this.PlaySound("panic");
},
"IDLE": {
@@ -996,7 +1060,6 @@ var UnitFsmSpec = {
{
this.MoveToTargetRangeExplicit(msg.data.added[0], +this.template.FleeDistance, +this.template.FleeDistance);
this.SetNextState("FLEEING");
- this.PlaySound("panic");
return;
}
}
@@ -1042,7 +1105,6 @@ var UnitFsmSpec = {
{
this.MoveToTargetRangeExplicit(msg.data.added[0], +this.template.FleeDistance, +this.template.FleeDistance);
this.SetNextState("FLEEING");
- this.PlaySound("panic");
return;
}
}
@@ -1060,8 +1122,10 @@ var UnitFsmSpec = {
},
},
- "FLEEING": {
+ "FLEEING": { // TODO: would be nice to share more of this with non-animal units
"enter": function() {
+ this.PlaySound("panic");
+
// Run quickly
var speed = this.GetRunSpeed();
this.SelectAnimation("run", false, speed);
@@ -1092,7 +1156,7 @@ UnitAI.prototype.Init = function()
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isIdle = false;
- this.SetStance("aggressive");
+ this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsFormationController = function()
@@ -1572,6 +1636,28 @@ UnitAI.prototype.AttackVisibleEntity = function(ents)
return false;
};
+/**
+ * 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().respondFlee)
+ {
+ this.PushOrderFront("Flee", { "target": ents[0] });
+ return true;
+ }
+
+ return false;
+};
+
//// External interface functions ////
UnitAI.prototype.SetFormationController = function(ent)
@@ -1633,6 +1719,7 @@ UnitAI.prototype.ComputeWalkingDistance = function()
break; // and continue the loop
case "WalkToTarget":
+ case "Flee":
case "LeaveFoundation":
case "Attack":
case "Gather":
diff --git a/binaries/data/mods/public/simulation/templates/special/formation.xml b/binaries/data/mods/public/simulation/templates/special/formation.xml
index 835a88e523..98434a62eb 100644
--- a/binaries/data/mods/public/simulation/templates/special/formation.xml
+++ b/binaries/data/mods/public/simulation/templates/special/formation.xml
@@ -7,6 +7,8 @@
+ aggressive
+ 12.0
true
diff --git a/binaries/data/mods/public/simulation/templates/template_unit.xml b/binaries/data/mods/public/simulation/templates/template_unit.xml
index 2cd8ca9249..9eb3ff530d 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit.xml
@@ -8,6 +8,8 @@
unit
+ aggressive
+ 12.0
false
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_support.xml b/binaries/data/mods/public/simulation/templates/template_unit_support.xml
index 92d698c6ce..e84411346e 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_support.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_support.xml
@@ -7,6 +7,9 @@
support
+
+ noncombat
+
15
diff --git a/source/simulation2/components/CCmpUnitMotion.cpp b/source/simulation2/components/CCmpUnitMotion.cpp
index fa8b41e6c3..ee6cb4bcaa 100644
--- a/source/simulation2/components/CCmpUnitMotion.cpp
+++ b/source/simulation2/components/CCmpUnitMotion.cpp
@@ -1213,7 +1213,7 @@ bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos
{
goalDistance = minRange + g_GoalDelta;
}
- else if (distance > maxRange)
+ else if (maxRange >= entity_pos_t::Zero() && distance > maxRange)
{
goalDistance = maxRange - g_GoalDelta;
}
@@ -1328,7 +1328,7 @@ bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange
goal.hw = obstruction.hw + delta;
goal.hh = obstruction.hh + delta;
}
- else if (distance < maxRange)
+ else if (maxRange < entity_pos_t::Zero() || distance < maxRange)
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, goal.x, goal.z);
@@ -1436,7 +1436,7 @@ bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange,
return false;
// See if we're close enough to the target square
- if (distance <= maxRange)
+ if (maxRange < entity_pos_t::Zero() || distance <= maxRange)
return true;
entity_pos_t circleRadius = halfSize.Length();
@@ -1464,7 +1464,7 @@ bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange,
entity_pos_t distance = (pos - targetPos).Length();
- if (minRange <= distance && distance <= maxRange)
+ if (minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange))
return true;
return false;
diff --git a/source/simulation2/components/ICmpUnitMotion.h b/source/simulation2/components/ICmpUnitMotion.h
index 5649f47418..ce86efb91c 100644
--- a/source/simulation2/components/ICmpUnitMotion.h
+++ b/source/simulation2/components/ICmpUnitMotion.h
@@ -41,6 +41,7 @@ public:
* some other error, then returns false.
* Otherwise, returns true and sends a MotionChanged message after starting to move,
* and sends another MotionChanged after finishing moving.
+ * If maxRange is negative, then the maximum range is treated as infinity.
*/
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
@@ -56,6 +57,7 @@ public:
* some other error, then returns false.
* Otherwise, returns true and sends a MotionChanged message after starting to move,
* and sends another MotionChanged after finishing moving.
+ * If maxRange is negative, then the maximum range is treated as infinity.
*/
virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
diff --git a/source/tools/entity/Entity.pm b/source/tools/entity/Entity.pm
index 50d14dc379..5f081ddc38 100644
--- a/source/tools/entity/Entity.pm
+++ b/source/tools/entity/Entity.pm
@@ -113,7 +113,6 @@ sub find_entities
return $File::Find::prune = 1 if $_ eq '.svn';
my $n = $File::Find::name;
return if /~$/;
- return if $n =~ /\/special\//;
return unless -f $_;
$n =~ s~\Q$vfsroot\E/(public|internal)/simulation/templates/~~;
$n =~ s/\.xml$//;