diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index fe0768a5a1..b740c86ea6 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -42,6 +42,11 @@ UnitAI.prototype.Schema = "" + "" + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + "" + @@ -3462,6 +3467,7 @@ UnitAI.prototype.Init = function() this.formationAnimationVariant = undefined; this.cheeringTime = +(this.template.CheeringTime || 0); + this.rangeError = +(this.template.RangeError || 0); this.SetStance(this.template.DefaultStance); }; @@ -3776,7 +3782,7 @@ UnitAI.prototype.SetupLOSRangeQuery = function(enable = true) // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Identity, - cmpRangeManager.GetEntityFlagMask("normal"), false); + cmpRangeManager.GetEntityFlagMask("normal"), false, this.rangeError); if (enable) cmpRangeManager.EnableActiveQuery(this.losRangeQuery); @@ -3841,7 +3847,7 @@ UnitAI.prototype.SetupAttackRangeQuery = function(enable = true) // Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that. this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Resistance, - cmpRangeManager.GetEntityFlagMask("normal"), false); + cmpRangeManager.GetEntityFlagMask("normal"), false, this.rangeError); if (enable) cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery); diff --git a/binaries/data/mods/public/simulation/templates/template_unit.xml b/binaries/data/mods/public/simulation/templates/template_unit.xml index e819518061..0fb884a89c 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit.xml @@ -109,6 +109,7 @@ true 1 2800 + 10 special/formations/null special/formations/box diff --git a/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml b/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml index 05f3626771..5710abfe2a 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml @@ -42,6 +42,7 @@ + 15 special/formations/skirmish diff --git a/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml b/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml index 5fcbb13751..c5d203b8c0 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml @@ -45,6 +45,9 @@ attack/weapon/bow_attack.xml + + 20 + 0.6 1.2 diff --git a/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml b/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml index f547eb8491..55e679a6be 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml @@ -67,6 +67,7 @@ + 3 standground diff --git a/source/maths/FixedVector2D.h b/source/maths/FixedVector2D.h index 100c42597e..0ca06232d1 100644 --- a/source/maths/FixedVector2D.h +++ b/source/maths/FixedVector2D.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -169,6 +169,29 @@ public: return 0; } + /** + * Returns -1, 0, +1 depending on whether length estimate is less/equal/greater + * than the argument's length. + * Uses a percent error parameter to compare lengths with that percent error. + */ + int CompareLengthRough(const CFixedVector2D& other, u8 rangeError) const + { + u64 d2 = SQUARE_U64_FIXED(X) + SQUARE_U64_FIXED(Y); + u64 od2 = SQUARE_U64_FIXED(other.X) + SQUARE_U64_FIXED(other.Y); + + //overflow risk with long ranges (designed for unit ranges) + CheckMultiplicationOverflow(u64, d2, 100+rangeError, "Overflow in CFixedVector2D::CompareLengthRough()","Underflow in CFixedVector2D::CompareLengthRough()") + d2 = d2 * (100-rangeError + d2 % (1+(rangeError*2))); + CheckMultiplicationOverflow(u64, od2, 100, "Overflow in CFixedVector2D::CompareLengthRough()", "Underflow in CFixedVector2D::CompareLengthRough()") + od2 = od2 * 100; + + if (d2 < od2) + return -1; + if (d2 > od2) + return +1; + return 0; + } + bool IsZero() const { return X.IsZero() && Y.IsZero(); diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp index d54318caef..31c639d636 100644 --- a/source/simulation2/components/CCmpRangeManager.cpp +++ b/source/simulation2/components/CCmpRangeManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -162,6 +162,7 @@ struct Query u32 ownersMask; i32 interface; u8 flagsMask; + u8 rangeError; bool enabled; bool parabolic; bool accountForSize; // If true, the query accounts for unit sizes, otherwise it treats all entities as points. @@ -255,8 +256,8 @@ static_assert(sizeof(EntityData) == 24); class EntityDistanceOrdering { public: - EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source) : - m_EntityData(entities), m_Source(source) + EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source, u8 rangeError = 0) : + m_EntityData(entities), m_Source(source), m_RangeError(rangeError) { } @@ -268,11 +269,12 @@ public: const EntityData& db = m_EntityData.find(b)->second; CFixedVector2D vecA = CFixedVector2D(da.x, da.z) - m_Source; CFixedVector2D vecB = CFixedVector2D(db.x, db.z) - m_Source; - return (vecA.CompareLength(vecB) < 0); + return m_RangeError > 0 ? vecA.CompareLengthRough(vecB, m_RangeError) < 0 : vecA.CompareLength(vecB) < 0; } const EntityMap& m_EntityData; CFixedVector2D m_Source; + u8 m_RangeError; private: EntityDistanceOrdering& operator=(const EntityDistanceOrdering&); @@ -294,6 +296,7 @@ struct SerializeHelper serialize.NumberU32_Unbounded("owners mask", value.ownersMask); serialize.NumberI32_Unbounded("interface", value.interface); Serializer(serialize, "last match", value.lastMatch); + serialize.NumberU8_Unbounded("range percent error", value.rangeError); serialize.NumberU8_Unbounded("flagsMask", value.flagsMask); serialize.Bool("enabled", value.enabled); serialize.Bool("parabolic",value.parabolic); @@ -923,20 +926,20 @@ public: tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, - const std::vector& owners, int requiredInterface, u8 flags, bool accountForSize) override + const std::vector& owners, int requiredInterface, u8 flags, bool accountForSize, u8 rangeError) override { tag_t id = m_QueryNext++; - m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize); + m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize, rangeError); return id; } 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) override + const std::vector& owners, int requiredInterface, u8 flags, u8 rangeError) override { tag_t id = m_QueryNext++; - m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, yOrigin, owners, requiredInterface, flags, true); + m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, yOrigin, owners, requiredInterface, flags, true, rangeError); return id; } @@ -993,25 +996,25 @@ public: std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos, entity_pos_t minRange, entity_pos_t maxRange, - const std::vector& owners, int requiredInterface, bool accountForSize) override + const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError) override { - Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize); + Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize, rangeError); std::vector r; PerformQuery(q, r, pos); // Return the list sorted by distance from the entity - std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); + std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos, q.rangeError)); return r; } std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, - const std::vector& owners, int requiredInterface, bool accountForSize) override + const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError) override { PROFILE("ExecuteQuery"); - Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize); + Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize, rangeError); std::vector r; @@ -1026,7 +1029,7 @@ public: PerformQuery(q, r, pos); // Return the list sorted by distance from the entity - std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); + std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos, q.rangeError)); return r; } @@ -1061,7 +1064,7 @@ public: q.lastMatch = r; // Return the list sorted by distance from the entity - std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); + std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos, q.rangeError)); return r; } @@ -1145,7 +1148,7 @@ public: continue; if (cmpSourcePosition && cmpSourcePosition->IsInWorld()) - std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D())); + std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D(),query.rangeError)); messages.resize(messages.size() + 1); std::pair& back = messages.back(); @@ -1404,7 +1407,7 @@ public: Query ConstructQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, - const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const + const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize, u8 rangeError = 0) const { // Min range must be non-negative. if (minRange < entity_pos_t::Zero()) @@ -1453,6 +1456,7 @@ public: LOGWARNING("CCmpRangeManager: No owners in query for entity %u", source); q.interface = requiredInterface; + q.rangeError = rangeError; q.flagsMask = flagsMask; return q; @@ -1460,9 +1464,9 @@ public: Query ConstructParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, - const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const + const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize, u8 rangeError = 0) const { - Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize); + Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize, rangeError); q.parabolic = true; q.yOrigin = yOrigin; return q; diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h index 5200d01aa3..b5c9e535c3 100644 --- a/source/simulation2/components/ICmpRangeManager.h +++ b/source/simulation2/components/ICmpRangeManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -132,7 +132,7 @@ public: * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, - const std::vector& owners, int requiredInterface, bool accountForSize) = 0; + const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError = 0) = 0; /** * Execute a passive query. @@ -145,7 +145,7 @@ public: * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos, entity_pos_t minRange, entity_pos_t maxRange, - const std::vector& owners, int requiredInterface, bool accountForSize) = 0; + const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError = 0) = 0; /** * Construct an active query. The query will be disabled by default. @@ -159,7 +159,7 @@ public: * @return unique non-zero identifier of query. */ virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, - const std::vector& owners, int requiredInterface, u8 flags, bool accountForSize) = 0; + const std::vector& owners, int requiredInterface, u8 flags, bool accountForSize, u8 rangeError = 0) = 0; /** * Construct an active query of a paraboloic form around the unit. @@ -177,7 +177,7 @@ public: * @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 yOrigin, - const std::vector& owners, int requiredInterface, u8 flags) = 0; + const std::vector& owners, int requiredInterface, u8 flags, u8 rangeError = 0) = 0; /**