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$//;