Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -4419,10 +4419,7 @@ }; /** - * Check if the target is inside the attack range - * For melee attacks, this goes straigt to the regular range calculation - * For ranged attacks, the parabolic formula is used to accout for bigger ranges - * when the target is lower, and smaller ranges when the target is higher + * Check if the target is inside the attack range. */ UnitAI.prototype.CheckTargetAttackRange = function(target, type) { @@ -4439,9 +4436,6 @@ if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); - if (type != "Ranged") - return this.CheckTargetRange(target, IID_Attack, type); - let targetCmpPosition = Engine.QueryInterface(target, IID_Position); if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) return false; @@ -4454,11 +4448,19 @@ return false; let s = thisCmpPosition.GetPosition(); - let t = targetCmpPosition.GetPosition(); - let h = s.y - t.y + range.elevationBonus; - let maxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); + + let maxRange; + // Use a parabolic function for projectile attacks... + // When the target is lower larger range, + // and smaller ranges when the target is higher. + if (type == "Ranged") + maxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); + // and genuine Pythagoras for non-projectile attacks. + // Which means smaller ranges when the target is not on the same level. + else + maxRange = Math.sqrt(Math.square(range.max) - Math.square(h)); if (maxRange < 0) return false; Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -91,7 +91,8 @@ "MoveToFormationOffset": (target, x, z) => {}, "MoveToTargetRange": (target, min, max) => true, "StopMoving": () => {}, - "GetPassabilityClassName": () => "default" + "GetPassabilityClassName": () => "default", + "FaceTowardsPoint": () => true }); AddMock(unit, IID_Vision, { @@ -99,8 +100,8 @@ }); AddMock(unit, IID_Attack, { - GetRange: function() { return { "max": 10, "min": 0}; }, - GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, + GetRange: function() { return { "max": 10, "min": 0, "elevationBonus": 0 }; }, + GetFullAttackRange: function() { return { "max": 40, "min": 0, "elevationBonus": 0 }; }, GetBestAttackAgainst: function(t) { return "melee"; }, GetPreference: function(t) { return 0; }, GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, @@ -121,6 +122,13 @@ AddMock(enemy, IID_UnitAI, { IsAnimal: function() { return false; } }); + + AddMock(enemy, IID_Position, { + GetTurretParent: function() { return INVALID_ENTITY; }, + GetPosition: function() { return new Vector3D(); }, + GetPosition2D: function() { return new Vector2D(); }, + IsInWorld: function() { return true; }, + }); } else if (mode == 2) AddMock(enemy, IID_Health, { @@ -248,7 +256,8 @@ "MoveToFormationOffset": (target, x, z) => {}, "MoveToTargetRange": (target, min, max) => true, "StopMoving": () => {}, - "GetPassabilityClassName": () => "default" + "GetPassabilityClassName": () => "default", + "FaceTowardsPoint": () => true }); AddMock(unit + i, IID_Vision, { @@ -256,8 +265,8 @@ }); AddMock(unit + i, IID_Attack, { - GetRange: function() { return {"max":10, "min": 0}; }, - GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, + GetRange: function() { return { "max":10, "min": 0, "elevationBonus": 0 }; }, + GetFullAttackRange: function() { return { "max": 40, "min": 0 }; }, GetBestAttackAgainst: function(t) { return "melee"; }, GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, CanAttack: function(v) { return true; }, @@ -271,11 +280,18 @@ unitAIs.push(unitAI); } - // create enemy + // Create enemy. AddMock(enemy, IID_Health, { GetHitpoints: function() { return 40; }, }); + AddMock(enemy, IID_Position, { + GetTurretParent: function() { return INVALID_ENTITY; }, + GetPosition: function() { return new Vector3D(); }, + GetPosition2D: function() { return new Vector2D(); }, + IsInWorld: function() { return true; }, + }); + var controllerFormation = ConstructComponent(controller, "Formation", {"FormationName": "Line Closed", "FormationShape": "square", "ShiftRows": "false", "SortingClasses": "", "WidthDepthRatio": 1, "UnitSeparationWidthMultiplier": 1, "UnitSeparationDepthMultiplier": 1, "SpeedMultiplier": 1, "Sloppyness": 0}); var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); @@ -293,11 +309,12 @@ "SetSpeedMultiplier": (speed) => {}, "MoveToPointRange": (x, z, minRange, maxRange) => {}, "StopMoving": () => {}, - "GetPassabilityClassName": () => "default" + "GetPassabilityClassName": () => "default", + "FaceTowardsPoint": () => true }); AddMock(controller, IID_Attack, { - GetRange: function() { return {"max":10, "min": 0}; }, + GetRange: function() { return { "max":10, "min": 0, "elevationBonus": 0 }; }, CanAttackAsFormation: function() { return false; }, });