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,24 @@ 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; + + // With the given time, we either will compute the target's position by still + // extrapolating on the previous turn or by asking the unitMotion where the + // target will be after this time at the current path. The former case is + // required since the path can stop midway, i.e., a player can do many short + // orders in the same direction. The latter is required to account for stopping + // targets, or targets on a zig-zag path. + let predictedPosition = targetPosition; + if (timeToTarget !== false) + { + let cmpTargetUnitMotion = Engine.QueryInterface(target, IID_UnitMotion); + predictedPosition = cmpTargetUnitMotion && randBool() ? + cmpTargetUnitMotion.EstimateNextPosition(timeToTarget) : + 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 @@ -292,6 +292,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 Vector3D(); + let position = cmpPosition.GetPosition(); + + return Vector3D.add(position, Vector3D.sub(position, cmpPosition.GetPreviousPosition()).mult(dt/Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetLatestTurnLength())); +}; + UnitMotionFlying.prototype.IsMoveRequested = function() { return this.hasTarget; Index: source/simulation2/components/CCmpPosition.cpp =================================================================== --- source/simulation2/components/CCmpPosition.cpp +++ source/simulation2/components/CCmpPosition.cpp @@ -433,12 +433,12 @@ virtual void SetHeightFixed(entity_pos_t y) { // subtract the absolute height and replace it with a new absolute height - m_LastYDifference = y - GetHeightFixed(); + m_LastYDifference = y - GetHeightFixed(m_X, m_Z); m_Y += m_LastYDifference; AdvertiseInterpolatedPositionChanges(); } - virtual entity_pos_t GetHeightFixed() const + virtual entity_pos_t GetHeightFixed(entity_pos_t x, entity_pos_t z) const { if (!m_RelativeToGround) return m_Y; @@ -447,13 +447,13 @@ entity_pos_t baseY; CmpPtr cmpTerrain(GetSystemEntity()); if (cmpTerrain) - baseY = cmpTerrain->GetGroundLevel(m_X, m_Z); + baseY = cmpTerrain->GetGroundLevel(x, z); if (m_Floating) { CmpPtr cmpWaterManager(GetSystemEntity()); if (cmpWaterManager) - baseY = std::max(baseY, cmpWaterManager->GetWaterLevel(m_X, m_Z) - m_FloatDepth); + baseY = std::max(baseY, cmpWaterManager->GetWaterLevel(x, z) - m_FloatDepth); } return m_Y + baseY; } @@ -466,7 +466,7 @@ virtual void SetHeightRelative(bool relative) { // move y to use the right offset (from terrain or from map origin) - m_Y = relative ? GetHeightOffset() : GetHeightFixed(); + m_Y = relative ? GetHeightOffset() : GetHeightFixed(m_X, m_Z); m_RelativeToGround = relative; m_LastYDifference = entity_pos_t::Zero(); AdvertiseInterpolatedPositionChanges(); @@ -503,7 +503,7 @@ return CFixedVector3D(); } - return CFixedVector3D(m_X, GetHeightFixed(), m_Z); + return CFixedVector3D(m_X, GetHeightFixed(m_X, m_Z), m_Z); } virtual CFixedVector2D GetPosition2D() const @@ -525,7 +525,7 @@ return CFixedVector3D(); } - return CFixedVector3D(m_PrevX, GetHeightFixed(), m_PrevZ); + return CFixedVector3D(m_PrevX, GetHeightFixed(m_PrevX, m_PrevZ), m_PrevZ); } virtual CFixedVector2D GetPreviousPosition2D() const Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -398,6 +398,23 @@ return m_RunMultiplier; } + virtual CFixedVector3D EstimateNextPosition(const fixed dt) const + { + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return CFixedVector3D(); + + CFixedVector2D pos = cmpPosition->GetPosition2D(); + entity_angle_t angle = cmpPosition->GetRotation().Y; + + // Copy the path, we don't want to change it. + WaypointPath shortPath = m_ShortPath; + WaypointPath longPath = m_LongPath; + + PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, angle); + return CFixedVector3D(pos.X, cmpPosition->GetHeightFixed(pos.X, pos.Y), pos.Y); + } + virtual pass_class_t GetPassabilityClass() const { return m_PassClass; @@ -616,7 +633,7 @@ * This does not send actually change the position. * @returns true if the move was obstructed. */ - bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle); + bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) const; /** * Update other components on our speed. @@ -947,7 +964,7 @@ return false; } -bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) +bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, entity_angle_t& angle) const { // If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) Index: source/simulation2/components/ICmpPosition.h =================================================================== --- source/simulation2/components/ICmpPosition.h +++ source/simulation2/components/ICmpPosition.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -121,9 +121,9 @@ virtual void SetHeightFixed(entity_pos_t y) = 0; /** - * Returns the vertical offset above the map zero point + * Returns the vertical offset above the map zero point at a certain position */ - virtual entity_pos_t GetHeightFixed() const = 0; + virtual entity_pos_t GetHeightFixed(entity_pos_t x, entity_pos_t z) const = 0; /** * Returns true iff the entity will follow the terrain height (possibly with an offset) Index: source/simulation2/components/ICmpPosition.cpp =================================================================== --- source/simulation2/components/ICmpPosition.cpp +++ source/simulation2/components/ICmpPosition.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -32,7 +32,7 @@ DEFINE_INTERFACE_METHOD_1("SetHeightOffset", void, ICmpPosition, SetHeightOffset, entity_pos_t) DEFINE_INTERFACE_METHOD_CONST_0("GetHeightOffset", entity_pos_t, ICmpPosition, GetHeightOffset) DEFINE_INTERFACE_METHOD_1("SetHeightFixed", void, ICmpPosition, SetHeightFixed, entity_pos_t) -DEFINE_INTERFACE_METHOD_CONST_0("GetHeightFixed", entity_pos_t, ICmpPosition, GetHeightFixed) +DEFINE_INTERFACE_METHOD_CONST_2("GetHeightFixed", entity_pos_t, ICmpPosition, GetHeightFixed, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_CONST_0("IsHeightRelative", bool, ICmpPosition, IsHeightRelative) DEFINE_INTERFACE_METHOD_1("SetHeightRelative", void, ICmpPosition, SetHeightRelative, bool) DEFINE_INTERFACE_METHOD_CONST_0("CanFloat", bool, ICmpPosition, CanFloat) Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -117,6 +117,12 @@ virtual fixed GetSpeed() const = 0; /** + * Wrapper around PerformMove to estimate the next position assuming the current path. + * Make sure this copies all values, no actual movement allowed. + */ + virtual CFixedVector3D EstimateNextPosition(const fixed dt) const = 0; + + /** * Set whether the unit will turn to face the target point after finishing moving. */ virtual void SetFacePointAfterMove(bool facePointAfterMove) = 0; 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("EstimateNextPosition", CFixedVector3D, ICmpUnitMotion, EstimateNextPosition, 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 CFixedVector3D EstimateNextPosition(fixed dt) const + { + return m_Script.Call("EstimateNextPosition", dt); + } + virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_Script.CallVoid("SetFacePointAfterMove", facePointAfterMove); Index: source/simulation2/components/tests/test_Position.h =================================================================== --- source/simulation2/components/tests/test_Position.h +++ source/simulation2/components/tests/test_Position.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -152,12 +152,12 @@ TS_ASSERT(cmp->IsInWorld()); TS_ASSERT(cmp->CanFloat()); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0)), entity_pos_t::FromInt(122)); // Change height offset, the fixed height changes too cmp->SetHeightOffset(entity_pos_t::FromInt(11)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(11)); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(110)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0)), entity_pos_t::FromInt(110)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(0, 110, 0)); // Move @@ -168,20 +168,20 @@ // Change fixed height, the height offset changes too cmp->SetHeightFixed(entity_pos_t::FromInt(122)); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); // The entity can't float anymore, the fixed height is computed from the terrain base cmp->SetFloating(false); TS_ASSERT(!cmp->CanFloat()); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(73)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)), entity_pos_t::FromInt(73)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 73, 200)); // The entity can float again cmp->SetFloating(true); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); @@ -192,18 +192,18 @@ cmp->SetHeightOffset(entity_pos_t::FromInt(11)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(11)); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(110)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)), entity_pos_t::FromInt(110)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 110, 200)); cmp->SetHeightFixed(entity_pos_t::FromInt(122)); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); // The entity can't float anymore and height is not relative, fixed height doesn't change cmp->SetFloating(false); TS_ASSERT(!cmp->CanFloat()); - TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); + TS_ASSERT_EQUALS(cmp->GetHeightFixed(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(72)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); } Index: source/simulation2/components/tests/test_RangeManager.h =================================================================== --- source/simulation2/components/tests/test_RangeManager.h +++ source/simulation2/components/tests/test_RangeManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -51,7 +51,7 @@ virtual void SetHeightOffset(entity_pos_t UNUSED(dy)) { } virtual entity_pos_t GetHeightOffset() const { return entity_pos_t::Zero(); } virtual void SetHeightFixed(entity_pos_t UNUSED(y)) { } - virtual entity_pos_t GetHeightFixed() const { return entity_pos_t::Zero(); } + virtual entity_pos_t GetHeightFixed(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const { return entity_pos_t::Zero(); } virtual bool IsHeightRelative() const { return true; } virtual void SetHeightRelative(bool UNUSED(relative)) { } virtual bool CanFloat() const { return false; }