Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -308,8 +308,8 @@ ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; - if (template.UnitMotion.Run) - ret.speed.run = getEntityValue("UnitMotion/Run/Speed"); + if (template.UnitMotion.RunMultiplier) + ret.speed.run = getEntityValue("UnitMotion/WalkSpeed") * getEntityValue("UnitMotion/RunMultiplier"); } if (template.ProductionQueue) Index: binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.js @@ -0,0 +1,201 @@ +warn("Setting up test"); + +var tests = {}; +var start = 0; + +var failedTests = 0; + +Trigger.prototype.setupTests = function() +{ + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + start = cmpTimer.GetTime(); + + let cmpTechMgr = QueryPlayerIDInterface(1, IID_TechnologyManager); + + //XXXtreme hack: create a fake technology to drastically limit the range of everybody in place. + cmpTechMgr.modifications = {}; + cmpTechMgr.modifications['Attack/Ranged/MaxRange'] = [ {"affects":["Unit"], "replace":2.5} ]; + + cmpTechMgr = QueryPlayerIDInterface(2, IID_TechnologyManager); + cmpTechMgr.modifications = {}; + cmpTechMgr.modifications['Attack/Ranged/MaxRange'] = [ {"affects":["Unit"], "replace":0} ]; + + let cmpFoundation = Engine.QueryInterface(391, IID_Foundation); + cmpFoundation.InitialiseConstruction(1, "structures/maur_house"); + + // Ready to order people around. + + tests = { + "21" : {"target":353, "continuous":true}, // inbetween house: allies present + "200" : {"target":352, "continuous":true, "expectfail":true}, // Inbetweenhouse- should fail + "24" : {"target":361}, // basic labyrinth + "23" : {"target":356}, // corner house + "22" : {"target":354}, // between house + around + "49" : {"target":355}, // straight + "210" : {"target":357}, // formation - sparse + "211" : {"target":358}, // formation - wall + "186" : {"target":359, "expectfail" : true}, // inside forest - dense + "372" : {"target":359, "continuous":true, "expectfail" : true}, // inside forest - dense + "187" : {"target":360}, // inside forest - sparse + "50" : {"target":352}, // super long trip + "46" : {"target":363}, // trip inside hills + "53" : {"target":362}, // labyrinth: with hole for small + "54" : {"target":365}, // labyrinth: with hole for small - this is the elephant + "85" : {"target":362}, // labyrinth: with hole for small - this is the ram + "390" : {"target":391, "type" : "build"}, // build a house + "393" : {"target":392, "type" : "hunt"}, // hunt a chicken + }; + + // order units to move + for (let test in tests) + { + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + let cmpPos = Engine.QueryInterface(tests[test].target, IID_Position); + + if (tests[test].type && tests[test].type === "build") + { + cmpTesterAI.Repair(tests[test].target, false, false); + continue; + } + else if (tests[test].type && tests[test].type === "hunt") + { + cmpTesterAI.Gather(tests[test].target, false); + continue; + } + + if (!tests[test].continuous) + cmpTesterAI.Walk(cmpPos.GetPosition2D().x, cmpPos.GetPosition2D().y, false); + else + { + let TgPos = new Vector2D(cmpPos.GetPosition2D().x,cmpPos.GetPosition2D().y); + let MyCmpPos = Engine.QueryInterface(+test, IID_Position); + let MyPos = new Vector2D(MyCmpPos.GetPosition2D().x,MyCmpPos.GetPosition2D().y); + + // must be below C++ constant for "short pathfinder only" + let vector = new Vector2D(TgPos.x,TgPos.y); + vector = vector.sub(MyPos).normalize().mult(3.4); // 2 happened to put a waypoint inside a unit, which is unreachable. + + let position = new Vector2D(MyPos.x,MyPos.y); + while (position.distanceToSquared(TgPos) > 12) + { + position.add(vector); + cmpTesterAI.Walk(position.x, position.y, true); + } + } + } + + // set up traders, they're not tested but their behavior should be looked at. + let traders = Engine.GetEntitiesWithInterface(IID_Trader); + for (let tID of traders) + { + let cmpTraderAI = Engine.QueryInterface(+tID, IID_UnitAI); + cmpTraderAI.SetupTradeRoute(401, 402, undefined, false); + } + + let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + + cmpTrigger.RegisterTrigger("OnInterval", "CheckUnits", { + "enabled": true, + "delay": 2 * 1000, + "interval": 1 * 1000, + }); +} + +let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); +cmpTrigger.DoAfterDelay(4000, "setupTests", {}); + +function Success(test) +{ + warn("SUCCESS : test " + test + " succeeded"); + delete(tests[test]); +} + +function Fail(test) +{ + error("ERROR : test " + test + " failed"); + delete(tests[test]); + failedTests++; +} + +function testBuild(test) +{ + let cmpFoundation = Engine.QueryInterface(tests[test].target, IID_Foundation); + if (cmpFoundation.GetBuildProgress() > 0.1) + Success(test); +} + +function testHunt(test) +{ + /* + let cmpFoundation = Engine.QueryInterface(tests[tester].target, IID_Foundation); + if (cmpFoundation.GetBuildProgress() > 0) + Success(tester); + */ +} + +function testWalk(test) +{ + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let time = cmpTimer.GetTime(); + + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + let cmpTesterUM = Engine.QueryInterface(+test, IID_UnitMotion); + if (cmpTesterUM.IsTryingToMove()) + return; + + let cmpPos = Engine.QueryInterface(tests[test].target, IID_Position); + let TgPos = new Vector2D(cmpPos.GetPosition2D().x,cmpPos.GetPosition2D().y); + let MyCmpPos = Engine.QueryInterface(+test, IID_Position); + let MyPos = new Vector2D(MyCmpPos.GetPosition2D().x,MyCmpPos.GetPosition2D().y); + + cmpTesterAI.Stop(); + + if (MyPos.distanceTo(TgPos) > 8 || (tests[test].underTime && time > tests[test].underTime)) + if (!tests[test].expectfail) + { + Fail(test); + return; + } + else if (tests[test].expectfail && MyPos.distanceTo(TgPos) <= 8 && (!tests[test].underTime || time <= tests[test].underTime)) + { + Fail(test); + return; + } + Success(test); +} + + +Trigger.prototype.CheckUnits = function(data) +{ + // Check all tests + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let time = cmpTimer.GetTime(); + + let testsleft = 0; + for (let test in tests) + { + testsleft++; + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + + if (!tests[test].type) + testWalk(test); + else if (tests[test].type === "build") + testBuild(test); + else if (tests[test].type === "hunt") + testHunt(test); + } + if (time > 120000) + for (let test in tests) + { + let cmpTesterAI = Engine.QueryInterface(+test, IID_UnitAI); + cmpTesterAI.Stop(); + Fail(test); + } + if (testsleft === 0) + { + if (failedTests > 0) + QueryPlayerIDInterface(1, IID_Player).SetState("defeated"); + else + QueryPlayerIDInterface(1, IID_Player).SetState("won"); + } +} Index: binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/scenarios/Pathfinding_integrated_testmap.xml @@ -0,0 +1,2920 @@ + + + + + default + + + + + + + 0 + 0.5 + + + + + ocean + + + 177.809 + 4 + 0.45 + 0 + + + + 0 + 1 + 0.99 + 0.1999 + default + + + + + + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + 1 + + + + + + + Index: binaries/data/mods/public/simulation/components/Formation.js =================================================================== --- binaries/data/mods/public/simulation/components/Formation.js +++ binaries/data/mods/public/simulation/components/Formation.js @@ -865,7 +865,7 @@ { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) - minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed()); + minSpeed = Math.min(minSpeed, cmpUnitMotion.GetSpeed()); } minSpeed *= this.GetSpeedMultiplier(); Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -596,8 +596,9 @@ let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { - "walk": cmpUnitMotion.GetWalkSpeed(), - "run": cmpUnitMotion.GetRunSpeed() + "walk": cmpUnitMotion.GetBaseSpeed(), + "run": cmpUnitMotion.GetBaseSpeed() * cmpUnitMotion.GetTopSpeedRatio(), + "current": cmpUnitMotion.GetSpeed() }; return ret; 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 @@ -1,3 +1,5 @@ +const WALKING_SPEED = 1.0 + function UnitAI() {} UnitAI.prototype.Schema = @@ -643,7 +645,7 @@ Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes); // Stop showing the carried resource animation. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order @@ -1391,12 +1393,12 @@ "enter": function () { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - if (cmpFormation && cmpVisual) +/* TOREPLACE if (cmpFormation && cmpVisual) { cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk")); cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run")); } - this.SelectAnimation("move"); + */ }, // Occurs when the unit has reached its destination and the controller @@ -1405,13 +1407,13 @@ // We can only finish this order if the move was really completed. if (!msg.data.error && this.FinishOrder()) return; - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); +/* TOREPLACE var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { cmpVisual.ResetMoveAnimation("walk"); cmpVisual.ResetMoveAnimation("run"); } - +*/ var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.SetInPosition(this.entity); @@ -1424,7 +1426,6 @@ var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.UnsetInPosition(this.entity); - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -1585,7 +1586,6 @@ }, "MoveStarted": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -1603,7 +1603,6 @@ "WALKING": { "enter": function () { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -1614,10 +1613,9 @@ "WALKINGANDFIGHTING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); }, "Timer": function(msg) { @@ -1649,7 +1647,6 @@ } this.StartTimer(0, 1000); - this.SelectAnimation("move"); }, "leave": function() { @@ -1679,10 +1676,9 @@ "ESCORTING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); - this.SelectAnimation("move"); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, @@ -1699,27 +1695,29 @@ }, "leave": function(msg) { - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); this.StopTimer(); }, "MoveStarted": function(msg) { // Adapt the speed to the one of the target if needed - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange)) { - var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI); - if (cmpUnitAI) + var cmpOtherMotion = Engine.QueryInterface(this.isGuardOf, IID_UnitMotion); + if (cmpOtherMotion) { - var speed = cmpUnitAI.GetWalkSpeed(); - if (speed < this.GetWalkSpeed()) + let otherSpeed = cmpOtherMotion.GetSpeed(); + let mySpeed = cmpUnitMotion.GetSpeed(); + let speed = otherSpeed / mySpeed; + if (speed < WALKING_SPEED) this.SetMoveSpeed(speed); } } }, "MoveCompleted": function() { - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("GUARDING"); }, @@ -1774,19 +1772,12 @@ this.PlaySound("panic"); // Run quickly - var speed = this.GetRunSpeed(); - this.SelectAnimation("move"); - this.SetMoveSpeed(speed); - }, - - "HealthChanged": function() { - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); + this.SetMoveSpeed(this.GetRunSpeed()); }, "leave": function() { // Reset normal speed - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); }, "MoveCompleted": function() { @@ -1811,15 +1802,14 @@ "APPROACHING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { // Show carried resources when walking. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.StopTimer(); }, @@ -1970,6 +1960,7 @@ if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -2096,16 +2087,12 @@ "CHASING": { "enter": function () { // Show weapons rather than carried resources. - this.SetGathererAnimationOverride(true); + this.SetAnimationVariant("combat"); - this.SelectAnimation("move"); var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); + // Run after a fleeing target if (cmpUnitAI && cmpUnitAI.IsFleeing()) - { - // Run after a fleeing target - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); - } + this.SetMoveSpeed(this.GetRunSpeed()); this.StartTimer(1000, 1000); }, @@ -2113,15 +2100,16 @@ var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsFleeing()) return; - var speed = this.GetRunSpeed(); - this.SetMoveSpeed(speed); + // TODO: figure out what to do with fleeing + //var speed = this.GetRunSpeed(); + //this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed in case it was changed - this.SetMoveSpeed(this.GetWalkSpeed()); + this.SetMoveSpeed(WALKING_SPEED); // Show carried resources when walking. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.StopTimer(); }, @@ -2147,7 +2135,6 @@ "GATHER": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". @@ -2271,7 +2258,6 @@ // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { @@ -2385,7 +2371,8 @@ delete this.gatheringTarget; // Show the carried resource, if we've gathered anything. - this.SetGathererAnimationOverride(); + this.SelectAnimation("idle"); + this.SetAnimationVariant(); }, "Timer": function(msg) { @@ -2533,7 +2520,6 @@ "APPROACHING": { "enter": function () { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2584,6 +2570,7 @@ }, "leave": function() { + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2634,7 +2621,6 @@ }, "CHASING": { "enter": function () { - this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, @@ -2662,7 +2648,6 @@ "RETURNRESOURCE": { "APPROACHING": { "enter": function () { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -2684,7 +2669,7 @@ cmpResourceGatherer.CommitResources(dropsiteTypes); // Stop showing the carried resource animation. - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order @@ -2720,7 +2705,6 @@ "APPROACHINGMARKET": { "enter": function () { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -2748,7 +2732,6 @@ "REPAIR": { "APPROACHING": { "enter": function () { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -2806,6 +2789,7 @@ if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.repairTarget; + this.SelectAnimation("idle"); this.StopTimer(); }, @@ -2852,7 +2836,7 @@ { let dropsiteTypes = cmpResourceDropsite.GetTypes(); cmpResourceGatherer.CommitResources(dropsiteTypes); - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); } // We finished building it. @@ -2861,7 +2845,7 @@ { if (this.CanReturnResource(msg.data.newentity, true)) { - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } return; @@ -2880,7 +2864,7 @@ { if (this.CanReturnResource(msg.data.newentity, true)) { - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); this.PushOrder("ReturnResource", { "target": msg.data.newentity, "force": false }); } this.PerformGather(msg.data.newentity, true, false); @@ -2945,7 +2929,6 @@ "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -3026,7 +3009,7 @@ if (cmpResourceGatherer) { cmpResourceGatherer.CommitResources(dropsiteTypes); - this.SetGathererAnimationOverride(); + this.SetAnimationVariant(); } } @@ -3102,6 +3085,7 @@ this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); + this.SelectAnimation("idle"); }, "Timer": function(msg) { @@ -3148,7 +3132,6 @@ "PICKUP": { "APPROACHING": { "enter": function() { - this.SelectAnimation("move"); }, "MoveCompleted": function() { @@ -3227,7 +3210,6 @@ "ROAMING": { "enter": function() { // Walk in a random direction - this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); @@ -4010,12 +3992,14 @@ //// Message handlers ///// -UnitAI.prototype.OnMotionChanged = function(msg) +UnitAI.prototype.OnBeginMove = function(msg) { - if (msg.starting && !msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); - else if (!msg.starting || msg.error) - this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); + this.UnitFsm.ProcessMessage(this, { "type": "MoveStarted", "data": msg }); +}; + +UnitAI.prototype.OnFinishedMove = function(msg) +{ + this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": msg }); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) @@ -4076,22 +4060,10 @@ //// Helper functions to be called by the FSM //// -UnitAI.prototype.GetWalkSpeed = function() -{ - var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - return cmpUnitMotion.GetWalkSpeed(); -}; - UnitAI.prototype.GetRunSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - var runSpeed = cmpUnitMotion.GetRunSpeed(); - var walkSpeed = cmpUnitMotion.GetWalkSpeed(); - if (runSpeed <= walkSpeed) - return runSpeed; - var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); - var health = cmpHealth.GetHitpoints()/cmpHealth.GetMaxHitpoints(); - return (health*runSpeed + (1-health)*walkSpeed); + return cmpUnitMotion.GetTopSpeedRatio(); }; /** @@ -4277,37 +4249,42 @@ } }; -UnitAI.prototype.SetGathererAnimationOverride = function(disable) +// Select a visual actor variant for the purpose of animation +// This allows changing the walk animation for normal stance, combat stances, carrying stances… +// without using a hack of replacing the animation in code like we used to. +UnitAI.prototype.SetAnimationVariant = function(type) { - var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - if (!cmpResourceGatherer) - return; - - var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; - // Remove the animation override, so that weapons are shown again. - if (disable) + if (!type || type == "normal") { - cmpVisual.ResetMoveAnimation("walk"); - return; - } + // switch between default and carrying resources depending. - // Work out what we're carrying, in order to select an appropriate animation - var type = cmpResourceGatherer.GetLastCarriedType(); - if (type) - { - var typename = "carry_" + type.generic; + let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (!cmpResourceGatherer) + { + cmpVisual.SetVariant("animationVariant", ""); + return; + } + + let type = cmpResourceGatherer.GetLastCarriedType(); + if (type) + { + let typename = "carry_" + type.generic; - // Special case for meat - if (type.specific == "meat") - typename = "carry_" + type.specific; + // Special case for meat + if (type.specific == "meat") + typename = "carry_" + type.specific; - cmpVisual.ReplaceMoveAnimation("walk", typename); + cmpVisual.SetVariant("animationVariant", typename); + } + else + cmpVisual.SetVariant("animationVariant", ""); } - else - cmpVisual.ResetMoveAnimation("walk"); + else if (type === "combat") + cmpVisual.SetVariant("animationVariant", "combat"); }; UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) @@ -4316,17 +4293,6 @@ if (!cmpVisual) return; - // Special case: the "move" animation gets turned into a special - // movement mode that deals with speeds and walk/run automatically - if (name == "move") - { - // Speed to switch from walking to running animations - var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2; - - cmpVisual.SelectMovementAnimation(runThreshold); - return; - } - var soundgroup; if (sound) { @@ -4359,18 +4325,20 @@ UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpUnitMotion.StopMoving(); + cmpUnitMotion.DiscardMove(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(3); return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(3); return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; @@ -4380,6 +4348,7 @@ return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, 0, 0); }; @@ -4394,6 +4363,7 @@ var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; @@ -4450,6 +4420,7 @@ var guessedMaxRange = (range.max + parabolicMaxRange)/2; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(9); if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange)) return true; @@ -4463,6 +4434,7 @@ return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, min, max); }; @@ -4477,6 +4449,7 @@ var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetAbortIfStuck(5); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; @@ -5693,8 +5666,8 @@ UnitAI.prototype.SetMoveSpeed = function(speed) { - var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); - cmpMotion.SetSpeed(speed); + var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + cmpUnitMotion.SetSpeed(WALKING_SPEED); }; UnitAI.prototype.SetHeldPosition = function(x, z) 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 @@ -251,6 +251,11 @@ this.targetMinRange = minRange; this.targetMaxRange = maxRange; + // we'll tell the visual actor to set our animation here. + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + cmpVisual.SetMovingSpeed(this.speed); + return true; }; @@ -260,6 +265,11 @@ if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return false; + // we'll tell the visual actor to set our animation here. + let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); + if (cmpVisual) + cmpVisual.SetMovingSpeed(this.speed); + var targetPos = cmpTargetPosition.GetPosition2D(); this.hasTarget = true; @@ -295,7 +305,7 @@ return this.IsInPointRange(targetPos.x, targetPos.y, minRange, maxRange); }; -UnitMotionFlying.prototype.GetWalkSpeed = function() +UnitMotionFlying.prototype.GetSpeed = function() { return +this.template.MaxSpeed; }; @@ -325,7 +335,7 @@ // Ignore this - angle is controlled by the target-seeking code instead }; -UnitMotionFlying.prototype.StopMoving = function() +UnitMotionFlying.prototype.DiscardMove = function() { //Invert if (!this.waterDeath) 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 @@ -1,322 +1,172 @@ Engine.LoadHelperScript("FSM.js"); -Engine.LoadHelperScript("Entity.js"); -Engine.LoadHelperScript("Player.js"); -Engine.LoadComponentScript("interfaces/Attack.js"); -Engine.LoadComponentScript("interfaces/Auras.js"); -Engine.LoadComponentScript("interfaces/BuildingAI.js"); -Engine.LoadComponentScript("interfaces/Capturable.js"); -Engine.LoadComponentScript("interfaces/DamageReceiver.js"); -Engine.LoadComponentScript("interfaces/Formation.js"); -Engine.LoadComponentScript("interfaces/Heal.js"); -Engine.LoadComponentScript("interfaces/Health.js"); -Engine.LoadComponentScript("interfaces/Pack.js"); -Engine.LoadComponentScript("interfaces/ResourceSupply.js"); -Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); -Engine.LoadComponentScript("Formation.js"); Engine.LoadComponentScript("UnitAI.js"); -/* Regression test. - * Tests the FSM behaviour of a unit when walking as part of a formation, - * then exiting the formation. - * mode == 0: There is no enemy unit nearby. - * mode == 1: There is a live enemy unit nearby. - * mode == 2: There is a dead enemy unit nearby. - */ -function TestFormationExiting(mode) -{ - ResetState(); +Engine.LoadComponentScript("interfaces/Timer.js"); +Engine.LoadComponentScript("interfaces/Heal.js"); +Engine.LoadComponentScript("interfaces/Sound.js"); +Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); +Engine.LoadComponentScript("interfaces/DamageReceiver.js"); +Engine.LoadComponentScript("interfaces/Pack.js"); + +Engine.LoadHelperScript("Sound.js"); - var playerEntity = 5; - var unit = 10; - var enemy = 20; - var controller = 30; +const PLAYER_ENTITY = 2; +const UNIT_ID = 3; +const TARGET_ENTITY = 4; +var lastAnimationSet = ""; +function SetupMocks() +{ AddMock(SYSTEM_ENTITY, IID_Timer, { SetInterval: function() { }, SetTimeout: function() { }, }); - AddMock(SYSTEM_ENTITY, IID_RangeManager, { - CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { - return 1; - }, - EnableActiveQuery: function(id) { }, - ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; }, - DisableActiveQuery: function(id) { }, - GetEntityFlagMask: function(identifier) { }, - }); - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - GetCurrentTemplateName: function(ent) { return "formations/line_closed"; }, + GetCurrentTemplateName: function(ent) { return "units/gaul_infantry_spearman_b"; }, }); - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: function(id) { return playerEntity; }, - GetNumPlayers: function() { return 2; }, - }); - - AddMock(playerEntity, IID_Player, { - IsAlly: function() { return false; }, - IsEnemy: function() { return true; }, - GetEnemies: function() { return []; }, + AddMock(UNIT_ID, IID_Sound, { + PlaySoundGroup: function() {}, }); - - var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); - - AddMock(unit, IID_Identity, { - GetClassesList: function() { return []; }, + AddMock(UNIT_ID, IID_Position, { + "IsInWorld" : function() { return true; }, + "GetPosition" : function() { return new Vector2D(0,0); } }); - AddMock(unit, IID_Ownership, { - GetOwner: function() { return 1; }, + AddMock(UNIT_ID, IID_UnitMotion, { + GetTopSpeedRatio : function() { return 0; }, + SetSpeed: function() {}, }); - AddMock(unit, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(); }, - GetPosition2D: function() { return new Vector2D(); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, + AddMock(UNIT_ID, IID_DamageReceiver, { + SetInvulnerability : function() {}, }); - AddMock(unit, IID_UnitMotion, { - GetWalkSpeed: function() { return 1; }, - MoveToFormationOffset: function(target, x, z) { }, - IsInTargetRange: function(target, min, max) { return true; }, - MoveToTargetRange: function(target, min, max) { }, - StopMoving: function() { }, - GetPassabilityClassName: function() { return "default"; }, + AddMock(UNIT_ID, IID_Pack, { + Pack : function() {}, + Unpack : function() { }, }); - AddMock(unit, IID_Vision, { - GetRange: function() { return 10; }, - }); - - AddMock(unit, IID_Attack, { - GetRange: function() { return { "max": 10, "min": 0}; }, - GetFullAttackRange: function() { return { "max": 40, "min": 0}; }, - GetBestAttackAgainst: function(t) { return "melee"; }, - GetPreference: function(t) { return 0; }, - GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, - CanAttack: function(v) { return true; }, - CompareEntitiesByPreference: function(a, b) { return 0; }, - }); - - unitAI.OnCreate(); - - unitAI.SetupRangeQuery(1); - - - if (mode == 1) - { - AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 10; }, - }); - AddMock(enemy, IID_UnitAI, { - IsAnimal: function() { return false; } - }); - } - else if (mode == 2) - AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 0; }, - }); - - 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" }); - - AddMock(controller, IID_Position, { - JumpTo: function(x, z) { this.x = x; this.z = z; }, - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(this.x, 0, this.z); }, - GetPosition2D: function() { return new Vector2D(this.x, this.z); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, - }); - - AddMock(controller, IID_UnitMotion, { - SetSpeed: function(speed) { }, - MoveToPointRange: function(x, z, minRange, maxRange) { }, - GetPassabilityClassName: function() { return "default"; }, - }); - - controllerAI.OnCreate(); - - - TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE"); - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); - - controllerFormation.SetMembers([unit]); - controllerAI.Walk(100, 100, false); - controllerAI.OnMotionChanged({ "starting": true }); - - TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING"); - TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING"); - - controllerFormation.Disband(); - - if (mode == 0) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); - else if (mode == 1) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); - else if (mode == 2) - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE"); - else - TS_FAIL("invalid mode"); -} - -function TestMoveIntoFormationWhileAttacking() -{ - ResetState(); - - var playerEntity = 5; - var controller = 10; - var enemy = 20; - var unit = 30; - var units = []; - var unitCount = 8; - var unitAIs = []; - - AddMock(SYSTEM_ENTITY, IID_Timer, { - SetInterval: function() { }, - SetTimeout: function() { }, + AddMock(UNIT_ID, IID_Visual, { + SelectAnimation : function(name) { lastAnimationSet = name; }, + SetVariant : function(key, name) { }, }); - - +/* AddMock(SYSTEM_ENTITY, IID_RangeManager, { CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { return 1; }, EnableActiveQuery: function(id) { }, - ResetActiveQuery: function(id) { return [enemy]; }, + ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; }, DisableActiveQuery: function(id) { }, GetEntityFlagMask: function(identifier) { }, }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - GetCurrentTemplateName: function(ent) { return "formations/line_closed"; }, + GetCurrentTemplateName: function(ent) { return "units/gaul_infantry_spearman_b"; }, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { - GetPlayerByID: function(id) { return playerEntity; }, - GetNumPlayers: function() { return 2; }, - }); - - AddMock(playerEntity, IID_Player, { - IsAlly: function() { return false; }, - IsEnemy: function() { return true; }, - GetEnemies: function() { return []; }, - }); - - // create units - for (var i = 0; i < unitCount; i++) { - - units.push(unit + i); - - var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); - - AddMock(unit + i, IID_Identity, { - GetClassesList: function() { return []; }, - }); - - AddMock(unit + i, IID_Ownership, { - GetOwner: function() { return 1; }, - }); - - AddMock(unit + i, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - GetPosition: function() { return new Vector3D(); }, - GetPosition2D: function() { return new Vector2D(); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, - }); - - AddMock(unit + i, IID_UnitMotion, { - GetWalkSpeed: function() { return 1; }, - MoveToFormationOffset: function(target, x, z) { }, - IsInTargetRange: function(target, min, max) { return true; }, - MoveToTargetRange: function(target, min, max) { }, - StopMoving: function() { }, - GetPassabilityClassName: function() { return "default"; }, - }); - - AddMock(unit + i, IID_Vision, { - GetRange: function() { return 10; }, - }); - - AddMock(unit + i, IID_Attack, { - GetRange: function() { return {"max":10, "min": 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; }, - CompareEntitiesByPreference: function(a, b) { return 0; }, - }); - - unitAI.OnCreate(); - - unitAI.SetupRangeQuery(1); - - unitAIs.push(unitAI); - } - - // create enemy - AddMock(enemy, IID_Health, { - GetHitpoints: function() { return 40; }, - }); - - 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" }); - - AddMock(controller, IID_Position, { - GetTurretParent: function() { return INVALID_ENTITY; }, - JumpTo: function(x, z) { this.x = x; this.z = z; }, - GetPosition: function() { return new Vector3D(this.x, 0, this.z); }, - GetPosition2D: function() { return new Vector2D(this.x, this.z); }, - GetRotation: function() { return { "y": 0 }; }, - IsInWorld: function() { return true; }, - }); - - AddMock(controller, IID_UnitMotion, { - SetSpeed: function(speed) { }, - MoveToPointRange: function(x, z, minRange, maxRange) { }, - IsInTargetRange: function(target, min, max) { return true; }, - GetPassabilityClassName: function() { return "default"; }, - }); - - AddMock(controller, IID_Attack, { - GetRange: function() { return {"max":10, "min": 0}; }, - CanAttackAsFormation: function() { return false; }, - }); - - controllerAI.OnCreate(); + GetPlayerByID: function(id) { return PLAYER_ENTITY; }, + GetNumPlayers: function() { return 1; }, + });*/ +} - controllerFormation.SetMembers(units); +// The intention of this test is to validate that unitAI states that select an animation correctly reset it when leaving +// This tests on "unevaled" FSM state instead of trying to get every state because it's basically a nightmare to get 100% coverage in UnitAI +// And this seems to be good enough to actually detect the bugs. +function testAnimationsAreReset() +{ + ResetState(); + SetupMocks(); - controllerAI.Attack(enemy, []); + let cmpUnitAI = ConstructComponent(UNIT_ID, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); - for each (var ent in unitAIs) { - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); - } + cmpUnitAI.OnCreate(); + TS_ASSERT_EQUALS(cmpUnitAI.UnitFsm.GetCurrentState(cmpUnitAI), "INDIVIDUAL.IDLE"); - controllerAI.MoveIntoFormation({"name": "Circle"}); + cmpUnitAI.order = {"data" : { "targetClasses" : [], "target" : TARGET_ENTITY }}; - // let all units be in position - for each (var ent in unitAIs) { - controllerFormation.SetInPosition(ent); + let TestForReset = function(cmpUnitAI, totest) + { + let shouldReset = false; + for (let fc in totest) + { + if (fc === "leave") + continue; + + let stringified = uneval(totest[fc]); + let pos = stringified.search("SelectAnimation"); + if (pos !== -1) + { + let animation = stringified.substr(pos, stringified.indexOf(")", pos) - pos) + ")"; + if (animation.search("idle") === -1 && animation.search(", true") === -1) + shouldReset = true; + } + } + if (shouldReset) + { + if (!totest.leave) + { + TS_FAIL("No leave"); + return false; + } + + let doesReset = false; + let stringified = uneval(totest.leave); + let pos = stringified.search("SelectAnimation"); + if (pos !== -1) + { + let animation = stringified.substr(pos, stringified.indexOf(")", pos) - pos) + ")"; + if (animation.search("idle") !== -1) + doesReset = true; + } + if (!doesReset) + { + TS_FAIL("No reset in the leave"); + return false; + } + } + return true; } - for each (var ent in unitAIs) { - TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + for (let i in cmpUnitAI.UnitFsmSpec.INDIVIDUAL) + { + // skip the default "Enter" states and such. + if (typeof cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i] === "function") + continue; + + // skip IDLE because the following dumb test doesn't detect it properly. + if (i === "IDLE") + continue; + + // check if this state has 2 levels or 3 levels + // eg INDIVIDUAL.FLEEING or INDIVIDUAL.COMBAT.SOMETHING + let hasChildren = false; + for (let child in cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i]) + if (typeof cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i][child] !== "function") + { + hasChildren = true; + break; + } + if (hasChildren) + { + for (let child in cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i]) + { + if (!TestForReset(cmpUnitAI, cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i][child])) + warn("Failed in " + i + " substate " + child); + } + } + else + if (!TestForReset(cmpUnitAI, cmpUnitAI.UnitFsmSpec.INDIVIDUAL[i])) + warn("Failed in " + i); } - controllerFormation.Disband(); +// TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); } -TestFormationExiting(0); -TestFormationExiting(1); -TestFormationExiting(2); - -TestMoveIntoFormationWhileAttacking(); +testAnimationsAreReset(); \ No newline at end of file Index: binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_1.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_1.json +++ binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_1.json @@ -5,8 +5,7 @@ { "value": "Armour/Pierce", "add": 1 }, { "value": "Armour/Hack", "add": 1 }, { "value": "Armour/Crush", "add": 1 }, - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Formation Reforms", "auraDescription": "All soldiers in his formation +15% speed and +1 armor." Index: binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_2.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_2.json +++ binaries/data/mods/public/simulation/data/auras/athen_hero_iphicrates_2.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Javelin Infantry"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Peltast Reforms", "auraDescription": "All javelin infantry +15% speed." Index: binaries/data/mods/public/simulation/data/auras/athen_hero_themistocles_1.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/athen_hero_themistocles_1.json +++ binaries/data/mods/public/simulation/data/auras/athen_hero_themistocles_1.json @@ -3,8 +3,7 @@ "affects": ["Ship"], "affectedPlayers": ["MutualAlly"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.5 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.5 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.5 } ], "auraName": "Naval Commander Aura", "auraDescription": "When garrisoned in a ship, his ship is +50% faster." Index: binaries/data/mods/public/simulation/data/auras/brit_hero_boudicca.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/brit_hero_boudicca.json +++ binaries/data/mods/public/simulation/data/auras/brit_hero_boudicca.json @@ -4,7 +4,6 @@ "affects": ["Champion"], "modifications": [ { "value": "UnitMotion/WalkSpeed", "multiply": 1.25 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.25 }, { "value": "Attack/Melee/Hack", "multiply": 1.2 }, { "value": "Attack/Melee/Pierce", "multiply": 1.2 }, { "value": "Attack/Melee/Crush", "multiply": 1.2 }, Index: binaries/data/mods/public/simulation/data/auras/brit_hero_caratacos.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/brit_hero_caratacos.json +++ binaries/data/mods/public/simulation/data/auras/brit_hero_caratacos.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Soldier", "Siege"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Hero Aura", "auraDescription": "All soldiers and siege engines +15% speed." Index: binaries/data/mods/public/simulation/data/auras/cart_hero_hamilcar.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/cart_hero_hamilcar.json +++ binaries/data/mods/public/simulation/data/auras/cart_hero_hamilcar.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Soldier", "Siege"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Lightning Aura", "auraDescription": "All soldiers and siege engines +15% speed." Index: binaries/data/mods/public/simulation/data/auras/maur_pillar.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/maur_pillar.json +++ binaries/data/mods/public/simulation/data/auras/maur_pillar.json @@ -3,8 +3,7 @@ "radius": 75, "affects": ["Trader"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.20 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.20 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.20 } ], "auraDescription": "All traders in range +20% speed.", "overlayIcon": "art/textures/ui/session/auras/build_bonus.png" Index: binaries/data/mods/public/simulation/data/auras/pers_hero_darius.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/pers_hero_darius.json +++ binaries/data/mods/public/simulation/data/auras/pers_hero_darius.json @@ -2,8 +2,7 @@ "type": "global", "affects": ["Soldier", "Siege"], "modifications": [ - { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.15 } + { "value": "UnitMotion/WalkSpeed", "multiply": 1.15 } ], "auraName": "Leadership Aura", "auraDescription": "+15% movement speed for all soldiers and siege engines." Index: binaries/data/mods/public/simulation/data/auras/sele_hero_seleucus_victor.json =================================================================== --- binaries/data/mods/public/simulation/data/auras/sele_hero_seleucus_victor.json +++ binaries/data/mods/public/simulation/data/auras/sele_hero_seleucus_victor.json @@ -4,7 +4,6 @@ "affects": ["Elephant Champion"], "modifications": [ { "value": "UnitMotion/WalkSpeed", "multiply": 1.2 }, - { "value": "UnitMotion/Run/Speed", "multiply": 1.2 }, { "value": "Attack/Melee/Hack", "multiply": 1.2 }, { "value": "Attack/Melee/Crush", "multiply": 1.2 } ], Index: binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_camel.xml @@ -32,11 +32,6 @@ 3.0 - - 9.0 - 600.0 - 5.0 - fauna/camel.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml @@ -40,9 +40,6 @@ 1.0 - - 6.0 - fauna/chicken.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_crocodile.xml @@ -39,9 +39,6 @@ 2.0 - - 18.0 - 20 Index: binaries/data/mods/public/simulation/templates/gaia/fauna_deer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_deer.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_deer.xml @@ -14,11 +14,6 @@ 2.0 - - 10.0 - 600.0 - 5.0 - fauna/deer.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_elephant_north_african.xml @@ -60,9 +60,6 @@ 3.0 - - 10.0 - fauna/elephant_african_forest.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_gazelle.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_gazelle.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_gazelle.xml @@ -12,12 +12,6 @@ pitch - - - 600.0 - 5.0 - - fauna/gazelle.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe.xml @@ -32,11 +32,6 @@ 4.0 - - 12.0 - 600.0 - 5.0 - fauna/giraffe_adult.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_giraffe_infant.xml @@ -21,11 +21,6 @@ 4.0 - - 10.0 - 600.0 - 5.0 - fauna/giraffe_baby.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_hawk.xml @@ -7,7 +7,7 @@ 1.0 - 1000.0 + 1000.0 Index: binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_horse.xml @@ -27,11 +27,6 @@ 5.0 - - 12.0 - 600.0 - 5.0 - fauna/horse_a.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_lion.xml @@ -30,9 +30,6 @@ 3.0 - - 15.0 - fauna/lion.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_lioness.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_lioness.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_lioness.xml @@ -30,9 +30,6 @@ 3.0 - - 15.0 - fauna/lioness.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_peacock.xml @@ -30,11 +30,6 @@ 2.0 - - 5.0 - 600.0 - 5.0 - domestic Index: binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_shark.xml @@ -44,9 +44,6 @@ ship-small 4.0 - - 35.0 - false Index: binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_wildebeest.xml @@ -19,11 +19,6 @@ 6.0 - - 15.0 - 600.0 - 5.0 - fauna/wildebeest.xml Index: binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml =================================================================== --- binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml +++ binaries/data/mods/public/simulation/templates/gaia/fauna_zebra.xml @@ -18,11 +18,6 @@ 6.0 - - 15.0 - 600.0 - 5.0 - fauna/zebra.xml Index: binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit.xml +++ binaries/data/mods/public/simulation/templates/template_unit.xml @@ -106,13 +106,6 @@ false 9 - - 15.0 - 50.0 - 0.0 - 0.1 - 0.2 - default Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml @@ -94,11 +94,7 @@ 16.5 - - 26.0 - 600.0 - 5.0 - + 2.5 92 Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_spearman.xml @@ -19,10 +19,5 @@ 22.0 - - 40.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_melee_swordsman.xml @@ -29,10 +29,5 @@ 20.0 - - 28.75 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_archer.xml @@ -25,8 +25,5 @@ 17.5 - - 28.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged_javelinist.xml @@ -25,8 +25,5 @@ 17.5 - - 28.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml @@ -59,11 +59,6 @@ 16.5 - - 26.0 - 1000.0 - 10.0 - 96 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml @@ -34,10 +34,5 @@ 20.5 - - 28.0 - 1000.0 - 10.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml @@ -34,10 +34,5 @@ 20.5 - - 28.0 - 1000.0 - 10.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_spearman.xml @@ -28,8 +28,5 @@ 25.0 - - 40.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_swordsman.xml @@ -29,8 +29,5 @@ 23.0 - - 40.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_elephant.xml @@ -59,11 +59,6 @@ large 8.5 - - 14.0 - 1000.0 - 10.0 - 100 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry.xml @@ -46,11 +46,6 @@ 8.5 - - 17.5 - 600.0 - 5.0 - 84 Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml @@ -36,8 +36,5 @@ 11.0 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml @@ -36,8 +36,5 @@ 16.0 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_pikeman.xml @@ -42,10 +42,5 @@ 7.0 - - 13.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_spearman.xml @@ -39,10 +39,5 @@ 11.5 - - 23.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_swordsman.xml @@ -33,10 +33,5 @@ 12.5 - - 16.0 - 600.0 - 5.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_dog.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_dog.xml +++ binaries/data/mods/public/simulation/templates/template_unit_dog.xml @@ -86,11 +86,7 @@ 14.5 - - 26.0 - 600.0 - 5.0 - + 2.5 30 Index: binaries/data/mods/public/simulation/templates/template_unit_fauna.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna.xml @@ -32,9 +32,7 @@ 6.5 - - 15.0 - + 2.0 true Index: binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml +++ binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml @@ -49,8 +49,6 @@ ship-small 11.5 - - 15.0 - + 1.0 Index: binaries/data/mods/public/simulation/templates/template_unit_hero.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero.xml @@ -60,9 +60,6 @@ 10.5 - - 22.5 - 88 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml @@ -50,11 +50,7 @@ 16.5 - - 26.0 - 1000.0 - 16.0 - + 2.5 100 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml @@ -39,10 +39,5 @@ 17.0 - - 28.0 - 1000.0 - 16.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_elephant_melee.xml @@ -50,11 +50,6 @@ 8.5 - - 14.0 - 1000.0 - 10.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry.xml @@ -38,4 +38,7 @@ actor/human/death/{gender}_death.xml + + 1.6 + Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_pikeman.xml @@ -32,8 +32,5 @@ 8.5 - - 17.5 - Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_spearman.xml @@ -31,8 +31,5 @@ 9 - - 18.75 - Index: binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_swordsman.xml @@ -20,11 +20,6 @@ 9.5 - - 20.0 - 600.0 - 8.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -119,9 +119,7 @@ 9 - - 18.75 - + 1.6 80 Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_pikeman.xml @@ -37,8 +37,5 @@ 6.0 - - 8.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_spearman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_spearman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_spearman.xml @@ -32,8 +32,5 @@ 8.5 - - 15.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_swordsman.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_swordsman.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_melee_swordsman.xml @@ -29,8 +29,5 @@ 9.5 - - 16.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml @@ -34,4 +34,7 @@ attack/weapon/arrowfly.xml + + 2.0 + Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_archer.xml @@ -38,8 +38,5 @@ 8.0 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelinist.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelinist.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_javelinist.xml @@ -32,8 +32,5 @@ 13.5 - - 24.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_slinger.xml @@ -32,8 +32,5 @@ 11.0 - - 24.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml @@ -22,4 +22,7 @@ 4.0 + + 1.0 + Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_bireme.xml @@ -69,8 +69,5 @@ 14 - - 18.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fire.xml @@ -45,9 +45,6 @@ ship-small 17.5 - - 22.0 - 60 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_quinquereme.xml @@ -77,9 +77,6 @@ 16 - - 20 - 110 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_trireme.xml @@ -69,8 +69,5 @@ 16 - - 20.0 - Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ballista.xml @@ -53,9 +53,6 @@ 8 - - 12.0 - 120 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_onager.xml @@ -69,9 +69,6 @@ 7 - - 10.0 - 120 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ram.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ram.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_ram.xml @@ -48,9 +48,6 @@ 8 - - 11.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml +++ binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege_tower.xml @@ -71,9 +71,6 @@ 6.5 - - 10.0 - 80 Index: binaries/data/mods/public/simulation/templates/template_unit_support.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support.xml @@ -31,4 +31,7 @@ passive + + 2.0 + Index: binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support_female_citizen.xml @@ -87,11 +87,6 @@ 9.5 - - 16.0 - 200.0 - 0.0 - 32 Index: binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml @@ -41,11 +41,6 @@ 9 - - 12.0 - 200.0 - 0.0 - 30 Index: binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml +++ binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml @@ -85,10 +85,5 @@ 8 - - 15.0 - 1000.0 - 8.0 - Index: binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/athenians/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/athen_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/athenians/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/cart_cavalry_swordsman_gaul_b.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/cart_cavalry_swordsman_gaul_b.xml +++ binaries/data/mods/public/simulation/templates/units/cart_cavalry_swordsman_gaul_b.xml @@ -21,9 +21,6 @@ 15.5 - - 24.0 - units/celts/cavalry_swordsman_b.xml Index: binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_ballista_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_ballista_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_ballista_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/carthaginians/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/cart_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/carthaginians/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/gaul_champion_fanatic.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/gaul_champion_fanatic.xml +++ binaries/data/mods/public/simulation/templates/units/gaul_champion_fanatic.xml @@ -24,11 +24,6 @@ 5 - - 1.5 - 600.0 - 5.0 - units/celts/fanatic.xml Index: binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/mace_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/maur_elephant_archer_b.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur_elephant_archer_b.xml +++ binaries/data/mods/public/simulation/templates/units/maur_elephant_archer_b.xml @@ -41,11 +41,6 @@ large 8.5 - - 14.0 - 1000.0 - 10.0 - units/mauryans/elephant_archer_b.xml Index: binaries/data/mods/public/simulation/templates/units/maur_hero_chanakya.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur_hero_chanakya.xml +++ binaries/data/mods/public/simulation/templates/units/maur_hero_chanakya.xml @@ -50,11 +50,6 @@ 9 - - 12.0 - 200.0 - 0.0 - 30 Index: binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml +++ binaries/data/mods/public/simulation/templates/units/maur_support_elephant.xml @@ -66,9 +66,6 @@ large 5.5 - - 10.0 - 50 Index: binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_polybolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_polybolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/ptol_mechanical_siege_polybolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/ptolemies/siege_spear.xml Index: binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_ballista_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_ballista_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_ballista_unpacked.xml @@ -15,9 +15,6 @@ 0.001 - - 0.001 - units/romans/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_onager_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_onager_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_onager_unpacked.xml @@ -24,9 +24,6 @@ 0.001 - - 0.001 - 88 Index: binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_scorpio_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_scorpio_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/rome_mechanical_siege_scorpio_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/romans/siege_scorpio.xml Index: binaries/data/mods/public/simulation/templates/units/sele_mechanical_siege_lithobolos_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/sele_mechanical_siege_lithobolos_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/sele_mechanical_siege_lithobolos_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/hellenes/siege_rock.xml Index: binaries/data/mods/public/simulation/templates/units/spart_mechanical_siege_oxybeles_unpacked.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/spart_mechanical_siege_oxybeles_unpacked.xml +++ binaries/data/mods/public/simulation/templates/units/spart_mechanical_siege_oxybeles_unpacked.xml @@ -10,9 +10,6 @@ 0.001 - - 0.001 - units/athenians/siege_spear.xml Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -317,21 +317,55 @@ }; /** - * Sent by CCmpUnitMotion during Update, whenever the motion status has changed - * since the previous update. + * Sent by CCmpUnitMotion during Update, + * whenever we have started actually moving and were not moving before. + * We may or may not already have been trying to move */ -class CMessageMotionChanged : public CMessage +class CMessageBeginMove : public CMessage { public: - DEFAULT_MESSAGE_IMPL(MotionChanged) + DEFAULT_MESSAGE_IMPL(BeginMove) - CMessageMotionChanged(bool starting, bool error) : - starting(starting), error(error) + CMessageBeginMove() + { + } +}; + +/** + * Sent by CCmpUnitMotion during Update, + * whenever we were actually moving before, and cannot continue + * this can be because we've arrived (failed=false) or we failed moving (failed=true) + * After this message is sent, the unit won't remove/repath without orders. + * Will never be sent on the same turn as MT_BeginMove. + */ +class CMessageFinishedMove : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(FinishedMove) + + CMessageFinishedMove(bool fail) : failed(fail) { } - bool starting; // whether this is a start or end of movement - bool error; // whether we failed to start moving (couldn't find any path) + bool failed; // move failed +}; + +/** + * Sent by CCmpUnitMotion during Update, + * whenever we were actually moving before, and now stopped + * In this case, we will retry moving/pathing in the future on our own + * Unless ordered otherwise. + * We are just possibly stuck short-term, or must repath. + * Will never be sent on the same turn as MT_BeginMove. + */ +class CMessagePausedMove : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(PausedMove) + + CMessagePausedMove() + { + } }; /** Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -45,7 +45,9 @@ MESSAGE(PositionChanged) MESSAGE(InterpolatedPositionChanged) MESSAGE(TerritoryPositionChanged) -MESSAGE(MotionChanged) +MESSAGE(BeginMove) +MESSAGE(FinishedMove) +MESSAGE(PausedMove) MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) MESSAGE(VisibilityChanged) Index: source/simulation2/components/CCmpObstructionManager.cpp =================================================================== --- source/simulation2/components/CCmpObstructionManager.cpp +++ source/simulation2/components/CCmpObstructionManager.cpp @@ -20,6 +20,7 @@ #include "simulation2/system/Component.h" #include "ICmpObstructionManager.h" +#include "ICmpPosition.h" #include "ICmpTerrain.h" #include "simulation2/MessageTypes.h" @@ -465,6 +466,9 @@ } } + virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool IsInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); + virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false); virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out); virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out); @@ -658,6 +662,83 @@ REGISTER_COMPONENT_TYPE(ObstructionManager) +bool CCmpObstructionManager::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) +{ + CFixedVector2D pos(x, z); + + entity_pos_t distance = (pos - CFixedVector2D(px, pz)).Length(); + + if (distance < minRange) + return false; + else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) + return false; + else + return true; +} + +bool CCmpObstructionManager::IsInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +{ + CFixedVector2D pos(x, z); + + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager) + return false; + + bool hasObstruction = false; + ICmpObstructionManager::ObstructionSquare obstruction; + CmpPtr cmpObstruction(GetSimContext(), target); + if (cmpObstruction) + hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); + + if (hasObstruction) + { + CFixedVector2D halfSize(obstruction.hw, obstruction.hh); + entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true); + + // Compare with previous obstruction + ICmpObstructionManager::ObstructionSquare previousObstruction; + cmpObstruction->GetPreviousObstructionSquare(previousObstruction); + entity_pos_t previousDistance = Geometry::DistanceToSquare(pos - CFixedVector2D(previousObstruction.x, previousObstruction.z), obstruction.u, obstruction.v, halfSize, true); + + // See if we're too close to the target square + bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero(); + if ((distance < minRange && previousDistance < minRange) || inside) + return false; + + // See if we're close enough to the target square + if (maxRange < entity_pos_t::Zero() || distance <= maxRange || previousDistance <= maxRange) + return true; + + entity_pos_t circleRadius = halfSize.Length(); + + if (Geometry::ShouldTreatTargetAsCircle(maxRange, circleRadius)) + { + // The target is small relative to our range, so pretend it's a circle + // and see if we're close enough to that. + // Also check circle around previous position. + entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; + entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius; + + return circleDistance <= maxRange || previousCircleDistance <= maxRange; + } + + // take minimal clearance required in MoveToTargetRange into account, multiplying by 3/2 for diagonals + return distance <= maxRange || previousDistance <= maxRange; + } + else + { + CmpPtr cmpTargetPosition(GetSimContext(), target); + if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) + return false; + + CFixedVector2D targetPos = cmpTargetPosition->GetPreviousPosition2D(); + entity_pos_t distance = (pos - targetPos).Length(); + + return minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange); + } +} + + bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) { PROFILE("TestLine"); Index: source/simulation2/components/CCmpPathfinder_Vertex.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder_Vertex.cpp +++ source/simulation2/components/CCmpPathfinder_Vertex.cpp @@ -654,8 +654,6 @@ edges.emplace_back(Edge{ ev3, ev0 }); } - // TODO: should clip out vertexes and edges that are outside the range, - // to reduce the search space } // Add terrain obstructions Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -27,6 +27,7 @@ #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpValueModificationManager.h" +#include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Render.h" #include "simulation2/MessageTypes.h" @@ -106,15 +107,104 @@ */ static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000; +/** + * See unitmotion logic for details. Higher means units will retry more often before potentially failing. + */ +static const size_t MAX_PATH_REATTEMPS = 8; + static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1); static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1); class CCmpUnitMotion : public ICmpUnitMotion { +private: + struct SMotionGoal + { + private: + bool m_Valid = false; + + entity_pos_t m_TargetMinRange; + entity_pos_t m_TargetMaxRange; + + entity_id_t m_TargetEntity; + // pathfinder-compliant goal. + PathGoal m_Goal; + public: + SMotionGoal() : m_Valid(false) {}; + + SMotionGoal(PathGoal& goal, entity_pos_t minRange, entity_pos_t maxRange) + { + m_TargetEntity = INVALID_ENTITY; + + m_TargetMinRange = minRange; + m_TargetMaxRange = maxRange; + + m_Goal = goal; + m_Valid = true; + } + + SMotionGoal(const CSimContext& context, entity_id_t target, PathGoal& goal, entity_pos_t minRange, entity_pos_t maxRange) + { + m_TargetEntity = target; + m_TargetMinRange = minRange; + m_TargetMaxRange = maxRange; + + m_Goal = goal; + m_Valid = true; + + UpdateTargetPosition(context); + } + + template + void SerializeCommon(S& serialize) + { + serialize.Bool("valid", m_Valid); + + serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange); + serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange); + + serialize.NumberU32_Unbounded("target entity", m_TargetEntity); + + SerializeGoal()(serialize, "goal", m_Goal); + } + + const PathGoal& Goal() const { return m_Goal; }; + + bool TargetIsEntity() const { return m_TargetEntity != INVALID_ENTITY; } + entity_id_t GetEntity() const { return m_TargetEntity; } + + bool Valid() const { return m_Valid; } + void Clear() { m_Valid = false; } + + entity_pos_t MinRange() const { return m_TargetMinRange; }; + entity_pos_t MaxRange() const { return m_TargetMaxRange; }; + + CFixedVector2D Pos() const { return CFixedVector2D(m_Goal.x, m_Goal.z); } + entity_pos_t X() const { return m_Goal.x; } + entity_pos_t Z() const { return m_Goal.z; } + + void UpdateTargetPosition(const CSimContext& context) + { + if (!TargetIsEntity()) + return; + + CmpPtr cmpPosition(context, m_TargetEntity); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return; + + m_Goal.x = cmpPosition->GetPosition2D().X; + m_Goal.z = cmpPosition->GetPosition2D().Y; + } + + bool IsNotAPoint() const + { + return m_TargetMaxRange > fixed::Zero() || m_Goal.type != PathGoal::POINT; + } + }; + public: static void ClassInit(CComponentManager& componentManager) { - componentManager.SubscribeToMessageType(MT_Update_MotionFormation); componentManager.SubscribeToMessageType(MT_Update_MotionUnit); componentManager.SubscribeToMessageType(MT_PathResult); componentManager.SubscribeToMessageType(MT_OwnershipChanged); @@ -128,121 +218,46 @@ std::vector m_DebugOverlayLongPathLines; std::vector m_DebugOverlayShortPathLines; - // Template state: - - bool m_FormationController; - fixed m_WalkSpeed, m_OriginalWalkSpeed; // in metres per second - fixed m_RunSpeed, m_OriginalRunSpeed; + // Template state, never changed after init. + fixed m_TemplateWalkSpeed, m_TemplateTopSpeedRatio; pass_class_t m_PassClass; std::string m_PassClassName; - - // Dynamic state: - entity_pos_t m_Clearance; - bool m_Moving; - bool m_FacePointAfterMove; - - enum State - { - /* - * Not moving at all. - */ - STATE_IDLE, - - /* - * Not moving at all. Will go to IDLE next turn. - * (This one-turn delay is a hack to fix animation timings.) - */ - STATE_STOPPING, - - /* - * Member of a formation. - * Pathing to the target (depending on m_PathState). - * Target is m_TargetEntity plus m_TargetOffset. - */ - STATE_FORMATIONMEMBER_PATH, - - /* - * Individual unit or formation controller. - * Pathing to the target (depending on m_PathState). - * Target is m_TargetPos, m_TargetMinRange, m_TargetMaxRange; - * if m_TargetEntity is not INVALID_ENTITY then m_TargetPos is tracking it. - */ - STATE_INDIVIDUAL_PATH, - - STATE_MAX - }; - u8 m_State; - - enum PathState - { - /* - * There is no path. - * (This should only happen in IDLE and STOPPING.) - */ - PATHSTATE_NONE, - - /* - * We have an outstanding long path request. - * No paths are usable yet, so we can't move anywhere. - */ - PATHSTATE_WAITING_REQUESTING_LONG, - - /* - * We have an outstanding short path request. - * m_LongPath is valid. - * m_ShortPath is not yet valid, so we can't move anywhere. - */ - PATHSTATE_WAITING_REQUESTING_SHORT, - - /* - * We are following our path, and have no path requests. - * m_LongPath and m_ShortPath are valid. - */ - PATHSTATE_FOLLOWING, - - /* - * We are following our path, and have an outstanding long path request. - * (This is because our target moved a long way and we need to recompute - * the whole path). - * m_LongPath and m_ShortPath are valid. - */ - PATHSTATE_FOLLOWING_REQUESTING_LONG, - - /* - * We are following our path, and have an outstanding short path request. - * (This is because our target moved and we've got a new long path - * which we need to follow). - * m_LongPath is valid; m_ShortPath is valid but obsolete. - */ - PATHSTATE_FOLLOWING_REQUESTING_SHORT, - - PATHSTATE_MAX - }; - u8 m_PathState; - - u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none - entity_id_t m_TargetEntity; - CFixedVector2D m_TargetPos; - CFixedVector2D m_TargetOffset; - entity_pos_t m_TargetMinRange; - entity_pos_t m_TargetMaxRange; + // cached for efficiency + fixed m_TechModifiedWalkSpeed, m_TechModifiedTopSpeedRatio; + // TARGET + // As long as we have a valid target, the unit is considered "on the move". + // It may not be actually moving for a variety of reasons (no path, blocked path)… but it will shortly. + SMotionGoal m_FinalGoal; + + // MOTION PLANNING + // We will abort if we are stuck after X tries. + u8 m_AbortIfStuck; + // turn towards our target at the end + bool m_FacePointAfterMove; + // actual unit speed, after technology and ratio fixed m_Speed; + // cached for convenience + fixed m_SpeedRatio; - // Current mean speed (over the last turn). - fixed m_CurSpeed; + // asynchronous request ID we're waiting for, or 0 if none + u32 m_ExpectedPathTicket; // Currently active paths (storing waypoints in reverse order). // The last item in each path is the point we're currently heading towards. - WaypointPath m_LongPath; - WaypointPath m_ShortPath; - - // Motion planning - u8 m_Tries; // how many tries we've done to get to our current Final Goal. - - PathGoal m_FinalGoal; + WaypointPath m_Path; + // used for the short pathfinder, incremented on each unsuccessful try. + u8 m_Tries; + // Turns to wait before a certain action. + u8 m_WaitingTurns; + // if we actually started moving at some point. + bool m_StartedMoving; + + // Speed over the last turn + // cached so we can tell the visual actor when it changes + fixed m_ActualSpeed; static std::string GetSchema() { @@ -259,14 +274,8 @@ "" "" "" - "" - "" - "" - "" - "" - "" - "" - "" + "" + "" "" "" "" @@ -276,19 +285,15 @@ virtual void Init(const CParamNode& paramNode) { - m_FormationController = paramNode.GetChild("FormationController").ToBool(); - - m_Moving = false; m_FacePointAfterMove = true; - m_WalkSpeed = m_OriginalWalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed(); - m_Speed = m_WalkSpeed; - m_CurSpeed = fixed::Zero(); - - if (paramNode.GetChild("Run").IsOk()) - m_RunSpeed = m_OriginalRunSpeed = paramNode.GetChild("Run").GetChild("Speed").ToFixed(); - else - m_RunSpeed = m_OriginalRunSpeed = m_WalkSpeed; + m_TechModifiedWalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); + m_ActualSpeed = fixed::Zero(); + m_SpeedRatio = fixed::FromInt(1); + + m_TechModifiedTopSpeedRatio = m_TemplateTopSpeedRatio = fixed::FromInt(1); + if (paramNode.GetChild("RunMultiplier").IsOk()) + m_TechModifiedTopSpeedRatio = m_TemplateTopSpeedRatio = paramNode.GetChild("RunMultiplier").ToFixed(); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) @@ -302,18 +307,13 @@ cmpObstruction->SetUnitClearance(m_Clearance); } - m_State = STATE_IDLE; - m_PathState = PATHSTATE_NONE; - m_ExpectedPathTicket = 0; m_Tries = 0; - - m_TargetEntity = INVALID_ENTITY; - - m_FinalGoal.type = PathGoal::POINT; + m_WaitingTurns = 0; m_DebugOverlayEnabled = false; + m_AbortIfStuck = 0; } virtual void Deinit() @@ -323,33 +323,24 @@ template void SerializeCommon(S& serialize) { - serialize.NumberU8("state", m_State, 0, STATE_MAX-1); - serialize.NumberU8("path state", m_PathState, 0, PATHSTATE_MAX-1); - - serialize.StringASCII("pass class", m_PassClassName, 0, 64); + serialize.NumberU8("abort if stuck", m_AbortIfStuck, 0, 255); + serialize.Bool("face point after move", m_FacePointAfterMove); + serialize.NumberFixed_Unbounded("speed", m_Speed); + serialize.NumberFixed_Unbounded("speed ratio", m_SpeedRatio); serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket); - serialize.NumberU32_Unbounded("target entity", m_TargetEntity); - serialize.NumberFixed_Unbounded("target pos x", m_TargetPos.X); - serialize.NumberFixed_Unbounded("target pos y", m_TargetPos.Y); - serialize.NumberFixed_Unbounded("target offset x", m_TargetOffset.X); - serialize.NumberFixed_Unbounded("target offset y", m_TargetOffset.Y); - serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange); - serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange); - - serialize.NumberFixed_Unbounded("speed", m_Speed); - serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); - - serialize.Bool("moving", m_Moving); - serialize.Bool("facePointAfterMove", m_FacePointAfterMove); + SerializeVector()(serialize, "path", m_Path.m_Waypoints); serialize.NumberU8("tries", m_Tries, 0, 255); + serialize.NumberU8("waiting turns", m_WaitingTurns, 0, 255); - SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); - SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); + serialize.Bool("started moving", m_StartedMoving); - SerializeGoal()(serialize, "goal", m_FinalGoal); + // strictly speaking this doesn't need to be serialized since it's graphics-only, but it's nicer to. + serialize.NumberFixed_Unbounded("actual speed", m_ActualSpeed); + + m_FinalGoal.SerializeCommon(serialize); } virtual void Serialize(ISerializer& serialize) @@ -372,22 +363,10 @@ { switch (msg.GetType()) { - case MT_Update_MotionFormation: - { - if (m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } - break; - } case MT_Update_MotionUnit: { - if (!m_FormationController) - { - fixed dt = static_cast (msg).turnLength; - Move(dt); - } + fixed dt = static_cast (msg).turnLength; + Move(dt); break; } case MT_RenderSubmit: @@ -410,24 +389,25 @@ break; } // fall-through - case MT_OwnershipChanged: case MT_Deserialized: { + // tell the visual actor our speed. + // don't call setactualspeed since the if check will return immediately. + CmpPtr cmpVisualActor(GetEntityHandle()); + if (cmpVisualActor) + cmpVisualActor->SetMovingSpeed(m_ActualSpeed); + } + case MT_OwnershipChanged: + { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) break; - fixed newWalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_OriginalWalkSpeed, GetEntityId()); - fixed newRunSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/Run/Speed", m_OriginalRunSpeed, GetEntityId()); + m_TechModifiedWalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId()); + m_TechModifiedTopSpeedRatio = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateTopSpeedRatio, GetEntityId()); - // update m_Speed (the actual speed) if set to one of the variables - if (m_Speed == m_WalkSpeed) - m_Speed = newWalkSpeed; - else if (m_Speed == m_RunSpeed) - m_Speed = newRunSpeed; + m_Speed = m_SpeedRatio.Multiply(GetBaseSpeed()); - m_WalkSpeed = newWalkSpeed; - m_RunSpeed = newRunSpeed; break; } } @@ -439,19 +419,53 @@ GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } - virtual bool IsMoving() + virtual bool IsActuallyMoving() { - return m_Moving; + return m_StartedMoving; } - virtual fixed GetWalkSpeed() + virtual bool IsTryingToMove() { - return m_WalkSpeed; + // speed check as sanity check to avoid infinite loops. + return m_FinalGoal.Valid() && m_Speed > fixed::Zero(); } - virtual fixed GetRunSpeed() + virtual fixed GetBaseSpeed() { - return m_RunSpeed; + return m_TechModifiedWalkSpeed; + } + + virtual fixed GetSpeed() + { + return m_Speed; + } + + virtual fixed GetSpeedRatio() + { + return m_SpeedRatio; + } + + virtual fixed GetTopSpeedRatio() + { + return m_TechModifiedTopSpeedRatio; + } + + virtual void SetSpeed(fixed ratio) + { + m_SpeedRatio = std::min(ratio, GetTopSpeedRatio()); + m_Speed = m_SpeedRatio.Multiply(GetBaseSpeed()); + } + + // convenience wrapper + void SetActualSpeed(fixed newRealSpeed) + { + if (m_ActualSpeed == newRealSpeed) + return; + + m_ActualSpeed = newRealSpeed; + CmpPtr cmpVisualActor(GetEntityHandle()); + if (cmpVisualActor) + cmpVisualActor->SetMovingSpeed(m_ActualSpeed); } virtual pass_class_t GetPassabilityClass() @@ -472,16 +486,6 @@ m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); } - virtual fixed GetCurrentSpeed() - { - return m_CurSpeed; - } - - virtual void SetSpeed(fixed speed) - { - m_Speed = speed; - } - virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_FacePointAfterMove = facePointAfterMove; @@ -493,101 +497,110 @@ UpdateMessageSubscriptions(); } + virtual entity_pos_t GetUnitClearance() + { + return m_Clearance; + } + virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z); virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); - virtual void StopMoving() + virtual void SetAbortIfStuck(u8 shouldAbort) { - m_Moving = false; - m_ExpectedPathTicket = 0; - m_State = STATE_STOPPING; - m_PathState = PATHSTATE_NONE; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); + m_AbortIfStuck = shouldAbort; } - virtual entity_pos_t GetUnitClearance() + virtual void DiscardMove() { - return m_Clearance; + StopMovingQuietly(); } -private: - bool ShouldAvoidMovingUnits() const + // stop moving and send message + virtual void CompleteMove() { - return !m_FormationController; + // highlight bugs. + if (!IsTryingToMove()) + { + LOGERROR("Entity %i trying to stop moving but has not actually started", GetEntityId()); + return; + } + + StopMovingQuietly(); + + if (m_FacePointAfterMove) + { + CmpPtr cmpPosition(GetEntityHandle()); + if (cmpPosition && cmpPosition->IsInWorld()) + FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), m_FinalGoal.X(), m_FinalGoal.Z()); + } + + CMessageFinishedMove msg(false); + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } +private: +/* + TODO: reimplement bool IsFormationMember() const { return m_State == STATE_FORMATIONMEMBER_PATH; } - +*/ entity_id_t GetGroup() const { - return IsFormationMember() ? m_TargetEntity : GetEntityId(); + //return IsFormationMember() ? m_TargetEntity : GetEntityId(); + return GetEntityId(); } bool HasValidPath() const { - return m_PathState == PATHSTATE_FOLLOWING - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG - || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT; + return !m_Path.m_Waypoints.empty(); } - void StartFailed() + void StopMovingQuietly() { - StopMoving(); - m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started + // sanity + m_Tries = 0; + m_WaitingTurns = 0; + m_StartedMoving = false; + + SetActualSpeed(fixed::Zero()); + + // reset state. + m_ExpectedPathTicket = 0; + m_FinalGoal.Clear(); + m_Path.m_Waypoints.clear(); CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetMovingFlag(false); - - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } void MoveFailed() { - StopMoving(); - - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + StopMovingQuietly(); - CMessageMotionChanged msg(false, true); + CMessageFinishedMove msg(true); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void StartSucceeded() + void MovePaused() { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + m_StartedMoving = false; - m_Moving = true; - - CMessageMotionChanged msg(true, false); + CMessagePausedMove msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } - void MoveSucceeded() + void MoveStarted() { - m_Moving = false; - - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + m_StartedMoving = true; - // No longer moving, so speed is 0. - m_CurSpeed = fixed::Zero(); - - CMessageMotionChanged msg(false, false); + CMessageBeginMove msg; GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } @@ -604,45 +617,16 @@ void Move(fixed dt); /** - * Decide whether to approximate the given range from a square target as a circle, - * rather than as a square. - */ - bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const; - - /** - * Computes the current location of our target entity (plus offset). - * Returns false if no target entity or no valid position. - */ - bool ComputeTargetPosition(CFixedVector2D& out); - - /** - * Attempts to replace the current path with a straight line to the goal, - * if this goal is a point, is close enough and the route is not obstructed. - */ - bool TryGoingStraightToGoalPoint(const CFixedVector2D& from); - - /** - * Attempts to replace the current path with a straight line to the target - * entity, if it's close enough and the route is not obstructed. - */ - bool TryGoingStraightToTargetEntity(const CFixedVector2D& from); - - /** * Returns whether the target entity has moved more than minDelta since our * last path computations, and we're close enough to it to care. */ bool CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta); /** - * Update goal position if moving target - */ - void UpdateFinalGoal(); - - /** * Returns whether we are close enough to the target to assume it's a good enough * position to stop. */ - bool ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from); + bool ShouldConsiderOurselvesAtDestination(); /** * Returns whether the length of the given path, plus the distance from @@ -663,11 +647,11 @@ ControlGroupMovementObstructionFilter GetObstructionFilter(bool noTarget = false) const; /** - * Start moving to the given goal, from our current position 'from'. + * Dump current paths and request a new one. * Might go in a straight line immediately, or might start an asynchronous * path request. */ - void BeginPathing(const CFixedVector2D& from, const PathGoal& goal); + void RequestNewPath(); /** * Start an asynchronous long path query. @@ -691,489 +675,295 @@ void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { - // reset our state for sanity. - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - // Ignore obsolete path requests if (ticket != m_ExpectedPathTicket) return; m_ExpectedPathTicket = 0; // we don't expect to get this result again - // Check that we are still able to do something with that path - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - { - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT) - StartFailed(); - else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - StopMoving(); + if (!m_FinalGoal.Valid()) return; - } - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG) + if (path.m_Waypoints.empty()) { - m_LongPath = path; - - // If we are following a path, leave the old m_ShortPath so we can carry on following it - // until a new short path has been computed - if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG) - m_ShortPath.m_Waypoints.clear(); - - // If there's no waypoints then we couldn't get near the target. - // Sort of hack: Just try going directly to the goal point instead - // (via the short pathfinder), so if we're stuck and the user clicks - // close enough to the unit then we can probably get unstuck - if (m_LongPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.emplace_back(Waypoint{ m_FinalGoal.x, m_FinalGoal.z }); - - if (!HasValidPath()) - StartSucceeded(); - - m_PathState = PATHSTATE_FOLLOWING; - - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); + // no waypoints, path failed. + // if we have some room, pop waypoint + // TODO: this isn't particularly bright. + if (!m_Path.m_Waypoints.empty()) + m_Path.m_Waypoints.pop_back(); - m_Moving = true; + // we will then deal with this on the next Move() call. + return; } - else if (m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) - { - m_ShortPath = path; - // If there's no waypoints then we couldn't get near the target - if (m_ShortPath.m_Waypoints.empty()) - { - // If we're globally following a long path, try to remove the next waypoint, it might be obstructed - // If not, and we are not in a formation, retry - // unless we are close to our target and we don't have a target entity. - // This makes sure that units don't clump too much when they are not in a formation and tasked to move. - if (m_LongPath.m_Waypoints.size() > 1) - m_LongPath.m_Waypoints.pop_back(); - else if (IsFormationMember()) - { - m_Moving = false; - CMessageMotionChanged msg(true, true); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - return; - } - - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - if (ShouldConsiderOurselvesAtDestination(pos)) - return; - - UpdateFinalGoal(); - RequestLongPath(pos, m_FinalGoal); - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - return; - } - - // else we could, so reset our number of tries. - m_Tries = 0; - - // Now we've got a short path that we can follow - if (!HasValidPath()) - StartSucceeded(); - - m_PathState = PATHSTATE_FOLLOWING; - - if (cmpObstruction) - cmpObstruction->SetMovingFlag(true); - - m_Moving = true; - } - else - LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState); + // add to the top of our current waypoints + m_Path.m_Waypoints.insert(m_Path.m_Waypoints.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()); } void CCmpUnitMotion::Move(fixed dt) { PROFILE("Move"); - if (m_State == STATE_STOPPING) + if (!IsTryingToMove()) { - m_State = STATE_IDLE; - MoveSucceeded(); + SetActualSpeed(fixed::Zero()); return; } - if (m_State == STATE_IDLE) + m_FinalGoal.UpdateTargetPosition(GetSimContext()); + + // TODO: units will look at each other's position in an arbitrary order that must be the same for any simulation + // In particular this means no threading. Maybe we should update this someday if it's possible. + + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) return; - switch (m_PathState) - { - case PATHSTATE_NONE: - { - // If we're not pathing, do nothing + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition || !cmpPosition->IsInWorld()) return; - } - case PATHSTATE_WAITING_REQUESTING_LONG: - case PATHSTATE_WAITING_REQUESTING_SHORT: + CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + + // Check wether we are at our destination. + // This must be done only at the beginning of a turn, if we do at the end of a turn (after a unit position has changed) + // the unit's position will interpolate but the unit will already be doing the next thing, so it looks like it's gliding. + if (ShouldConsiderOurselvesAtDestination()) { - // If we're waiting for a path and don't have one yet, do nothing + CompleteMove(); return; } - case PATHSTATE_FOLLOWING: - case PATHSTATE_FOLLOWING_REQUESTING_SHORT: - case PATHSTATE_FOLLOWING_REQUESTING_LONG: - { - // TODO: there's some asymmetry here when units look at other - // units' positions - the result will depend on the order of execution. - // Maybe we should split the updates into multiple phases to minimise - // that problem. + // TODO: here should go things such as: + // - has our target moved enough that we should re-path? + // end TODO - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return; + // Keep track of the current unit's position during the update + CFixedVector2D pos = initialPos; - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + // Find the speed factor of the underlying terrain + // (We only care about the tile we start on - it doesn't matter if we're moving + // partially onto a much slower/faster tile) + // TODO: Terrain-dependent speeds are not currently supported + // TODO: note that this is also linked to pathfinding so maybe never supported + // fixed terrainSpeed = fixed::FromInt(1); - CFixedVector2D initialPos = cmpPosition->GetPosition2D(); + bool wasObstructed = false; - // If we're chasing a potentially-moving unit and are currently close - // enough to its current position, and we can head in a straight line - // to it, then throw away our current path and go straight to it - if (m_PathState == PATHSTATE_FOLLOWING) - TryGoingStraightToTargetEntity(initialPos); - - // Keep track of the current unit's position during the update - CFixedVector2D pos = initialPos; - - // If in formation, run to keep up; otherwise just walk - fixed basicSpeed; - if (IsFormationMember()) - basicSpeed = GetRunSpeed(); - else - basicSpeed = m_Speed; // (typically but not always WalkSpeed) + // We want to move (at most) m_Speed*dt units from pos towards the next waypoint - // Find the speed factor of the underlying terrain - // (We only care about the tile we start on - it doesn't matter if we're moving - // partially onto a much slower/faster tile) - // TODO: Terrain-dependent speeds are not currently supported - fixed terrainSpeed = fixed::FromInt(1); + fixed timeLeft = dt; - fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); - - bool wasObstructed = false; - - // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint + // TODO: I think this may be a little buggy if we want to compute it several times per turn. + while (timeLeft > fixed::Zero()) + { + // If we ran out of path, we have to stop + if (!HasValidPath()) + break; - fixed timeLeft = dt; - fixed zero = fixed::Zero(); + CFixedVector2D target; + target = CFixedVector2D(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z); - while (timeLeft > zero) + CFixedVector2D offset = target - pos; + fixed offsetLength = offset.Length(); + // Work out how far we can travel in timeLeft + fixed maxdist = m_Speed.Multiply(timeLeft); + + CFixedVector2D destination; + if (offsetLength <= maxdist) + destination = target; + else { - // If we ran out of path, we have to stop - if (m_ShortPath.m_Waypoints.empty() && m_LongPath.m_Waypoints.empty()) - break; - - CFixedVector2D target; - if (m_ShortPath.m_Waypoints.empty()) - target = CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z); - else - target = CFixedVector2D(m_ShortPath.m_Waypoints.back().x, m_ShortPath.m_Waypoints.back().z); - - CFixedVector2D offset = target - pos; - - // Work out how far we can travel in timeLeft - fixed maxdist = maxSpeed.Multiply(timeLeft); - - // If the target is close, we can move there directly - fixed offsetLength = offset.Length(); - if (offsetLength <= maxdist) - { - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) - { - pos = target; - - // Spend the rest of the time heading towards the next waypoint - timeLeft = timeLeft - (offsetLength / maxSpeed); - - if (m_ShortPath.m_Waypoints.empty()) - m_LongPath.m_Waypoints.pop_back(); - else - m_ShortPath.m_Waypoints.pop_back(); - - continue; - } - else - { - // Error - path was obstructed - wasObstructed = true; - break; - } - } - else - { - // Not close enough, so just move in the right direction - offset.Normalize(maxdist); - target = pos + offset; - - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) - pos = target; - else - wasObstructed = true; // Error - path was obstructed - - break; - } + offset.Normalize(maxdist); + destination = pos + offset; } - // Update the Position component after our movement (if we actually moved anywhere) - if (pos != initialPos) + // TODO: try moving as much as we can still? + // TODO: get more information about what blocked us. + if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, destination.X, destination.Y, m_Clearance, m_PassClass)) { - CFixedVector2D offset = pos - initialPos; + pos = destination; - // Face towards the target - entity_angle_t angle = atan2_approx(offset.X, offset.Y); - cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); + timeLeft = (timeLeft.Multiply(m_Speed) - offsetLength) / m_Speed; - // Calculate the mean speed over this past turn. - m_CurSpeed = cmpPosition->GetDistanceTravelled() / dt; + if (destination == target) + m_Path.m_Waypoints.pop_back(); + continue; } - - if (wasObstructed) + else { - // Oops, we hit something (very likely another unit). - // This is when we might easily get stuck wrongly. + // Error - path was obstructed + wasObstructed = true; + break; + } + } - // check if we've arrived. - if (ShouldConsiderOurselvesAtDestination(pos)) - return; + if (!m_StartedMoving && wasObstructed) + // If this is the turn we start moving, and we're already obstructed, + // fail the move entirely to avoid weirdness. + // (we would need to send a "move started" and a "move failed" message in the same turn) + pos = initialPos; - // If we still have long waypoints, try and compute a short path - // This will get us around units, amongst others. - // However in some cases a long waypoint will be in located in the obstruction of - // an idle unit. In that case, we need to scrap that waypoint or we might never be able to reach it. - // I am not sure why this happens but the following code seems to work. - if (!m_LongPath.m_Waypoints.empty()) - { - CmpPtr cmpObstructionManager(GetSystemEntity()); - if (cmpObstructionManager) - { - // create a fake obstruction to represent our waypoint. - ICmpObstructionManager::ObstructionSquare square; - square.hh = m_Clearance; - square.hw = m_Clearance; - square.u = CFixedVector2D(entity_pos_t::FromInt(1),entity_pos_t::FromInt(0)); - square.v = CFixedVector2D(entity_pos_t::FromInt(0),entity_pos_t::FromInt(1)); - square.x = m_LongPath.m_Waypoints.back().x; - square.z = m_LongPath.m_Waypoints.back().z; - std::vector unitOnGoal; - // don't ignore moving units as those might be units like us, ie not really moving. - cmpObstructionManager->GetUnitsOnObstruction(square, unitOnGoal, GetObstructionFilter(), true); - if (!unitOnGoal.empty()) - m_LongPath.m_Waypoints.pop_back(); - } - if (!m_LongPath.m_Waypoints.empty()) - { - PathGoal goal; - if (m_LongPath.m_Waypoints.size() > 1 || m_FinalGoal.DistanceToPoint(pos) > LONG_PATH_MIN_DIST) - goal = { PathGoal::POINT, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z }; - else - { - UpdateFinalGoal(); - goal = m_FinalGoal; - m_LongPath.m_Waypoints.clear(); - CFixedVector2D target = goal.NearestPointOnGoal(pos); - m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y }); - } - RequestShortPath(pos, goal, true); - m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT; - return; - } - } - // Else, just entirely recompute - UpdateFinalGoal(); - BeginPathing(pos, m_FinalGoal); + // Update the Position component after our movement (if we actually moved anywhere) + if (pos != initialPos) + { + CFixedVector2D offset = pos - initialPos; + + // Face towards the target + entity_angle_t angle = atan2_approx(offset.X, offset.Y); + cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); - // potential TODO: We could switch the short-range pathfinder for something else entirely. + // Calculate the mean speed over this past turn. + // TODO: this is often just a little different from our actual top speed + // so we end up changing the actual speed quite often, which is a little silly. + SetActualSpeed(cmpPosition->GetDistanceTravelled() / dt); + + // tell other components and visual actor we are moving. + if (!m_StartedMoving) + MoveStarted(); + + if (!wasObstructed) + { + // everything is going smoothly, return. + m_Tries = 0; + m_WaitingTurns = 0; return; } + } + else + SetActualSpeed(fixed::Zero()); + + + // tell relevant components we have paused if necessary + if (m_StartedMoving) + MovePaused(); - // We successfully moved along our path, until running out of - // waypoints or time. + // Oops, we've had a problem. Either we were obstructed, or we ran out of path (but still have a goal). + // Handle it. + // Failure to handle it will result in stuckness and players complaining. - if (m_PathState == PATHSTATE_FOLLOWING) + if (m_ExpectedPathTicket != 0) + // wait until we get our path to see where that leads us. + return; + + // if our next waypoint is close enough to our goal and our goal isn't a point, drop our path and recompute directly. + if (m_FinalGoal.IsNotAPoint() && !m_Path.m_Waypoints.empty()) + { + + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (cmpObstructionManager) { - // If we're not currently computing any new paths: - if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) + bool inRange = false; + if (m_FinalGoal.TargetIsEntity()) + inRange = cmpObstructionManager->IsInTargetRange(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z, + m_FinalGoal.GetEntity(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); + else + inRange = cmpObstructionManager->IsInPointRange(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z, + m_FinalGoal.X(), m_FinalGoal.Z(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); + if (inRange) { - if (IsFormationMember()) - { - // We've reached our assigned position. If the controller - // is idle, send a notification in case it should disband, - // otherwise continue following the formation next turn. - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (cmpUnitMotion && !cmpUnitMotion->IsMoving()) - { - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); - - m_Moving = false; - CMessageMotionChanged msg(false, false); - GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); - } - } - else - { - // check if target was reached in case of a moving target - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (cmpUnitMotion && cmpUnitMotion->IsMoving() && - MoveToTargetRange(m_TargetEntity, m_TargetMinRange, m_TargetMaxRange)) - return; - - // Not in formation, so just finish moving - StopMoving(); - m_State = STATE_IDLE; - MoveSucceeded(); - - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(pos, m_FinalGoal.x, m_FinalGoal.z); - // TODO: if the goal was a square building, we ought to point towards the - // nearest point on the square, not towards its center - } + m_Path.m_Waypoints.clear(); + m_WaitingTurns = MAX_PATH_REATTEMPS; // short path } - - // If we have a target entity, and we're not miles away from the end of - // our current path, and the target moved enough, then recompute our - // whole path - if (IsFormationMember()) - CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION); - else - CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA); } } + + // give us some turns to recover. + // TODO: only do this if we ran into a moving unit and not something else, because something else won't move + // specifically: if we ran into a moving unit, we should wait a turn and see what happens + // if we ran into a static unit, recompute a short-path directly + // if we ran into a static obstruction, recompute long-path directly + // And then maybe we could add some finetuning based on target. + if (m_WaitingTurns == 0) + { + if (HasValidPath()) + m_WaitingTurns = MAX_PATH_REATTEMPS; + else + m_WaitingTurns = 3; } -} -bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) -{ - if (m_TargetEntity == INVALID_ENTITY) - return false; + --m_WaitingTurns; - CmpPtr cmpPosition(GetSimContext(), m_TargetEntity); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; + // Try again next turn, no changes + if (m_WaitingTurns >= MAX_PATH_REATTEMPS) + return; - if (m_TargetOffset.IsZero()) + // already waited one turn, no changes, so try computing a short path. + if (m_WaitingTurns >= 3) { - // No offset, just return the position directly - out = cmpPosition->GetPosition2D(); + PathGoal goal; + if (m_Path.m_Waypoints.empty()) + goal = { PathGoal::POINT, m_FinalGoal.X(), m_FinalGoal.Z() }; + else + { + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + m_Path.m_Waypoints.pop_back(); + } + RequestShortPath(pos, goal, true); + return; } - else + + // Last resort, compute a long path + if (m_WaitingTurns == 2) { - // There is an offset, so compute it relative to orientation - entity_angle_t angle = cmpPosition->GetRotation().Y; - CFixedVector2D offset = m_TargetOffset.Rotate(angle); - out = cmpPosition->GetPosition2D() + offset; + PathGoal goal; + if (m_Path.m_Waypoints.empty()) + goal = { PathGoal::POINT, m_FinalGoal.X(), m_FinalGoal.Z() }; + else + { + goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z }; + m_Path.m_Waypoints.pop_back(); + } + RequestLongPath(pos, goal); + return; } - return true; -} -bool CCmpUnitMotion::TryGoingStraightToGoalPoint(const CFixedVector2D& from) -{ - // Make sure the goal is a point (and not a point-like target like a formation controller) - if (m_FinalGoal.type != PathGoal::POINT || m_TargetEntity != INVALID_ENTITY) - return false; - // Fail if the goal is too far away - CFixedVector2D goalPos(m_FinalGoal.x, m_FinalGoal.z); - if ((goalPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; + // m_waitingTurns == 1 here - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; + // we tried getting a renewed path and still got stuck + if (m_AbortIfStuck == 0) + { + MoveFailed(); + return; + } - // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) - return false; + --m_AbortIfStuck; - // That route is okay, so update our path - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); + // Recompute a new path, but wait a few turns first + m_WaitingTurns = 4 + MAX_PATH_REATTEMPS; - return true; + return; } -bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from) +// TODO: this should care about target movement +bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta) { - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) + if (!m_FinalGoal.TargetIsEntity()) return false; - // Fail if the target is too far away - if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) - return false; - - CmpPtr cmpPathfinder(GetSystemEntity()); - if (!cmpPathfinder) - return false; - - // Move the goal to match the target entity's new position - PathGoal goal = m_FinalGoal; - goal.x = targetPos.X; - goal.z = targetPos.Y; - // (we ignore changes to the target's rotation, since only buildings are - // square and buildings don't move) - - // Find the point on the goal shape that we should head towards - CFixedVector2D goalPos = goal.NearestPointOnGoal(from); + if (!HasValidPath()) + return true; - // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) - return false; - - // That route is okay, so update our path - m_FinalGoal = goal; - m_LongPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.clear(); - m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); - - return true; -} + // Fail unless the target has moved enough + CFixedVector2D oldTargetPos = CFixedVector2D(m_Path.m_Waypoints[0].x,m_Path.m_Waypoints[0].z); -bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta) -{ - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) + if ((m_FinalGoal.Pos() - oldTargetPos).CompareLength(minDelta) < 0) return false; - // Fail unless the target has moved enough - CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z); - if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0) - return false; CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); CFixedVector2D oldDir = (oldTargetPos - pos); - CFixedVector2D newDir = (targetPos - pos); + CFixedVector2D newDir = (m_FinalGoal.Pos() - pos); oldDir.Normalize(); newDir.Normalize(); // Fail unless we're close enough to the target to care about its movement // and the angle between the (straight-line) directions of the previous and new target positions is small - if (oldDir.Dot(newDir) > CHECK_TARGET_MOVEMENT_MIN_COS && !PathIsShort(m_LongPath, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST)) + if (oldDir.Dot(newDir) > CHECK_TARGET_MOVEMENT_MIN_COS && !PathIsShort(m_Path, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST)) return false; // Fail if the target is no longer visible to this entity's owner @@ -1183,47 +973,26 @@ if (cmpOwnership) { CmpPtr cmpRangeManager(GetSystemEntity()); - if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) + if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_FinalGoal.GetEntity(), cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) return false; } // The target moved and we need to update our current path; - // change the goal here and expect our caller to start the path request - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; - RequestLongPath(from, m_FinalGoal); - m_PathState = PATHSTATE_FOLLOWING_REQUESTING_LONG; + // Expect our caller to recompute + // Dump our current path. + m_Path.m_Waypoints.clear(); return true; } -void CCmpUnitMotion::UpdateFinalGoal() -{ - if (m_TargetEntity == INVALID_ENTITY) - return; - CmpPtr cmpUnitMotion(GetSimContext(), m_TargetEntity); - if (!cmpUnitMotion) - return; - if (IsFormationMember()) - return; - CFixedVector2D targetPos; - if (!ComputeTargetPosition(targetPos)) - return; - m_FinalGoal.x = targetPos.X; - m_FinalGoal.z = targetPos.Y; -} - -bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from) +// TODO: ought to be cleverer here. +// In particular maybe we should support some "margin" for error. +bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination() { - if (m_TargetEntity != INVALID_ENTITY || m_FinalGoal.DistanceToPoint(from) > SHORT_PATH_GOAL_RADIUS) - return false; - - StopMoving(); - MoveSucceeded(); - - if (m_FacePointAfterMove) - FaceTowardsPointFromPos(from, m_FinalGoal.x, m_FinalGoal.z); - return true; + if (m_FinalGoal.TargetIsEntity()) + return IsInTargetRange(m_FinalGoal.GetEntity(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); + else + return IsInPointRange(m_FinalGoal.X(),m_FinalGoal.Z(), m_FinalGoal.MinRange(), m_FinalGoal.MaxRange()); } bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const @@ -1275,79 +1044,49 @@ ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool noTarget) const { - entity_id_t group = noTarget ? m_TargetEntity : GetGroup(); - return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), group); + entity_id_t group = noTarget ? m_FinalGoal.GetEntity() : GetGroup(); + // TODO: if we sometimes want to consider moving units, change here. + return ControlGroupMovementObstructionFilter(false, group); } - - -void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal) +// TODO: this should be improved, it's a little limited +// EG use of hierarchical pathfinder,… +// also it should probably make the goal passable directly, to avoid conflict with the paths returned. +void CCmpUnitMotion::RequestNewPath() { - // reset our state for sanity. - m_ExpectedPathTicket = 0; + ENSURE(m_ExpectedPathTicket == 0); - CmpPtr cmpObstruction(GetEntityHandle()); - if (cmpObstruction) - cmpObstruction->SetMovingFlag(false); + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition) + return; - m_Moving = false; + // dump current path + m_Path.m_Waypoints.clear(); - m_PathState = PATHSTATE_NONE; + CFixedVector2D position = cmpPosition->GetPosition2D(); #if DISABLE_PATHFINDER { CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); - CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from); + CFixedVector2D goalPos = m_FinalGoal.Goal().NearestPointOnGoal(position); m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); - m_PathState = PATHSTATE_FOLLOWING; return; } #endif - // If we're aiming at a target entity and it's close and we can reach - // it in a straight line, then we'll just go along the straight line - // instead of computing a path. - if (TryGoingStraightToTargetEntity(from)) - { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; - return; - } - - // Same thing applies to non-entity points - if (TryGoingStraightToGoalPoint(from)) - { - if (!HasValidPath()) - StartSucceeded(); - m_PathState = PATHSTATE_FOLLOWING; - return; - } - - // Otherwise we need to compute a path. - // If it's close then just do a short path, not a long path // TODO: If it's close on the opposite side of a river then we really // need a long path, so we shouldn't simply check linear distance // the check is arbitrary but should be a reasonably small distance. - if (goal.DistanceToPoint(from) < LONG_PATH_MIN_DIST) - { - // add our final goal as a long range waypoint so we don't forget - // where we are going if the short-range pathfinder returns - // an aborted path. - m_LongPath.m_Waypoints.clear(); - CFixedVector2D target = m_FinalGoal.NearestPointOnGoal(from); - m_LongPath.m_Waypoints.emplace_back(Waypoint{ target.X, target.Y }); - m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT; - RequestShortPath(from, goal, true); - } + // Maybe use PathIsShort? + + // TODO: note by wraitii: figure out if the above comment is still true. It seems false. + if (m_FinalGoal.Goal().DistanceToPoint(position) < LONG_PATH_MIN_DIST) + RequestShortPath(position, m_FinalGoal.Goal(), true); else - { - m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; - RequestLongPath(from, goal); - } + RequestLongPath(position, m_FinalGoal.Goal()); } void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) @@ -1373,7 +1112,7 @@ return; // wrapping around on m_Tries isn't really a problem so don't check for overflow. - fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * ++m_Tries, goal.DistanceToPoint(from)); + fixed searchRange = std::max(SHORT_PATH_MIN_SEARCH_RANGE * (++m_Tries + 1), goal.DistanceToPoint(from)); if (goal.type != PathGoal::POINT && searchRange < goal.hw && searchRange < SHORT_PATH_MIN_SEARCH_RANGE * 2) searchRange = std::min(goal.hw, SHORT_PATH_MIN_SEARCH_RANGE * 2); if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) @@ -1389,8 +1128,11 @@ bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target) { + // Must closely mirror CmpObstructionManager::IsInPointRange PROFILE("MoveToPointRange"); + DiscardMove(); + CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; @@ -1442,76 +1184,23 @@ } } - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; - - BeginPathing(pos, goal); - - return true; -} - -bool CCmpUnitMotion::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) -{ - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return false; - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - - bool hasObstruction = false; - CmpPtr cmpObstructionManager(GetSystemEntity()); - ICmpObstructionManager::ObstructionSquare obstruction; -//TODO if (cmpObstructionManager) -// hasObstruction = cmpObstructionManager->FindMostImportantObstruction(GetObstructionFilter(), x, z, m_Radius, obstruction); - - if (minRange.IsZero() && maxRange.IsZero() && hasObstruction) - { - // Handle the non-ranged mode: - CFixedVector2D halfSize(obstruction.hw, obstruction.hh); - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize); - - // See if we're too close to the target square - if (distance < minRange) - return false; - - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange) - return true; - - return false; - } + if (target == INVALID_ENTITY) + m_FinalGoal = SMotionGoal(goal, minRange, maxRange); else - { - entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); + m_FinalGoal = SMotionGoal(GetSimContext(), target, goal, minRange, maxRange); - if (distance < minRange) - return false; - else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) - return false; - else - return true; - } -} + RequestNewPath(); -bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const -{ - // Given a square, plus a target range we should reach, the shape at that distance - // is a round-cornered square which we can approximate as either a circle or as a square. - // Previously, we used the shape that minimized the worst-case error. - // However that is unsage in some situations. So let's be less clever and - // just check if our range is at least three times bigger than the circleradius - return (range > circleRadius*3); + return true; } bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { + // Must closely mirror CmpObstructionManager::IsInTargetRange PROFILE("MoveToTargetRange"); + DiscardMove(); + CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; @@ -1588,7 +1277,7 @@ entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA; - if (ShouldTreatTargetAsCircle(minRange, circleRadius)) + if (Geometry::ShouldTreatTargetAsCircle(minRange, circleRadius)) { // The target is small relative to our range, so pretend it's a circle goal.type = PathGoal::INVERTED_CIRCLE; @@ -1616,7 +1305,7 @@ // Circumscribe the square entity_pos_t circleRadius = halfSize.Length(); - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) + if (Geometry::ShouldTreatTargetAsCircle(maxRange, circleRadius)) { // The target is small relative to our range, so pretend it's a circle @@ -1655,117 +1344,47 @@ } } - m_State = STATE_INDIVIDUAL_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(); - m_TargetMinRange = minRange; - m_TargetMaxRange = maxRange; - m_FinalGoal = goal; - m_Tries = 0; + if (target == INVALID_ENTITY) + m_FinalGoal = SMotionGoal(goal, minRange, maxRange); + else + m_FinalGoal = SMotionGoal(GetSimContext(), target, goal, minRange, maxRange); - BeginPathing(pos, goal); + RequestNewPath(); return true; } -bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) +bool CCmpUnitMotion::IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) { - // This function closely mirrors MoveToTargetRange - it needs to return true - // after that Move has completed - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) + if (!cmpPosition) return false; - CFixedVector2D pos = cmpPosition->GetPosition2D(); - CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) - return false; + return true; // what's a sane default here? - bool hasObstruction = false; - ICmpObstructionManager::ObstructionSquare obstruction; - CmpPtr cmpObstruction(GetSimContext(), target); - if (cmpObstruction) - hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); - - if (hasObstruction) - { - CFixedVector2D halfSize(obstruction.hw, obstruction.hh); - entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize, true); - - // Compare with previous obstruction - ICmpObstructionManager::ObstructionSquare previousObstruction; - cmpObstruction->GetPreviousObstructionSquare(previousObstruction); - entity_pos_t previousDistance = Geometry::DistanceToSquare(pos - CFixedVector2D(previousObstruction.x, previousObstruction.z), obstruction.u, obstruction.v, halfSize, true); - - // See if we're too close to the target square - bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero(); - if ((distance < minRange && previousDistance < minRange) || inside) - return false; - - // See if we're close enough to the target square - if (maxRange < entity_pos_t::Zero() || distance <= maxRange || previousDistance <= maxRange) - return true; - - entity_pos_t circleRadius = halfSize.Length(); - - if (ShouldTreatTargetAsCircle(maxRange, circleRadius)) - { - // The target is small relative to our range, so pretend it's a circle - // and see if we're close enough to that. - // Also check circle around previous position. - entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; - entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius; - - return circleDistance <= maxRange || previousCircleDistance <= maxRange; - } - - // take minimal clearance required in MoveToTargetRange into account, multiplying by 3/2 for diagonals - entity_pos_t maxDist = std::max(maxRange, (m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16)*3/2); - return distance <= maxDist || previousDistance <= maxDist; - } - else - { - CmpPtr cmpTargetPosition(GetSimContext(), target); - if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) - return false; - - CFixedVector2D targetPos = cmpTargetPosition->GetPreviousPosition2D(); - entity_pos_t distance = (pos - targetPos).Length(); + CFixedVector2D pos = cmpPosition->GetPosition2D(); - return minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange); - } + return cmpObstructionManager->IsInPointRange(pos.X, pos.Y, x, z, minRange, maxRange + m_Clearance.Multiply(fixed::FromInt(3)/2)); } -void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) +bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { - CmpPtr cmpPosition(GetSimContext(), target); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; + CmpPtr cmpPosition(GetEntityHandle()); + if (!cmpPosition) + return false; - CFixedVector2D pos = cmpPosition->GetPosition2D(); + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (!cmpObstructionManager) + return true; // what's a sane default here? - PathGoal goal; - goal.type = PathGoal::POINT; - goal.x = pos.X; - goal.z = pos.Y; - - m_State = STATE_FORMATIONMEMBER_PATH; - m_TargetEntity = target; - m_TargetOffset = CFixedVector2D(x, z); - m_TargetMinRange = entity_pos_t::Zero(); - m_TargetMaxRange = entity_pos_t::Zero(); - m_FinalGoal = goal; - m_Tries = 0; + CFixedVector2D pos = cmpPosition->GetPosition2D(); - BeginPathing(pos, goal); + return cmpObstructionManager->IsInTargetRange(pos.X, pos.Y, target, minRange, maxRange + m_Clearance.Multiply(fixed::FromInt(3)/2)); } - - - void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { bool floating = false; @@ -1800,8 +1419,7 @@ if (!m_DebugOverlayEnabled) return; - RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); - RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH); + RenderPath(m_Path, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i) collector.Submit(&m_DebugOverlayLongPathLines[i]); Index: source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- source/simulation2/components/CCmpVisualActor.cpp +++ source/simulation2/components/CCmpVisualActor.cpp @@ -54,7 +54,6 @@ public: static void ClassInit(CComponentManager& componentManager) { - componentManager.SubscribeToMessageType(MT_Update_Final); componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); @@ -71,10 +70,7 @@ fixed m_R, m_G, m_B; // shading color - std::map m_AnimOverride; - // Current animation state - fixed m_AnimRunThreshold; // if non-zero this is the special walk/run mode std::string m_AnimName; bool m_AnimOnce; fixed m_AnimSpeed; @@ -83,6 +79,9 @@ fixed m_AnimSyncRepeatTime; // 0.0 if not synced fixed m_AnimSyncOffsetTime; + std::string m_MovingPrefix; + fixed m_MovingSpeed; + std::map m_VariantSelections; u32 m_Seed; // seed used for random variations @@ -190,6 +189,7 @@ { m_Unit = NULL; m_R = m_G = m_B = fixed::FromInt(1); + m_MovingSpeed = fixed::FromInt(1); m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk(); @@ -225,9 +225,6 @@ serialize.NumberFixed_Unbounded("g", m_G); serialize.NumberFixed_Unbounded("b", m_B); - SerializeMap()(serialize, "anim overrides", m_AnimOverride); - - serialize.NumberFixed_Unbounded("anim run threshold", m_AnimRunThreshold); serialize.StringASCII("anim name", m_AnimName, 0, 256); serialize.Bool("anim once", m_AnimOnce); serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed); @@ -282,12 +279,6 @@ { switch (msg.GetType()) { - case MT_Update_Final: - { - const CMessageUpdate_Final& msgData = static_cast (msg); - Update(msgData.turnLength); - break; - } case MT_OwnershipChanged: { if (!m_Unit) @@ -423,7 +414,6 @@ virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) { - m_AnimRunThreshold = fixed::Zero(); m_AnimName = name; m_AnimOnce = once; m_AnimSpeed = speed; @@ -432,28 +422,36 @@ m_AnimSyncRepeatTime = fixed::Zero(); m_AnimSyncOffsetTime = fixed::Zero(); - SetVariant("animation", m_AnimName); + // TODO: change this once we support walk/run-anims + std::string animName = name; + /*if (!m_MovingPrefix.empty() && m_AnimName != "idle") + animName = m_MovingPrefix + "-" + m_AnimName; + else */if (!m_MovingPrefix.empty()) + animName = m_MovingPrefix; - if (m_Unit && m_Unit->GetAnimation()) - m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); - } + SetVariant("animation", animName); - virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) - { - m_AnimOverride[name] = replace; + if (m_Unit && m_Unit->GetAnimation()) + m_Unit->GetAnimation()->SetAnimationState(animName, m_AnimOnce, m_MovingSpeed.Multiply(m_AnimSpeed).ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); } - virtual void ResetMoveAnimation(const std::string& name) + virtual void SetMovingSpeed(fixed movingSpeed) { - std::map::const_iterator it = m_AnimOverride.find(name); - if (it != m_AnimOverride.end()) - m_AnimOverride.erase(name); - } + // TODO: don't copy strings for fun. + std::string prefix; + if (movingSpeed.IsZero()) + prefix = ""; + else + { + CmpPtr cmpUnitMotion(GetEntityHandle()); + if (!cmpUnitMotion) + return; + prefix = cmpUnitMotion->GetSpeedRatio() <= fixed::FromInt(1) ? "walk" : "run"; + } + m_MovingPrefix = prefix; + m_MovingSpeed = movingSpeed.IsZero() ? fixed::FromInt(1) : movingSpeed; - virtual void SelectMovementAnimation(fixed runThreshold) - { - SelectAnimation("walk", false, fixed::FromFloat(1.f), L""); - m_AnimRunThreshold = runThreshold; + SelectAnimation(m_AnimName, m_AnimOnce, m_AnimSpeed, m_SoundGroup); } virtual void SetAnimationSyncRepeat(fixed repeattime) @@ -534,8 +532,6 @@ // ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical. // It is also used by ReloadActor. void ReloadUnitAnimation(); - - void Update(fixed turnLength); }; REGISTER_COMPONENT_TYPE(VisualActor) @@ -741,45 +737,3 @@ m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } -void CCmpVisualActor::Update(fixed UNUSED(turnLength)) -{ - // This function is currently only used to update the animation if the speed in - // CCmpUnitMotion changes. This also only happens in the "special movement mode" - // triggered by SelectMovementAnimation. - - // TODO: This should become event based, in order to save performance and to make the code - // far less hacky. We should also take into account the speed when the animation is different - // from the "special movement mode" walking animation. - - // If we're not in the special movement mode, nothing to do. - if (m_AnimRunThreshold.IsZero()) - return; - - CmpPtr cmpPosition(GetEntityHandle()); - if (!cmpPosition || !cmpPosition->IsInWorld()) - return; - - CmpPtr cmpUnitMotion(GetEntityHandle()); - if (!cmpUnitMotion) - return; - - fixed speed = cmpUnitMotion->GetCurrentSpeed(); - std::string name; - - if (speed.IsZero()) - { - speed = fixed::FromFloat(1.f); - name = "idle"; - } - else - name = speed < m_AnimRunThreshold ? "walk" : "run"; - - std::map::const_iterator it = m_AnimOverride.find(name); - if (it != m_AnimOverride.end()) - name = it->second; - - // Selecting the animation is going to reset the anim run threshold, so save it - fixed runThreshold = m_AnimRunThreshold; - SelectAnimation(name, false, speed, L""); - m_AnimRunThreshold = runThreshold; -} Index: source/simulation2/components/ICmpObstructionManager.h =================================================================== --- source/simulation2/components/ICmpObstructionManager.h +++ source/simulation2/components/ICmpObstructionManager.h @@ -159,6 +159,16 @@ virtual void RemoveShape(tag_t tag) = 0; /** + * Check if the given point is in range of the other point given those parameters + */ + virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** + * Check if the given point is in range of the target given those parameters + */ + virtual bool IsInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; + + /** * Collision test a flat-ended thick line against the current set of shapes. * The line caps extend by @p r beyond the end points. * Only intersections going from outside to inside a shape are counted. Index: source/simulation2/components/ICmpUnitMotion.h =================================================================== --- source/simulation2/components/ICmpUnitMotion.h +++ source/simulation2/components/ICmpUnitMotion.h @@ -47,14 +47,12 @@ virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** - * Determine wether the givven point is within the given range, using the same measurement - * as MoveToPointRange. + * Wrapper around ObstructionManager::IsInPointRange with unit position */ virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** - * Determine whether the target is within the given range, using the same measurement - * as MoveToTargetRange. + * Wrapper around ObstructionManager::IsInTargetRange with unit position */ virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; @@ -71,45 +69,70 @@ virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** - * Join a formation, and move towards a given offset relative to the formation controller entity. - * Continues following the formation until given a different command. + * Turn to look towards the given point. */ - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0; + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; /** - * Turn to look towards the given point. + * Determine whether to abort or retry X times if pathing fails. + * Generally safer to let it abort and inform us. */ - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; + virtual void SetAbortIfStuck(u8 shouldAbort) = 0; + + /** + * Stop moving immediately, don't send messages. + * This should be used if you are going to ask for a new path, + * in the same function, for example. + * In doubt, UnitAI should probably call this. + * Use with caution. + */ + virtual void DiscardMove() = 0; /** - * Stop moving immediately. + * Stop moving immediately, send messages. + * In doubt, components that are not UnitIA should probably call this. */ - virtual void StopMoving() = 0; + virtual void CompleteMove() = 0; /** - * Get the current movement speed. + * Get how much faster/slower we are at than normal. */ - virtual fixed GetCurrentSpeed() = 0; + virtual fixed GetSpeedRatio() = 0; + + /** + * Get how much faster than our regular speed we can go. + */ + virtual fixed GetTopSpeedRatio() = 0; /** * Set the current movement speed. + * 'speed' in % of top speed (ie 3.0 will be 3 times top speed). */ virtual void SetSpeed(fixed speed) = 0; /** - * Get whether the unit is moving. + * Get whether the unit is actually moving on the map this turn. + */ + virtual bool IsActuallyMoving() = 0; + + /** + * Get whether a unit is trying to go somewhere + * NB: this does not mean its position is actually changing right now. */ - virtual bool IsMoving() = 0; + virtual bool IsTryingToMove() = 0; /** - * Get the default speed that this unit will have when walking, in metres per second. + * Get the unit theoretical speed in metres per second. + * GetActualSpeed will return historical speed + * This is affected by SetSpeed. */ - virtual fixed GetWalkSpeed() = 0; + virtual fixed GetSpeed() = 0; /** - * Get the default speed that this unit will have when running, in metres per second. + * Get the unit base/walk speed in metres per second. + * This is NOT affected by SetSpeed. */ - virtual fixed GetRunSpeed() = 0; + virtual fixed GetBaseSpeed() = 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 @@ -27,14 +27,16 @@ DEFINE_INTERFACE_METHOD_4("IsInPointRange", bool, ICmpUnitMotion, IsInPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("IsInTargetRange", bool, ICmpUnitMotion, IsInTargetRange, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("MoveToTargetRange", bool, ICmpUnitMotion, MoveToTargetRange, entity_id_t, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t) -DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving) -DEFINE_INTERFACE_METHOD_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed) +DEFINE_INTERFACE_METHOD_1("SetAbortIfStuck", void, ICmpUnitMotion, SetAbortIfStuck, u8) +DEFINE_INTERFACE_METHOD_0("DiscardMove", void, ICmpUnitMotion, DiscardMove) +DEFINE_INTERFACE_METHOD_0("CompleteMove", void, ICmpUnitMotion, CompleteMove) +DEFINE_INTERFACE_METHOD_0("GetTopSpeedRatio", fixed, ICmpUnitMotion, GetTopSpeedRatio) DEFINE_INTERFACE_METHOD_1("SetSpeed", void, ICmpUnitMotion, SetSpeed, fixed) -DEFINE_INTERFACE_METHOD_0("IsMoving", bool, ICmpUnitMotion, IsMoving) -DEFINE_INTERFACE_METHOD_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) -DEFINE_INTERFACE_METHOD_0("GetRunSpeed", fixed, ICmpUnitMotion, GetRunSpeed) +DEFINE_INTERFACE_METHOD_0("IsActuallyMoving", bool, ICmpUnitMotion, IsActuallyMoving) +DEFINE_INTERFACE_METHOD_0("IsTryingToMove", bool, ICmpUnitMotion, IsTryingToMove) +DEFINE_INTERFACE_METHOD_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed) +DEFINE_INTERFACE_METHOD_0("GetBaseSpeed", fixed, ICmpUnitMotion, GetBaseSpeed) DEFINE_INTERFACE_METHOD_0("GetPassabilityClassName", std::string, ICmpUnitMotion, GetPassabilityClassName) DEFINE_INTERFACE_METHOD_0("GetUnitClearance", entity_pos_t, ICmpUnitMotion, GetUnitClearance) DEFINE_INTERFACE_METHOD_1("SetFacePointAfterMove", void, ICmpUnitMotion, SetFacePointAfterMove, bool) @@ -66,24 +68,29 @@ return m_Script.Call("MoveToTargetRange", target, minRange, maxRange); } - virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) + virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { - m_Script.CallVoid("MoveToFormationOffset", target, x, z); + m_Script.CallVoid("FaceTowardsPoint", x, z); } - virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) + virtual void DiscardMove() { - m_Script.CallVoid("FaceTowardsPoint", x, z); + m_Script.CallVoid("DiscardMove"); } - virtual void StopMoving() + virtual void CompleteMove() { - m_Script.CallVoid("StopMoving"); + m_Script.CallVoid("CompleteMove"); } - virtual fixed GetCurrentSpeed() + virtual void SetAbortIfStuck(u8 shouldAbort) { - return m_Script.Call("GetCurrentSpeed"); + m_Script.CallVoid("SetAbortIfStuck", shouldAbort); + } + + virtual fixed GetActualSpeed() + { + return m_Script.Call("GetActualSpeed"); } virtual void SetSpeed(fixed speed) @@ -91,19 +98,29 @@ m_Script.CallVoid("SetSpeed", speed); } - virtual bool IsMoving() + virtual fixed GetTopSpeedRatio() + { + return m_Script.Call("GetTopSpeedRatio"); + } + + virtual bool IsActuallyMoving() + { + return m_Script.Call("IsActuallyMoving"); + } + + virtual bool IsTryingToMove() { - return m_Script.Call("IsMoving"); + return m_Script.Call("IsTryingToMove"); } - virtual fixed GetWalkSpeed() + virtual fixed GetSpeed() { - return m_Script.Call("GetWalkSpeed"); + return m_Script.Call("GetSpeed"); } - virtual fixed GetRunSpeed() + virtual fixed GetBaseSpeed() { - return m_Script.Call("GetRunSpeed"); + return m_Script.Call("GetBaseSpeed"); } virtual void SetFacePointAfterMove(bool facePointAfterMove) @@ -116,6 +133,11 @@ return m_Script.Call("GetPassabilityClass"); } + virtual fixed GetSpeedRatio() + { + return fixed::FromInt(1); + } + virtual std::string GetPassabilityClassName() { return m_Script.Call("GetPassabilityClassName"); Index: source/simulation2/components/ICmpVisual.h =================================================================== --- source/simulation2/components/ICmpVisual.h +++ source/simulation2/components/ICmpVisual.h @@ -99,25 +99,10 @@ virtual void SelectAnimation(const std::string& name, bool once, fixed speed, const std::wstring& soundgroup) = 0; /** - * Replaces a specified animation with another. Only affects the special speed-based - * animation determination behaviour. - * @param name Animation to match. - * @param replace Animation that should replace the matched animation. + * Tell the visual actor that the unit is currently moving at the given speed. + * If speed is 0, the unit will become idle. */ - virtual void ReplaceMoveAnimation(const std::string& name, const std::string& replace) = 0; - - /** - * Ensures that the given animation will be used when it normally would be, - * removing reference to any animation that might replace it. - * @param name Animation name to remove from the replacement map. - */ - virtual void ResetMoveAnimation(const std::string& name) = 0; - - /** - * Start playing the walk/run animations, scaled to the unit's movement speed. - * @param runThreshold movement speed at which to switch to the run animation - */ - virtual void SelectMovementAnimation(fixed runThreshold) = 0; + virtual void SetMovingSpeed(fixed movingSpeed) = 0; /** * Adjust the speed of the current animation, so it can match simulation events. Index: source/simulation2/components/ICmpVisual.cpp =================================================================== --- source/simulation2/components/ICmpVisual.cpp +++ source/simulation2/components/ICmpVisual.cpp @@ -24,13 +24,11 @@ BEGIN_INTERFACE_WRAPPER(Visual) DEFINE_INTERFACE_METHOD_2("SetVariant", void, ICmpVisual, SetVariant, CStr, CStr) DEFINE_INTERFACE_METHOD_4("SelectAnimation", void, ICmpVisual, SelectAnimation, std::string, bool, fixed, std::wstring) -DEFINE_INTERFACE_METHOD_1("SelectMovementAnimation", void, ICmpVisual, SelectMovementAnimation, fixed) -DEFINE_INTERFACE_METHOD_1("ResetMoveAnimation", void, ICmpVisual, ResetMoveAnimation, std::string) -DEFINE_INTERFACE_METHOD_2("ReplaceMoveAnimation", void, ICmpVisual, ReplaceMoveAnimation, std::string, std::string) DEFINE_INTERFACE_METHOD_1("SetAnimationSyncRepeat", void, ICmpVisual, SetAnimationSyncRepeat, fixed) DEFINE_INTERFACE_METHOD_1("SetAnimationSyncOffset", void, ICmpVisual, SetAnimationSyncOffset, fixed) DEFINE_INTERFACE_METHOD_4("SetShadingColor", void, ICmpVisual, SetShadingColor, fixed, fixed, fixed, fixed) DEFINE_INTERFACE_METHOD_2("SetVariable", void, ICmpVisual, SetVariable, std::string, float) +DEFINE_INTERFACE_METHOD_1("SetMovingSpeed", void, ICmpVisual, SetMovingSpeed, fixed) DEFINE_INTERFACE_METHOD_0("GetActorSeed", u32, ICmpVisual, GetActorSeed) DEFINE_INTERFACE_METHOD_1("SetActorSeed", void, ICmpVisual, SetActorSeed, u32) DEFINE_INTERFACE_METHOD_0("HasConstructionPreview", bool, ICmpVisual, HasConstructionPreview) Index: source/simulation2/helpers/Geometry.h =================================================================== --- source/simulation2/helpers/Geometry.h +++ source/simulation2/helpers/Geometry.h @@ -30,6 +30,21 @@ namespace Geometry { +/* + * Check if we should treat a square as a circle, given the radius + * of the resulting circle and a distance to it + * used by UnitMotion and ObstructionManager + */ +inline bool ShouldTreatTargetAsCircle(const fixed& range, const fixed& circleRadius) +{ + // Given a square, plus a target range we should reach, the shape at that distance + // is a round-cornered square which we can approximate as either a circle or as a square. + // Previously, we used the shape that minimized the worst-case error. + // However that is unsage in some situations. So let's be less clever and + // just check if our range is at least three times bigger than the circleradius + return (range > circleRadius*3); +} + /** * Checks if a point is inside the given rotated rectangle. * Points precisely on an edge are considered to be inside. Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -267,20 +267,46 @@ //////////////////////////////// -JS::Value CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const +JS::Value CMessageBeginMove::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); - SET_MSG_PROPERTY(starting); - SET_MSG_PROPERTY(error); return JS::ObjectValue(*obj); } -CMessage* CMessageMotionChanged::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +CMessage* CMessageBeginMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) { FROMJSVAL_SETUP(); - GET_MSG_PROPERTY(bool, starting); - GET_MSG_PROPERTY(bool, error); - return new CMessageMotionChanged(starting, error); + return new CMessageBeginMove(); +} + +//////////////////////////////// + +JS::Value CMessagePausedMove::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + return JS::ObjectValue(*obj); +} + +CMessage* CMessagePausedMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + return new CMessagePausedMove(); +} + +//////////////////////////////// + +JS::Value CMessageFinishedMove::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(failed); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageFinishedMove::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(bool, failed); + return new CMessageFinishedMove(failed); } //////////////////////////////// Index: source/tools/atlas/GameInterface/ActorViewer.cpp =================================================================== --- source/tools/atlas/GameInterface/ActorViewer.cpp +++ source/tools/atlas/GameInterface/ActorViewer.cpp @@ -375,7 +375,7 @@ { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) - speed = cmpUnitMotion->GetWalkSpeed().ToFloat(); + speed = cmpUnitMotion->GetBaseSpeed().ToFloat(); else speed = 7.f; // typical unit speed @@ -385,7 +385,7 @@ { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) - speed = cmpUnitMotion->GetRunSpeed().ToFloat(); + speed = cmpUnitMotion->GetBaseSpeed().ToFloat(); else speed = 12.f; // typical unit speed