Index: binaries/data/mods/public/maps/scenarios/unit_dancing_test_triggers.js =================================================================== --- binaries/data/mods/public/maps/scenarios/unit_dancing_test_triggers.js +++ binaries/data/mods/public/maps/scenarios/unit_dancing_test_triggers.js @@ -39,6 +39,19 @@ return ent; }; +var FormationWalkTo = function(x, z, queued, ent, owner=1) +{ + ProcessCommand(owner, { + "type": "walk", + "entities": Array.isArray(ent) ? ent : [ent], + "x": x, + "z": z, + "queued": queued, + "force": true, + "formation": "special/formations/box" + }); + return ent; +}; var Patrol = function(x, z, queued, ent, owner=1) { @@ -82,7 +95,7 @@ for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); - return [dancer, attackers]; + return [[dancer], attackers]; }; }; @@ -102,7 +115,7 @@ for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); - return [dancer, attackers]; + return [[dancer], attackers]; }; }; @@ -130,7 +143,7 @@ for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); - return [dancer, attackers]; + return [[dancer], attackers]; }; }; @@ -144,10 +157,29 @@ for (let i = 0; i < n_attackers; ++i) attackers.push(Attack(dancer, WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); - return [dancer, attackers]; + return [[dancer], attackers]; }; }; +var manual_formation_dance = function(attacker, target, dance_distance, att_distance = 50, n_attackers = 1) +{ + return () => { + let dancers = []; + for (let x = 0; x < 4; x++) + for (let z = 0; z < 4; z++) + dancers.push(QuickSpawn(gx+x, gy+z, target)); + for (let i = 0; i < 100; ++i) + FormationWalkTo(gx, gy + dance_distance * (i % 2), i != 0, dancers); + + let attackers = []; + for (let i = 0; i < n_attackers; ++i) + attackers.push(Attack(dancers[0], WalkTo(gx + att_distance, gy + i * 2, true, QuickSpawn(gx + att_distance + i, gy, attacker, ATTACKER), ATTACKER))); + + return [dancers, attackers.concat(dancers)]; + }; +}; + + experiments.unit_manual_dance_archer = { "spawn": manual_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 5) }; @@ -232,6 +264,22 @@ "spawn": manual_zigzag_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 3, 50, 5) }; +experiments.formation_dance_slow_archer = { + "spawn": manual_formation_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 25, 50, 5) +}; + +experiments.formation_dance_fast_archer = { + "spawn": manual_formation_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 25, 50, 5) +}; + +experiments.formation_bad_dance_slow_archer = { + "spawn": manual_formation_dance(ARCHER_TEMPLATE, REG_UNIT_TEMPLATE, 50, 50, 5) +}; + +experiments.formation_bad_dance_fast_archer = { + "spawn": manual_formation_dance(ARCHER_TEMPLATE, FAST_UNIT_TEMPLATE, 50, 50, 5) +}; + var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); Trigger.prototype.SetupUnits = function() @@ -242,16 +290,15 @@ gy = 100; for (let key in experiments) { - let [dancer, attackers] = experiments[key].spawn(); + let [dancers, attackers] = experiments[key].spawn(); let ReportResults = (killed) => { warn((killed ? "Success " : "Failure ") + "(" + (Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() - start) + "): Experiment " + key + " finished: target was " + (killed ? "killed" : "not killed")); - if (!killed) - ProcessCommand(1, { - "type": "delete-entities", - "entities": [dancer], - "controlAllUnits": true - }); + ProcessCommand(1, { + "type": "delete-entities", + "entities": dancers, + "controlAllUnits": true + }); ProcessCommand(2, { "type": "delete-entities", "entities": attackers, @@ -259,7 +306,7 @@ }); }; // xxtreme hack: hook into UnitAI - let uai = Engine.QueryInterface(dancer, IID_UnitAI); + let uai = Engine.QueryInterface(dancers[0], IID_UnitAI); let odes = uai.OnDestroy; uai.OnDestroy = () => ReportResults(true) && odes(); uai.FindNewTargets = () => { @@ -268,7 +315,7 @@ }; gx += 70; - if (gx >= 450) + if (gx >= 520) { gx = 100; gy += 70; Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -515,6 +515,9 @@ let gravity = +this.template[type].Projectile.Gravity; // horizSpeed /= 2; gravity /= 2; // slow it down for testing + // We will try to estimate the position of the target, where we can hit it. + // We first estimate the time-till-hit by extrapolating linearly the movement + // of the last turn. We compute the time till an arrow will intersect the target. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; @@ -524,11 +527,36 @@ return; let targetPosition = cmpTargetPosition.GetPosition(); - let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); - let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); + let targetVelocity = Vector3D.sub(targetPosition, cmpTargetPosition.GetPreviousPosition()).div(turnLength); let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); - let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition; + + // 'Cheat' and use UnitMotion to predict the position in the near-future. + // This avoids 'dancing' issues with units zigzagging over very short distances. + // However, this could fail if the player gives several short move orders, so + // occasionally fall back to basic interpolation. + let predictedPosition = targetPosition; + if (timeToTarget !== false) + { + // Don't predict too far in the future, but avoid threshold effects. + // After 1 second, always use the 'dumb' interpolated past-motion prediction. + let useUnitMotion = randBool(Math.max(0, 0.75 - timeToTarget / 1.333)); + if (useUnitMotion) + { + let cmpTargetUnitMotion = Engine.QueryInterface(target, IID_UnitMotion); + let cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); + if (cmpTargetUnitMotion && (!cmpTargetUnitAI || !cmpTargetUnitAI.IsFormationMember())) + { + let pos2D = cmpTargetUnitMotion.EstimateFuturePosition(timeToTarget); + predictedPosition.x = pos2D.x; + predictedPosition.z = pos2D.y; + } + else + predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); + } + else + predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); + } // Add inaccuracy based on spread. let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) * Index: binaries/data/mods/public/simulation/components/UnitMotionFlying.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitMotionFlying.js +++ binaries/data/mods/public/simulation/components/UnitMotionFlying.js @@ -311,6 +311,20 @@ return 1; }; +/** + * Estimate the next position of the unit. Just linearly extrapolate. + * TODO: Reuse the movement code for a better estimate. + */ +UnitMotionFlying.prototype.EstimateNextPosition = function(dt) +{ + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return Vector2D(); + let position = cmpPosition.GetPosition2D(); + + return Vector2D.add(position, Vector2D.sub(position, cmpPosition.GetPreviousPosition2D()).mult(dt/Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetLatestTurnLength())); +}; + UnitMotionFlying.prototype.IsMoveRequested = function() { return this.hasTarget; Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -22,6 +22,7 @@ Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Attack.js"); Engine.LoadComponentScript("DelayedDamage.js"); Engine.LoadComponentScript("Timer.js"); Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -406,6 +406,25 @@ return m_RunMultiplier; } + virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const + { + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return CFixedVector2D(); + + // TODO: formation members should perhaps try to use the controller's position. + + CFixedVector2D pos = cmpPosition->GetPosition2D(); + entity_angle_t angle = cmpPosition->GetRotation().Y; + + // Copy the path so we don't change it. + WaypointPath shortPath = m_ShortPath; + WaypointPath longPath = m_LongPath; + + PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, angle); + return pos; + } + virtual pass_class_t GetPassabilityClass() const { return m_PassClass; Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -116,6 +116,12 @@ */ virtual fixed GetSpeed() const = 0; + /** + * @return the estimated position of the unit in @param dt seconds, + * following current paths. This is allowed to 'look into the future'. + */ + virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const = 0; + /** * Set whether the unit will turn to face the target point after finishing moving. */ Index: source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- source/simulation2/components/ICmpUnitMotion.cpp +++ source/simulation2/components/ICmpUnitMotion.cpp @@ -34,6 +34,7 @@ DEFINE_INTERFACE_METHOD_CONST_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed) DEFINE_INTERFACE_METHOD_CONST_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) DEFINE_INTERFACE_METHOD_CONST_0("GetRunMultiplier", fixed, ICmpUnitMotion, GetRunMultiplier) +DEFINE_INTERFACE_METHOD_CONST_1("EstimateFuturePosition", CFixedVector2D, ICmpUnitMotion, EstimateFuturePosition, fixed) DEFINE_INTERFACE_METHOD_1("SetSpeedMultiplier", void, ICmpUnitMotion, SetSpeedMultiplier, fixed) DEFINE_INTERFACE_METHOD_CONST_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName) DEFINE_INTERFACE_METHOD_CONST_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance) @@ -112,6 +113,11 @@ return m_Script.Call("GetSpeedMultiplier"); } + virtual CFixedVector2D EstimateFuturePosition(fixed dt) const + { + return m_Script.Call("EstimateFuturePosition", dt); + } + virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove);