Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js @@ -739,28 +739,14 @@ */ Attack.prototype.IsTargetInRange = function(target, type) { - let range = this.GetRange(type); - if (type == "Ranged") - { - let cmpPositionTarget = Engine.QueryInterface(target, IID_Position); - if (!cmpPositionTarget || !cmpPositionTarget.IsInWorld()) - return false; - - let cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position); - if (!cmpPositionSelf || !cmpPositionSelf.IsInWorld()) - return false; - - let positionSelf = cmpPositionSelf.GetPosition(); - let positionTarget = cmpPositionTarget.GetPosition(); - - const heightDifference = positionSelf.y + this.GetAttackYOrigin(type) - positionTarget.y; - range.max = Math.sqrt(Math.square(range.max) + 2 * range.max * heightDifference); - - if (range.max < 0) - return false; - } - let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); - return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false); + const range = this.GetRange(type); + return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetParabolicRange( + this.entity, + target, + range.min, + range.max, + this.GetAttackYOrigin(type), + false); }; Attack.prototype.OnValueModification = function(msg) Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -332,36 +332,26 @@ // The obstruction manager performs approximate range checks. // so we need to verify them here. // TODO: perhaps an optional 'precise' mode to range queries would be more performant. - let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + const cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); const range = cmpAttack.GetRange(attackType); - - let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!thisCmpPosition.IsInWorld()) - return; - const y = thisCmpPosition.GetPosition().y + cmpAttack.GetAttackYOrigin(attackType); + const yOrigin = cmpAttack.GetAttackYOrigin(attackType); let firedArrows = 0; while (firedArrows < arrowsToFire && targets.length()) { - let selectedTarget = targets.randomItem(); - - let targetCmpPosition = Engine.QueryInterface(selectedTarget, IID_Position); - if (targetCmpPosition && targetCmpPosition.IsInWorld() && this.CheckTargetVisible(selectedTarget)) + const selectedTarget = targets.randomItem(); + if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange( + this.entity, + selectedTarget, + range.min, + range.max, + yOrigin, + false)) { - // Parabolic range compuation is the same as in UnitAI's MoveToTargetAttackRange. - // h is positive when I'm higher than the target. - const h = y - targetCmpPosition.GetPosition().y; - if (h > -range.max / 2 && cmpObstructionManager.IsInTargetRange( - this.entity, - selectedTarget, - range.min, - Math.sqrt(Math.square(range.max) + 2 * range.max * h), false)) - { - cmpAttack.PerformAttack(attackType, selectedTarget); - PlaySound("attack_" + attackType.toLowerCase(), this.entity); - ++firedArrows; - continue; - } + cmpAttack.PerformAttack(attackType, selectedTarget); + PlaySound("attack_" + attackType.toLowerCase(), this.entity); + ++firedArrows; + continue; } // Could not attack target, try a different target. Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -4716,11 +4716,11 @@ return false; } - let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + const cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!this.AbleToMove(cmpUnitMotion)) return false; - let cmpFormation = Engine.QueryInterface(target, IID_Formation); + const cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); @@ -4735,28 +4735,11 @@ return false; const range = cmpAttack.GetRange(type); - let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!thisCmpPosition.IsInWorld()) - return false; - let s = thisCmpPosition.GetPosition(); - - let targetCmpPosition = Engine.QueryInterface(target, IID_Position); - if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) - return false; - - // Parabolic range compuation is the same as in BuildingAI's FireArrows. - let t = targetCmpPosition.GetPosition(); - // h is positive when I'm higher than the target - const h = s.y - t.y + cmpAttack.GetAttackYOrigin(type); - - let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); - // No negative roots please - if (h <= -range.max / 2) - // return false? Or hope you come close enough? - parabolicMaxRange = 0; + // In case the range returns negative, we are probably too high compared to the target. Hope we come close enough. + const parabolicMaxRange = Math.max(0, Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEffectiveParabolicRange(this.entity, target, range.max, cmpAttack.GetAttackYOrigin(type))); // The parabole changes while walking so be cautious: - let guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange; + const guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange; return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); }; Index: ps/trunk/source/simulation2/components/CCmpObstructionManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpObstructionManager.cpp +++ ps/trunk/source/simulation2/components/CCmpObstructionManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -21,6 +21,7 @@ #include "ICmpObstructionManager.h" #include "ICmpPosition.h" +#include "ICmpRangeManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/helpers/Geometry.h" @@ -475,6 +476,7 @@ virtual bool IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const; virtual bool IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const; + virtual bool IsInTargetParabolicRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, bool opposite) const; virtual bool IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) const; virtual bool AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const; @@ -816,39 +818,43 @@ * to set the opposite bool false and use the edge to egde distance. * * We don't use squares because the are likely to overflow. + * TODO Avoid the overflows and use squares instead. * We use a 0.0001 margin to avoid rounding errors. */ bool CCmpObstructionManager::IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const { fixed dist = DistanceToPoint(ent, px, pz); - // Treat -1 max range as infinite - return dist != fixed::FromInt(-1) && - (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange < fixed::Zero()) && + return maxRange != NEVER_IN_RANGE && dist != fixed::FromInt(-1) && + (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && (opposite ? MaxDistanceToPoint(ent, px, pz) : dist) >= minRange - fixed::FromFloat(0.0001f); } bool CCmpObstructionManager::IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const { fixed dist = DistanceToTarget(ent, target); - // Treat -1 max range as infinite - return dist != fixed::FromInt(-1) && - (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange < fixed::Zero()) && + return maxRange != NEVER_IN_RANGE && dist != fixed::FromInt(-1) && + (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && (opposite ? MaxDistanceToTarget(ent, target) : dist) >= minRange - fixed::FromFloat(0.0001f); } + +bool CCmpObstructionManager::IsInTargetParabolicRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, bool opposite) const +{ + CmpPtr cmpRangeManager(GetSystemEntity()); + return IsInTargetRange(ent, target, minRange, cmpRangeManager->GetEffectiveParabolicRange(ent, target, maxRange, yOrigin), opposite); +} + bool CCmpObstructionManager::IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) const { entity_pos_t distance = (CFixedVector2D(x, z) - CFixedVector2D(px, pz)).Length(); - // Treat -1 max range as infinite - return (distance <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange < fixed::Zero()) && + return maxRange != NEVER_IN_RANGE && (distance <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && distance >= minRange - fixed::FromFloat(0.0001f); } bool CCmpObstructionManager::AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const { fixed dist = DistanceBetweenShapes(source, target); - // Treat -1 max range as infinite - return dist != fixed::FromInt(-1) && - (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange < fixed::Zero()) && + return maxRange != NEVER_IN_RANGE && dist != fixed::FromInt(-1) && + (dist <= (maxRange + fixed::FromFloat(0.0001f)) || maxRange == ALWAYS_IN_RANGE) && (opposite ? MaxDistanceBetweenShapes(source, target) : dist) >= minRange - fixed::FromFloat(0.0001f); } Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp +++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -158,7 +158,7 @@ CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it entity_pos_t minRange; entity_pos_t maxRange; - entity_pos_t elevationBonus; // Used for parabolas only. + entity_pos_t yOrigin; // Used for parabolas only. u32 ownersMask; i32 interface; u8 flagsMask; @@ -290,7 +290,7 @@ { serialize.NumberFixed_Unbounded("min range", value.minRange); serialize.NumberFixed_Unbounded("max range", value.maxRange); - serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus); + serialize.NumberFixed_Unbounded("yOrigin", value.yOrigin); serialize.NumberU32_Unbounded("owners mask", value.ownersMask); serialize.NumberI32_Unbounded("interface", value.interface); Serializer(serialize, "last match", value.lastMatch); @@ -932,11 +932,11 @@ } virtual tag_t CreateActiveParabolicQuery(entity_id_t source, - entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, + entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, const std::vector& owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; - m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags, true); + m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, yOrigin, owners, requiredInterface, flags, true); return id; } @@ -1195,8 +1195,8 @@ void PerformQuery(const Query& q, std::vector& r, CFixedVector2D pos) { - // Special case: range -1.0 means check all entities ignoring distance - if (q.maxRange == entity_pos_t::FromInt(-1)) + // Special case: range is ALWAYS_IN_RANGE means check all entities ignoring distance. + if (q.maxRange == ALWAYS_IN_RANGE) { for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { @@ -1206,14 +1206,14 @@ r.push_back(it->first); } } - // Not the entire world, so check a parabolic range, or a regular range + // Not the entire world, so check a parabolic range, or a regular range. else if (q.parabolic) { - // elevationBonus is part of the 3D position, as the source is really that much heigher + // The yOrigin is part of the 3D position, as the source is really that much heigher. CmpPtr cmpSourcePosition(q.source); CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+ - CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; - // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange + CFixedVector3D(entity_pos_t::Zero(), q.yOrigin, entity_pos_t::Zero()) ; + // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange. m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange * 2); @@ -1279,12 +1279,35 @@ } } - virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos1, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) const + virtual entity_pos_t GetEffectiveParabolicRange(entity_id_t source, entity_id_t target, entity_pos_t range, entity_pos_t yOrigin) const + { + // For non-positive ranges, just return the range. + if (range < entity_pos_t::Zero()) + return range; + + CmpPtr cmpSourcePosition(GetSimContext(), source); + if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) + return NEVER_IN_RANGE; + + CmpPtr cmpTargetPosition(GetSimContext(), target); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) + return NEVER_IN_RANGE; + + entity_pos_t heightDifference = cmpSourcePosition->GetHeightOffset() - cmpTargetPosition->GetHeightOffset() + yOrigin; + if (heightDifference < -range / 2) + return NEVER_IN_RANGE; + + entity_pos_t effectiveRange; + effectiveRange.SetInternalValue(static_cast(isqrt64(SQUARE_U64_FIXED(range) + static_cast(heightDifference.GetInternalValue()) * static_cast(range.GetInternalValue()) * 2))); + return effectiveRange; + } + + virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos1, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t yOrigin, entity_pos_t angle) const { entity_pos_t r = entity_pos_t::Zero(); CFixedVector3D pos(pos1); - pos.Y += elevationBonus; + pos.Y += yOrigin; entity_pos_t orientation = rot.Y; entity_pos_t maxAngle = orientation + angle/2; @@ -1383,12 +1406,13 @@ entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const { - // Min range must be non-negative + // Min range must be non-negative. if (minRange < entity_pos_t::Zero()) LOGWARNING("CCmpRangeManager: Invalid min range %f in query for entity %u", minRange.ToDouble(), source); - // Max range must be non-negative, or else -1 - if (maxRange < entity_pos_t::Zero() && maxRange != entity_pos_t::FromInt(-1)) + // Max range must be non-negative, or else ALWAYS_IN_RANGE. + // TODO add NEVER_IN_RANGE. + if (maxRange < entity_pos_t::Zero() && maxRange != ALWAYS_IN_RANGE) LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source); Query q; @@ -1397,10 +1421,10 @@ q.source = GetSimContext().GetComponentManager().LookupEntityHandle(source); q.minRange = minRange; q.maxRange = maxRange; - q.elevationBonus = entity_pos_t::Zero(); + q.yOrigin = entity_pos_t::Zero(); q.accountForSize = accountForSize; - if (q.accountForSize && q.source.GetId() != INVALID_ENTITY && q.maxRange != entity_pos_t::FromInt(-1)) + if (q.accountForSize && q.source.GetId() != INVALID_ENTITY && q.maxRange != ALWAYS_IN_RANGE) { u32 size = 0; if (ENTITY_IS_LOCAL(q.source.GetId())) @@ -1435,12 +1459,12 @@ } Query ConstructParabolicQuery(entity_id_t source, - entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, + entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const { Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize); q.parabolic = true; - q.elevationBonus = elevationBonus; + q.yOrigin = yOrigin; return q; } @@ -1475,9 +1499,9 @@ } else { - // elevation bonus is part of the 3D position. As if the unit is really that much higher + // yOrigin is part of the 3D position. As if the unit is really that much higher. CFixedVector3D pos3D = cmpSourcePosition->GetPosition(); - pos3D.Y += q.elevationBonus; + pos3D.Y += q.yOrigin; std::vector coords; Index: ps/trunk/source/simulation2/components/ICmpObstructionManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpObstructionManager.h +++ ps/trunk/source/simulation2/components/ICmpObstructionManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -208,25 +208,31 @@ /** * Check if the given entity is in range of the other point given those parameters. - * @param maxRange - if -1, treated as infinite. + * @param maxRange - Can be a nonnegative decimal, ALWAYS_IN_RANGE or NEVER_IN_RANGE. */ virtual bool IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const = 0; /** * Check if the given entity is in range of the target given those parameters. - * @param maxRange - if -1, treated as infinite. + * @param maxRange - Can be a nonnegative decimal, ALWAYS_IN_RANGE or NEVER_IN_RANGE. */ virtual bool IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const = 0; /** + * Check if the given entity is in parabolic range of the target given those parameters. + * @param maxRange - Can be a nonnegative decimal, ALWAYS_IN_RANGE or NEVER_IN_RANGE. + */ + virtual bool IsInTargetParabolicRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, bool opposite) const = 0; + + /** * Check if the given point is in range of the other point given those parameters. - * @param maxRange - if -1, treated as infinite. + * @param maxRange - Can be a nonnegative decimal, ALWAYS_IN_RANGE or NEVER_IN_RANGE. */ virtual bool IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) const = 0; /** * Check if the given shape is in range of the target shape given those parameters. - * @param maxRange - if -1, treated as infinite. + * @param maxRange - Can be a nonnegative decimal, ALWAYS_IN_RANGE or NEVER_IN_RANGE. */ virtual bool AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange, bool opposite) const = 0; Index: ps/trunk/source/simulation2/components/ICmpObstructionManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpObstructionManager.cpp +++ ps/trunk/source/simulation2/components/ICmpObstructionManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -30,5 +30,6 @@ DEFINE_INTERFACE_METHOD("MaxDistanceToTarget", ICmpObstructionManager, MaxDistanceToTarget) DEFINE_INTERFACE_METHOD("IsInPointRange", ICmpObstructionManager, IsInPointRange) DEFINE_INTERFACE_METHOD("IsInTargetRange", ICmpObstructionManager, IsInTargetRange) +DEFINE_INTERFACE_METHOD("IsInTargetParabolicRange", ICmpObstructionManager, IsInTargetParabolicRange) DEFINE_INTERFACE_METHOD("IsPointInPointRange", ICmpObstructionManager, IsPointInPointRange) END_INTERFACE_WRAPPER(ObstructionManager) Index: ps/trunk/source/simulation2/components/ICmpRangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.h +++ ps/trunk/source/simulation2/components/ICmpRangeManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -30,6 +30,18 @@ class FastSpatialSubdivision; /** + * Value assigned to a range we will always be in (caused by out of world or "too high" in parabolic ranges). + * TODO Add this for minRanges too. + */ +const entity_pos_t ALWAYS_IN_RANGE = entity_pos_t::FromInt(-1); + +/** + * Value assigned to a range we will never be in (caused by out of world or "too high" in parabolic ranges). + * TODO Add this to range queries too. + */ +const entity_pos_t NEVER_IN_RANGE = entity_pos_t::FromInt(-2); + +/** * Since GetVisibility queries are run by the range manager * other code using these must include ICmpRangeManager.h anyways, * so define this enum here (Ideally, it'd be in its own header file, @@ -156,25 +168,35 @@ * @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks. * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; * or -1.0 to ignore distance. - * For units on a different elevation, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them - * @param elevationBonus extra bonus so the source can be placed higher and shoot further + * For units on a different height positions, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them + * @param yOrigin extra bonus so the source can be placed higher and shoot further * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @param flags if a entity in range has one of the flags set it will show up. * NB: this one has no accountForSize parameter (assumed true), because we currently can only have 7 arguments for JS functions. * @return unique non-zero identifier of query. */ - virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, + virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, const std::vector& owners, int requiredInterface, u8 flags) = 0; /** + * Get the effective range in a parablic range query. + * @param source The entity id at the origin of the query. + * @param target A target entity id. + * @param range The distance to compare terrain height with. + * @param yOrigin Height the source gains over the target by default. + * @return a fixed number representing the effective range correcting parabolicly for the height difference. Returns -1 when the target is too high compared to the source to be in range. + */ + virtual entity_pos_t GetEffectiveParabolicRange(entity_id_t source, entity_id_t target, entity_pos_t range, entity_pos_t yOrigin) const = 0; + + /** * Get the average elevation over 8 points on distance range around the entity * @param id the entity id to look around * @param range the distance to compare terrain height with * @return a fixed number representing the average difference. It's positive when the entity is on average higher than the terrain surrounding it. */ - virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) const = 0; + virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t yOrigin, entity_pos_t angle) const = 0; /** * Destroy a query and clean up resources. This must be called when an entity no longer needs its Index: ps/trunk/source/simulation2/components/ICmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.cpp +++ ps/trunk/source/simulation2/components/ICmpRangeManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -64,6 +64,7 @@ DEFINE_INTERFACE_METHOD("ExploreTerritories", ICmpRangeManager, ExploreTerritories) DEFINE_INTERFACE_METHOD("SetLosRevealAll", ICmpRangeManager, SetLosRevealAll) DEFINE_INTERFACE_METHOD("GetLosRevealAll", ICmpRangeManager, GetLosRevealAll) +DEFINE_INTERFACE_METHOD("GetEffectiveParabolicRange", ICmpRangeManager, GetEffectiveParabolicRange) DEFINE_INTERFACE_METHOD("GetElevationAdaptedRange", ICmpRangeManager, GetElevationAdaptedRange) DEFINE_INTERFACE_METHOD("ActivateScriptedVisibility", ICmpRangeManager, ActivateScriptedVisibility) DEFINE_INTERFACE_METHOD("GetLosVisibility", ICmpRangeManager, GetLosVisibility_wrapper) Index: ps/trunk/source/simulation2/components/tests/test_RangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_RangeManager.h +++ ps/trunk/source/simulation2/components/tests/test_RangeManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -268,4 +268,46 @@ TS_ASSERT_EQUALS(nearby, std::vector{}); } + + void test_IsInTargetParabolicRange() + { + ComponentTestHelper test(g_ScriptContext); + ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); + const entity_id_t source = 200; + const entity_id_t target = 201; + entity_pos_t range = fixed::FromInt(-3); + entity_pos_t yOrigin = fixed::FromInt(-20); + + // Invalid range. + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range); + + // No source ICmpPosition. + range = fixed::FromInt(10); + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); + + // No target ICmpPosition. + MockPositionRgm cmpSourcePosition; + test.AddMock(source, IID_Position, cmpSourcePosition); + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); + + // Too much height difference. + MockPositionRgm cmpTargetPosition; + test.AddMock(target, IID_Position, cmpTargetPosition); + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); + + // If no offset we get the range. + range = fixed::FromInt(20); + yOrigin = fixed::Zero(); + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range); + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, fixed::Zero(), yOrigin), fixed::Zero()); + + // Normal case. + yOrigin = fixed::FromInt(5); + range = fixed::FromInt(10); + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(14.142136f)); + + // Big range. + range = fixed::FromInt(260); + TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f)); + } };