Index: ps/trunk/binaries/data/mods/public/maps/scenarios/unit_chasing_test.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scenarios/unit_chasing_test.xml
+++ ps/trunk/binaries/data/mods/public/maps/scenarios/unit_chasing_test.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+ default
+
+
+
+
+
+ 0
+ 0.5
+
+
+
+
+ ocean
+
+
+ 5
+ 4
+ 0.45
+ 0
+
+
+
+ 0
+ 1
+ 0.99
+ 0.1999
+ default
+
+
+
+
+
+
+
+
+
+
+
+
Index: ps/trunk/binaries/data/mods/public/maps/scenarios/unit_chasing_test_triggers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scenarios/unit_chasing_test_triggers.js
+++ ps/trunk/binaries/data/mods/public/maps/scenarios/unit_chasing_test_triggers.js
@@ -0,0 +1,277 @@
+const ARCHER_TEMPLATE = "units/maur/infantry_archer_b";
+const JAV_TEMPLATE = "units/mace/infantry_javelineer_b";
+
+const REG_UNIT_TEMPLATE = "units/athen/infantry_spearman_b";
+const FAST_UNIT_TEMPLATE = "units/athen/cavalry_swordsman_b";
+const FAST_UNIT_TEMPLATE_2 = "units/athen/cavalry_javelineer_b";
+
+const ATTACKER = 2;
+
+var QuickSpawn = function(x, z, template, owner = 1)
+{
+ let ent = Engine.AddEntity(template);
+
+ let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(owner);
+
+ let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpEntPosition.JumpTo(x, z);
+ return ent;
+};
+
+var WalkTo = function(x, z, queued, ent, owner=1)
+{
+ ProcessCommand(owner, {
+ "type": "walk",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "x": x,
+ "z": z,
+ "queued": queued,
+ "force": false,
+ });
+ return ent;
+};
+
+var Attack = function(target, ent)
+{
+ let comm = {
+ "type": "attack",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "target": target,
+ "queued": true,
+ "force": true,
+ };
+ ProcessCommand(ATTACKER, comm);
+ return ent;
+};
+
+var Garrison = function(target, ent)
+{
+ let comm = {
+ "type": "garrison",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "target": target,
+ "queued": true,
+ "force": true,
+ };
+ ProcessCommand(1, comm);
+ return ent;
+};
+
+var gx;
+var gy;
+
+var straight_line = function(attacker_first, attacker, target, walk = true)
+{
+ return () => {
+ let chaser;
+ let chasee;
+ if (attacker_first)
+ {
+ chaser = QuickSpawn(gx, 80, attacker, ATTACKER);
+ chasee = QuickSpawn(gx, 80+40, target);
+ }
+ else
+ {
+ chasee = QuickSpawn(gx, 80+40, target);
+ chaser = QuickSpawn(gx, 80, attacker, ATTACKER);
+ }
+ if (walk)
+ WalkTo(gx, 900, true, chasee);
+ Attack(chasee, chaser, ATTACKER);
+ return [chaser, chasee];
+ };
+};
+
+var straight_line_garrison = function(garrison_first, attacker, target)
+{
+ return () => {
+ let chaser;
+ let chasee;
+ if (garrison_first)
+ {
+ chaser = QuickSpawn(gx, gy, attacker);
+ chasee = QuickSpawn(gx, gy+50, target);
+ }
+ else
+ {
+ chasee = QuickSpawn(gx, gy+50, target);
+ chaser = QuickSpawn(gx, gy, attacker);
+ }
+ WalkTo(gx, 900, true, chasee);
+ Garrison(chasee, chaser);
+ return [chaser, chasee];
+ };
+};
+
+var experiments = {};
+
+experiments.fast_on_fast = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE, FAST_UNIT_TEMPLATE_2)
+};
+
+experiments.fast_on_fast_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE, FAST_UNIT_TEMPLATE_2)
+};
+
+experiments.fast_on_slow = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE, ARCHER_TEMPLATE)
+};
+
+experiments.fast_on_slow_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE, ARCHER_TEMPLATE)
+};
+
+experiments.slow_on_slow = {
+ "spawn": straight_line(false, "units/athen/infantry_spearman_b", "units/mace/infantry_pikeman_b")
+};
+
+experiments.slow_on_slow_2 = {
+ "spawn": straight_line(true, "units/athen/infantry_spearman_b", "units/mace/infantry_pikeman_b")
+};
+
+experiments.fast_on_trader = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE, "units/mace/support_trader")
+};
+
+experiments.fast_on_trader_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE, "units/mace/support_trader")
+};
+
+// Traders are passive, let them flee
+experiments.fast_on_trader_flee = {
+ "spawn": straight_line(false, JAV_TEMPLATE, "units/mace/support_trader", false)
+};
+
+experiments.fast_on_trader_flee_2 = {
+ "spawn": straight_line(true, JAV_TEMPLATE, "units/mace/support_trader", false)
+};
+
+experiments.slow_on_trader = {
+ "spawn": straight_line(false, "units/athen/infantry_spearman_b", "units/mace/support_trader", false)
+};
+
+experiments.slow_on_trader_2 = {
+ "spawn": straight_line(true, "units/athen/infantry_spearman_b", "units/mace/support_trader", false)
+};
+
+experiments.fast_on_muskox = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE, "gaia/fauna_muskox")
+};
+
+experiments.fast_on_muskox_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE, "gaia/fauna_muskox")
+};
+
+// Women flee
+experiments.fast_on_women_flee = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE, "units/mace/support_female_citizen", false)
+};
+
+experiments.fast_on_women_flee_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE, "units/mace/support_female_citizen", false)
+};
+
+// Women flee
+experiments.slow_on_women_flee = {
+ "spawn": straight_line(false, "units/athen/infantry_spearman_b", "units/mace/support_female_citizen", false)
+};
+
+experiments.slow_on_women_flee_2 = {
+ "spawn": straight_line(true, "units/athen/infantry_spearman_b", "units/mace/support_female_citizen", false)
+};
+
+experiments.straight_line_garrison = {
+ "spawn": straight_line_garrison(false, "units/athen/infantry_spearman_b", "units/mace/siege_ram")
+};
+
+experiments.straight_line_garrison_2 = {
+ "spawn": straight_line_garrison(true, "units/athen/infantry_spearman_b", "units/mace/siege_ram")
+};
+
+experiments.archer_on_spearman = {
+ "spawn": straight_line(false, ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, true)
+};
+
+experiments.archer_on_spearman_2 = {
+ "spawn": straight_line(true, ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, true)
+};
+
+experiments.jav_on_spearman = {
+ "spawn": straight_line(false, JAV_TEMPLATE, REG_UNIT_TEMPLATE, true)
+};
+
+experiments.jav_on_spearman_2 = {
+ "spawn": straight_line(true, JAV_TEMPLATE, REG_UNIT_TEMPLATE, true)
+};
+
+experiments.fast_archer_on_spearman = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE_2, REG_UNIT_TEMPLATE, true)
+};
+
+experiments.fast_archer_on_spearman_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE_2, REG_UNIT_TEMPLATE, true)
+};
+
+experiments.fast_on_semi = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE_2, ARCHER_TEMPLATE, true)
+};
+
+experiments.fast_on_semi_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE_2, ARCHER_TEMPLATE, true)
+};
+
+experiments.fast_on_flee = {
+ "spawn": straight_line(false, FAST_UNIT_TEMPLATE_2, "units/mace/support_female_citizen", false)
+};
+
+experiments.fast_on_flee_2 = {
+ "spawn": straight_line(true, FAST_UNIT_TEMPLATE_2, "units/mace/support_female_citizen", false)
+};
+
+
+var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+
+var onDelete = {};
+
+Trigger.prototype.SetupUnits = function()
+{
+ gx = 130;
+ gy = 150;
+ for (let key in experiments)
+ {
+ let ents = experiments[key].spawn();
+ onDelete[ents[0]] = ents;
+ onDelete[ents[1]] = ents;
+ gx += 15;
+ if (gx >= 620)
+ {
+ gx = 120;
+ gy += 70;
+ }
+ }
+};
+
+Trigger.prototype.OnOwnershipChanged = function(msg)
+{
+ if (msg.to === -1 && msg.entity in onDelete)
+ {
+ Engine.DestroyEntity(onDelete[msg.entity][0]);
+ Engine.DestroyEntity(onDelete[msg.entity][1]);
+ }
+};
+
+cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true });
+
+var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
+
+// Prevent promotions, messes up things.
+cmpModifiersManager.AddModifiers("no_promotion_A", {
+ "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }],
+}, 3);
+cmpModifiersManager.AddModifiers("no_promotion_B", {
+ "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }],
+}, 4); // player 2 is ent 4
+
+cmpTrigger.DoAfterDelay(3000, "SetupUnits", {});
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion.h
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion.h
@@ -716,7 +716,7 @@
* Attempts to replace the current path with a straight line to the target,
* if it's close enough and the route is not obstructed.
*/
- bool TryGoingStraightToTarget(const CFixedVector2D& from);
+ bool TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths);
/**
* Returns whether our we need to recompute a path to reach our target.
@@ -955,7 +955,7 @@
// If we're chasing a potentially-moving unit and are currently close
// enough to its current position, and we can head in a straight line
// to it, then throw away our current path and go straight to it.
- state.wentStraight = TryGoingStraightToTarget(state.initialPos);
+ state.wentStraight = TryGoingStraightToTarget(state.initialPos, true);
state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.angle);
}
@@ -1274,50 +1274,28 @@
else
{
out = cmpTargetPosition->GetPosition2D();
- // Because units move one-at-a-time and pathing is asynchronous, we need to account for target movement,
- // if we are computing this during the MT_Motion* part of the turn.
- // If our entity ID is lower, we move first, and so we need to add a predicted movement to compute a path for next turn.
- // If our entity ID is higher, the target has already moved, so we can just use the position directly.
+ // Position is only updated after all units have moved & pushed.
+ // Therefore, we may need to interpolate the target position, depending on when this call takes place during the turn:
+ // - On "Turn Start", we'll check positions directly without interpolation.
+ // - During movement, we'll call this for direct-pathing & we need to interpolate
+ // (this way, we move where the unit will end up at the end of _this_ turn, making it match on next turn start).
+ // - After movement, we'll call this to request paths & we need to interpolate
+ // (this way, we'll move where the unit ends up in the end of _next_ turn, making it a match in 2 turns).
// TODO: This does not really aim many turns in advance, with orthogonal trajectories it probably should.
CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity);
CmpPtr cmpUnitMotionManager(GetSystemEntity());
bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion();
- if (needInterpolation && GetEntityId() < moveRequest.m_Entity)
+ if (needInterpolation)
{
// Add predicted movement.
CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D());
-
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return true; // Still return true since we don't need a position for the target to have one.
-
- // Fleeing fix: if we anticipate the target to go through us, we'll suddenly turn around, which is bad.
- // Pretend that the target is still behind us in those cases.
- if (m_MoveRequest.m_MinRange > fixed::Zero())
- {
- if ((out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) >= 0)
- out = tempPos;
- }
- else
- out = tempPos;
- }
- else if (needInterpolation && GetEntityId() > moveRequest.m_Entity)
- {
- CmpPtr cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return true; // Still return true since we don't need a position for the target to have one.
-
- // Fleeing fix: opposite to above, check if our target has travelled through us already this turn.
- CFixedVector2D tempPos = out - (out - cmpTargetPosition->GetPreviousPosition2D());
- if (m_MoveRequest.m_MinRange > fixed::Zero() &&
- (out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) < 0)
- out = tempPos;
+ out = tempPos;
}
}
return true;
}
-bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from)
+bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths)
{
// Assume if we have short paths we want to follow them.
// Exception: offset movement (formations) generally have very short deltas
@@ -1368,6 +1346,9 @@
else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
return false;
+ if (!updatePaths)
+ return true;
+
// That route is okay, so update our path
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
@@ -1600,10 +1581,17 @@
}
#endif
- // If the target is close and we can reach it in a straight line,
- // then we'll just go along the straight line instead of computing a path.
- if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from))
+ // If the target is close enough, hope that we'll be able to go straight next turn.
+ if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from, false))
+ {
+ // NB: since we may fail to move straight next turn, we should edge our bets.
+ // Since the 'go straight' logic currently fires only if there's no short path,
+ // we'll compute a long path regardless to make sure _that_ stays up to date.
+ // (it's also extremely likely to be very fast to compute, so no big deal).
+ m_ShortPath.m_Waypoints.clear();
+ RequestLongPath(from, goal);
return;
+ }
// Otherwise we need to compute a path.