Page MenuHomeWildfire Games

D13.id197.diff
No OneTemporary

Size
400 KB
Referenced Files
None
Subscribers
None

D13.id197.diff

This file is larger than 256 KB, so syntax highlighting was skipped.
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,9 @@
ret.speed = {
"walk": getEntityValue("UnitMotion/WalkSpeed"),
};
- if (template.UnitMotion.Run)
- ret.speed.run = getEntityValue("UnitMotion/Run/Speed");
+ ret.speed.run = getEntityValue("UnitMotion/WalkSpeed");
+ if (template.UnitMotion.RunMultiplier)
+ ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier");
}
if (template.ProductionQueue)
Index: binaries/data/mods/public/gui/session/unit_actions.js
===================================================================
--- binaries/data/mods/public/gui/session/unit_actions.js
+++ binaries/data/mods/public/gui/session/unit_actions.js
@@ -947,10 +947,31 @@
return false;
}
+ // if the target has a static obstruction, move the rallypoint position closer to us
+ // keep this in sync with Rallypoint.js
+ let position = {};
+ let template = GetTemplateData(targetState.template);
+ if (template.obstruction && template.obstruction.shape && template.obstruction.shape.type == "static")
+ {
+ let size = Math.min(+template.obstruction.shape.width, +template.obstruction.shape.depth);
+ let vector = new Vector2D(targetState.position.x-entState.position.x,targetState.position.z-entState.position.z);
+ let pos = new Vector2D(targetState.position.x, targetState.position.z);
+ pos = pos.sub(vector.normalize().mult(size * 0.49));
+ position.x = pos.x;
+ position.z = pos.y;
+ position.y = targetState.position.y;
+ }
+ else
+ {
+ position.x = targetState.position.x;
+ position.z = targetState.position.z;
+ position.y = targetState.position.y;
+ }
+
return {
"possible": true,
"data": data,
- "position": targetState.position,
+ "position": position,
"cursor": cursor,
"tooltip": tooltip
};
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;
+
+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":1.5} ];
+
+cmpTechMgr = QueryPlayerIDInterface(2, IID_TechnologyManager);
+cmpTechMgr.modifications = {};
+cmpTechMgr.modifications['Attack/Ranged/MaxRange'] = [ {"affects":[["Unit"]], "replace":0} ];
+
+Trigger.prototype.setupTests = function()
+{
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ start = cmpTimer.GetTime();
+
+ 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", "becomes":440},
+ "428" : {"target":426, "expectfail":true}, // try to reach unreachable obelisk.
+ "422" : {"target":363}, // Get out of impassable house
+ };
+
+ // 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 cmpResourceSupply = Engine.QueryInterface(tests[test].becomes, IID_ResourceSupply);
+ if (cmpResourceSupply && cmpResourceSupply.GetCurrentAmount() < cmpResourceSupply.GetMaxAmount())
+ Success(test);
+}
+
+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) > 10 || (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,3060 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<Scenario version="6">
+ <Environment>
+ <SkySet>default</SkySet>
+ <SunColor r="0.74902" g="0.74902" b="0.74902"/>
+ <SunElevation angle="0.785398"/>
+ <SunRotation angle="-0.785396"/>
+ <TerrainAmbientColor r="0.501961" g="0.501961" b="0.501961"/>
+ <UnitsAmbientColor r="0.501961" g="0.501961" b="0.501961"/>
+ <Fog>
+ <FogFactor>0</FogFactor>
+ <FogThickness>0.5</FogThickness>
+ <FogColor r="0.8" g="0.8" b="0.894118"/>
+ </Fog>
+ <Water>
+ <WaterBody>
+ <Type>ocean</Type>
+ <Color r="0.294118" g="0.34902" b="0.694118"/>
+ <Tint r="0.27451" g="0.294118" b="0.584314"/>
+ <Height>177.809</Height>
+ <Waviness>4</Waviness>
+ <Murkiness>0.45</Murkiness>
+ <WindAngle>0</WindAngle>
+ </WaterBody>
+ </Water>
+ <Postproc>
+ <Brightness>0</Brightness>
+ <Contrast>1</Contrast>
+ <Saturation>0.99</Saturation>
+ <Bloom>0.1999</Bloom>
+ <PostEffect>default</PostEffect>
+ </Postproc>
+ </Environment>
+ <Camera>
+ <Position x="104.74" y="227.08" z="347.02"/>
+ <Rotation angle="1.82891"/>
+ <Declination angle="0.610865"/>
+ </Camera>
+ <ScriptSettings><![CDATA[
+{
+ "CircularMap": true,
+ "DefaultStance": "standground",
+ "Description": "Automatically test the behavior of the pathfinder using triggers. Should validate a fair amount of test cases.",
+ "GameType": "conquest",
+ "Keywords": [
+ "demo"
+ ],
+ "LockTeams": false,
+ "Name": "Pathfinder Integrated Test",
+ "PlayerData": [
+ null,
+ {
+ "Civ": "pers",
+ "StartingCamera": {
+ "Position": {
+ "x": 146.373,
+ "y": 178.087,
+ "z": 100.526
+ },
+ "Rotation": {
+ "x": 35,
+ "y": 0,
+ "z": 0
+ }
+ }
+ },
+ {
+ "Civ": "cart"
+ }
+ ],
+ "Preview": "",
+ "RevealMap": true,
+ "TriggerScripts": [
+ "scenarios/Pathfinding_integrated_testmap.js"
+ ]
+}
+]]></ScriptSettings>
+ <Entities>
+ <Entity uid="11">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="116.99428" z="113.14905"/>
+ <Orientation y="0.6877"/>
+ <Actor seed="53724"/>
+ </Entity>
+ <Entity uid="12">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="126.32523" z="122.92574"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="15240"/>
+ </Entity>
+ <Entity uid="13">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="151.91141" z="150.21473"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58527"/>
+ </Entity>
+ <Entity uid="14">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="159.5135" z="157.27591"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="19242"/>
+ </Entity>
+ <Entity uid="15">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="171.22568" z="154.61554"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8014"/>
+ </Entity>
+ <Entity uid="16">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="120.17402" z="175.39667"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="3256"/>
+ </Entity>
+ <Entity uid="17">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="129.567" z="165.54151"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="37347"/>
+ </Entity>
+ <Entity uid="18">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="138.04551" z="183.18265"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29900"/>
+ </Entity>
+ <Entity uid="19">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="96.90453" z="132.55277"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29755"/>
+ </Entity>
+ <Entity uid="20">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="87.13465" z="141.63929"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="26092"/>
+ </Entity>
+ <Entity uid="21">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="78.20896" z="123.85108"/>
+ <Orientation y="0.69785"/>
+ <Actor seed="59279"/>
+ </Entity>
+ <Entity uid="22">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="111.90554" z="153.67957"/>
+ <Orientation y="0.88221"/>
+ <Actor seed="33068"/>
+ </Entity>
+ <Entity uid="23">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="158.28577" z="131.82486"/>
+ <Orientation y="0.2562"/>
+ <Actor seed="65510"/>
+ </Entity>
+ <Entity uid="24">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="129.60523" z="64.80619"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="18088"/>
+ </Entity>
+ <Entity uid="25">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="133.30961" z="82.11263"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="2926"/>
+ </Entity>
+ <Entity uid="26">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="140.69623" z="89.18596"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="24192"/>
+ </Entity>
+ <Entity uid="27">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="150.00953" z="84.21305"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="60309"/>
+ </Entity>
+ <Entity uid="28">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="148.56574" z="64.00571"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="20826"/>
+ </Entity>
+ <Entity uid="29">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="159.40867" z="60.49816"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="31642"/>
+ </Entity>
+ <Entity uid="30">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="167.61387" z="66.78667"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="15035"/>
+ </Entity>
+ <Entity uid="31">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="170.38331" z="78.356"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="51931"/>
+ </Entity>
+ <Entity uid="32">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="169.34446" z="91.84455"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="55755"/>
+ </Entity>
+ <Entity uid="33">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="162.29981" z="99.06067"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="49751"/>
+ </Entity>
+ <Entity uid="34">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="140.73398" z="103.39345"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21801"/>
+ </Entity>
+ <Entity uid="35">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="148.13508" z="110.272"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="31936"/>
+ </Entity>
+ <Entity uid="36">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="156.87815" z="115.43648"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21567"/>
+ </Entity>
+ <Entity uid="37">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="169.02717" z="117.98197"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="38978"/>
+ </Entity>
+ <Entity uid="38">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="177.1514" z="111.72628"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58207"/>
+ </Entity>
+ <Entity uid="39">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="184.12409" z="103.21557"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="6320"/>
+ </Entity>
+ <Entity uid="40">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="189.2129" z="94.35817"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29950"/>
+ </Entity>
+ <Entity uid="41">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="189.69529" z="80.76287"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="36701"/>
+ </Entity>
+ <Entity uid="42">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="188.4809" z="69.21586"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="6719"/>
+ </Entity>
+ <Entity uid="43">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="147.3281" z="54.29436"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="49389"/>
+ </Entity>
+ <Entity uid="44">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="143.11338" z="49.43548"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="15805"/>
+ </Entity>
+ <Entity uid="45">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="138.48334" z="46.30921"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="60938"/>
+ </Entity>
+ <Entity uid="46">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="209.8246" z="34.78124"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21041"/>
+ </Entity>
+ <Entity uid="47">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="198.04926" z="64.9746"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="31354"/>
+ </Entity>
+ <Entity uid="48">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="205.43464" z="58.6341"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="57280"/>
+ </Entity>
+ <Entity uid="49">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="42.36638" z="172.1775"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="13636"/>
+ </Entity>
+ <Entity uid="50">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="286.3523" z="80.75289"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="51">
+ <Template>units/maur_ship_fishing</Template>
+ <Player>1</Player>
+ <Position x="457.66211" z="198.84632"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="16286"/>
+ </Entity>
+ <Entity uid="52">
+ <Template>structures/maur_dock</Template>
+ <Player>1</Player>
+ <Position x="384.335" z="224.93381"/>
+ <Orientation y="1.57102"/>
+ <Actor seed="29940"/>
+ </Entity>
+ <Entity uid="53">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="203.14133" z="118.00812"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="14463"/>
+ </Entity>
+ <Entity uid="54">
+ <Template>units/maur_champion_elephant</Template>
+ <Player>1</Player>
+ <Position x="207.01224" z="109.28206"/>
+ <Orientation y="0.86361"/>
+ <Actor seed="11773"/>
+ </Entity>
+ <Entity uid="55">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="210.86857" z="128.45508"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="31946"/>
+ </Entity>
+ <Entity uid="56">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="216.8365" z="134.84971"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56015"/>
+ </Entity>
+ <Entity uid="57">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="223.29291" z="141.76774"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="59406"/>
+ </Entity>
+ <Entity uid="58">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="228.63483" z="135.29209"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29796"/>
+ </Entity>
+ <Entity uid="59">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="234.09375" z="130.0276"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="43474"/>
+ </Entity>
+ <Entity uid="60">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="239.58558" z="125.1192"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="25440"/>
+ </Entity>
+ <Entity uid="61">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="244.42057" z="117.17994"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="63198"/>
+ </Entity>
+ <Entity uid="62">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="250.57703" z="113.66636"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="44781"/>
+ </Entity>
+ <Entity uid="63">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="223.12555" z="108.55082"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="33851"/>
+ </Entity>
+ <Entity uid="64">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="228.64072" z="101.495"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21452"/>
+ </Entity>
+ <Entity uid="65">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="234.8722" z="97.37511"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="36207"/>
+ </Entity>
+ <Entity uid="66">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="242.20047" z="92.56348"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="31460"/>
+ </Entity>
+ <Entity uid="67">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="253.8773" z="89.30027"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21088"/>
+ </Entity>
+ <Entity uid="68">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="262.03443" z="92.89357"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="24266"/>
+ </Entity>
+ <Entity uid="69">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="267.25071" z="97.62075"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="45183"/>
+ </Entity>
+ <Entity uid="70">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="270.82093" z="103.40671"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="71">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="275.73795" z="111.37838"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="49543"/>
+ </Entity>
+ <Entity uid="72">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="274.28107" z="122.85216"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="51547"/>
+ </Entity>
+ <Entity uid="73">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="265.82514" z="134.41843"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="38792"/>
+ </Entity>
+ <Entity uid="74">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="262.4781" z="140.20075"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="40945"/>
+ </Entity>
+ <Entity uid="75">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="258.19712" z="142.55344"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="45530"/>
+ </Entity>
+ <Entity uid="76">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="253.57856" z="148.00525"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54359"/>
+ </Entity>
+ <Entity uid="77">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="248.1597" z="154.98694"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="383"/>
+ </Entity>
+ <Entity uid="78">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="207.58014" z="131.85923"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58980"/>
+ </Entity>
+ <Entity uid="79">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="203.14014" z="134.8762"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="26155"/>
+ </Entity>
+ <Entity uid="80">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="195.66428" z="132.19642"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8060"/>
+ </Entity>
+ <Entity uid="81">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="189.34934" z="125.85277"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="19050"/>
+ </Entity>
+ <Entity uid="82">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="184.72883" z="121.89583"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="20037"/>
+ </Entity>
+ <Entity uid="83">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="180.1879" z="115.41393"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29641"/>
+ </Entity>
+ <Entity uid="84">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="247.15574" z="87.22818"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="33338"/>
+ </Entity>
+ <Entity uid="85">
+ <Template>units/spart_mechanical_siege_ram</Template>
+ <Player>1</Player>
+ <Position x="207.34256" z="95.97799"/>
+ <Orientation y="1.12764"/>
+ <Actor seed="53993"/>
+ </Entity>
+ <Entity uid="86">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="190.93888" z="243.42371"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29172"/>
+ </Entity>
+ <Entity uid="87">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="194.0828" z="246.09668"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="26684"/>
+ </Entity>
+ <Entity uid="88">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="193.97986" z="241.37916"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="53617"/>
+ </Entity>
+ <Entity uid="89">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="196.89478" z="243.13514"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8888"/>
+ </Entity>
+ <Entity uid="90">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="197.00779" z="236.15882"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="57979"/>
+ </Entity>
+ <Entity uid="91">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="199.1406" z="239.02048"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="31200"/>
+ </Entity>
+ <Entity uid="92">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="196.11112" z="239.65625"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="19934"/>
+ </Entity>
+ <Entity uid="93">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="202.07773" z="237.59714"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="5036"/>
+ </Entity>
+ <Entity uid="94">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="203.21146" z="242.09736"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="17974"/>
+ </Entity>
+ <Entity uid="95">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="199.65665" z="246.18772"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="6940"/>
+ </Entity>
+ <Entity uid="96">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="197.39533" z="250.59385"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="38138"/>
+ </Entity>
+ <Entity uid="97">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="201.8985" z="249.88511"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="49538"/>
+ </Entity>
+ <Entity uid="98">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="205.55338" z="246.70054"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="10074"/>
+ </Entity>
+ <Entity uid="99">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="208.28238" z="242.77704"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="17892"/>
+ </Entity>
+ <Entity uid="100">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="207.54432" z="237.74082"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="30357"/>
+ </Entity>
+ <Entity uid="101">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="203.33878" z="236.1963"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="25186"/>
+ </Entity>
+ <Entity uid="102">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="205.88511" z="233.75425"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="55362"/>
+ </Entity>
+ <Entity uid="103">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="201.3722" z="233.05292"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="35435"/>
+ </Entity>
+ <Entity uid="104">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="198.67933" z="234.87409"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="55654"/>
+ </Entity>
+ <Entity uid="105">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="196.87552" z="231.77076"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="7699"/>
+ </Entity>
+ <Entity uid="106">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="201.77347" z="228.45835"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="10534"/>
+ </Entity>
+ <Entity uid="107">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="204.46641" z="230.47407"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="33575"/>
+ </Entity>
+ <Entity uid="108">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="207.41623" z="230.62958"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="17712"/>
+ </Entity>
+ <Entity uid="109">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="208.5156" z="233.42759"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="59197"/>
+ </Entity>
+ <Entity uid="110">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="221.75174" z="205.96744"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="46590"/>
+ </Entity>
+ <Entity uid="111">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="225.74482" z="209.43943"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56295"/>
+ </Entity>
+ <Entity uid="112">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="229.64502" z="212.93116"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="3057"/>
+ </Entity>
+ <Entity uid="113">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="233.06797" z="209.77701"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="2574"/>
+ </Entity>
+ <Entity uid="114">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="229.33945" z="205.73502"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="46416"/>
+ </Entity>
+ <Entity uid="115">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="225.03571" z="201.88035"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="12140"/>
+ </Entity>
+ <Entity uid="116">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="227.83597" z="198.06034"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="65411"/>
+ </Entity>
+ <Entity uid="117">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="232.7146" z="202.7415"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="4518"/>
+ </Entity>
+ <Entity uid="118">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="236.35157" z="205.92267"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="46061"/>
+ </Entity>
+ <Entity uid="119">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="198.81974" z="252.43442"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56785"/>
+ </Entity>
+ <Entity uid="120">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="204.83906" z="253.8581"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="2004"/>
+ </Entity>
+ <Entity uid="121">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="202.90448" z="256.2016"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8213"/>
+ </Entity>
+ <Entity uid="122">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="206.784" z="258.01417"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="31809"/>
+ </Entity>
+ <Entity uid="123">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="205.97403" z="259.89515"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="26684"/>
+ </Entity>
+ <Entity uid="124">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="210.70325" z="261.75062"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="28567"/>
+ </Entity>
+ <Entity uid="125">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="212.15113" z="258.81684"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="41211"/>
+ </Entity>
+ <Entity uid="126">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="208.74546" z="255.39786"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54124"/>
+ </Entity>
+ <Entity uid="127">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="210.41086" z="238.76975"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="63861"/>
+ </Entity>
+ <Entity uid="128">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="211.29767" z="236.1662"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="46381"/>
+ </Entity>
+ <Entity uid="129">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="212.53766" z="233.28663"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="25563"/>
+ </Entity>
+ <Entity uid="130">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="215.8303" z="234.02726"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="4953"/>
+ </Entity>
+ <Entity uid="131">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="214.67975" z="236.69913"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8242"/>
+ </Entity>
+ <Entity uid="132">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="220.55637" z="239.29755"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="52641"/>
+ </Entity>
+ <Entity uid="133">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="219.46464" z="235.19404"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="55932"/>
+ </Entity>
+ <Entity uid="134">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="218.33664" z="237.35447"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="44156"/>
+ </Entity>
+ <Entity uid="135">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="223.98768" z="239.12256"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="39684"/>
+ </Entity>
+ <Entity uid="136">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="224.09763" z="242.34049"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="57856"/>
+ </Entity>
+ <Entity uid="137">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="226.99167" z="242.53522"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="62023"/>
+ </Entity>
+ <Entity uid="138">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="225.80488" z="245.64048"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="48474"/>
+ </Entity>
+ <Entity uid="139">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="229.89371" z="247.53486"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="36926"/>
+ </Entity>
+ <Entity uid="140">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="228.01975" z="249.41843"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="62161"/>
+ </Entity>
+ <Entity uid="141">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="224.7173" z="247.46631"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54193"/>
+ </Entity>
+ <Entity uid="142">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="222.59632" z="244.29978"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="20793"/>
+ </Entity>
+ <Entity uid="143">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="223.22864" z="252.90842"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21673"/>
+ </Entity>
+ <Entity uid="144">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="225.48246" z="252.34327"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="38405"/>
+ </Entity>
+ <Entity uid="145">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="227.20463" z="255.32383"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="1142"/>
+ </Entity>
+ <Entity uid="146">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="223.66486" z="255.88358"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="34807"/>
+ </Entity>
+ <Entity uid="147">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="221.30262" z="257.61103"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="32475"/>
+ </Entity>
+ <Entity uid="148">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="224.7913" z="261.23758"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="50232"/>
+ </Entity>
+ <Entity uid="149">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="226.31061" z="258.97477"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54711"/>
+ </Entity>
+ <Entity uid="150">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="221.78803" z="260.25718"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29861"/>
+ </Entity>
+ <Entity uid="151">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="219.54154" z="263.52695"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="50332"/>
+ </Entity>
+ <Entity uid="152">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="218.31003" z="259.4629"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="11937"/>
+ </Entity>
+ <Entity uid="153">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="213.75754" z="260.60447"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21191"/>
+ </Entity>
+ <Entity uid="154">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="212.42725" z="264.89725"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="49640"/>
+ </Entity>
+ <Entity uid="155">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="215.98826" z="264.00431"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="57016"/>
+ </Entity>
+ <Entity uid="156">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="216.3747" z="260.71518"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="52342"/>
+ </Entity>
+ <Entity uid="157">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="231.85618" z="192.84156"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="59456"/>
+ </Entity>
+ <Entity uid="158">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="238.25641" z="197.03834"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29077"/>
+ </Entity>
+ <Entity uid="159">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="244.56357" z="200.44648"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="5746"/>
+ </Entity>
+ <Entity uid="160">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="242.62641" z="193.63532"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="65228"/>
+ </Entity>
+ <Entity uid="161">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="248.60465" z="197.28156"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="22378"/>
+ </Entity>
+ <Entity uid="162">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="251.4733" z="203.54242"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="33123"/>
+ </Entity>
+ <Entity uid="163">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="254.44474" z="199.58017"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="11628"/>
+ </Entity>
+ <Entity uid="164">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="257.257" z="205.94733"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="60484"/>
+ </Entity>
+ <Entity uid="165">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="252.60871" z="208.8589"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="6297"/>
+ </Entity>
+ <Entity uid="166">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="260.89396" z="204.26175"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="2647"/>
+ </Entity>
+ <Entity uid="167">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="259.39469" z="197.9062"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58843"/>
+ </Entity>
+ <Entity uid="168">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="252.05832" z="193.0829"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="2930"/>
+ </Entity>
+ <Entity uid="169">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="262.47431" z="209.72934"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="24694"/>
+ </Entity>
+ <Entity uid="170">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="257.2992" z="211.58198"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="52293"/>
+ </Entity>
+ <Entity uid="171">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="262.47086" z="215.61213"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="13769"/>
+ </Entity>
+ <Entity uid="172">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="256.80124" z="215.92884"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="32872"/>
+ </Entity>
+ <Entity uid="173">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="250.70847" z="213.81824"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="43747"/>
+ </Entity>
+ <Entity uid="174">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="258.08265" z="221.4026"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="27780"/>
+ </Entity>
+ <Entity uid="175">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="252.59055" z="219.04422"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21168"/>
+ </Entity>
+ <Entity uid="176">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="254.16626" z="225.85517"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="16650"/>
+ </Entity>
+ <Entity uid="177">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="248.61008" z="223.09272"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="61893"/>
+ </Entity>
+ <Entity uid="178">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="245.72101" z="217.84897"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="12749"/>
+ </Entity>
+ <Entity uid="179">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="246.23038" z="228.49851"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58641"/>
+ </Entity>
+ <Entity uid="180">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="243.3931" z="222.86136"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="1929"/>
+ </Entity>
+ <Entity uid="181">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="240.80683" z="217.57154"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="50984"/>
+ </Entity>
+ <Entity uid="182">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="238.66736" z="223.11762"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="53495"/>
+ </Entity>
+ <Entity uid="183">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="232.41871" z="218.32932"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="18572"/>
+ </Entity>
+ <Entity uid="184">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="236.46683" z="218.50068"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="35195"/>
+ </Entity>
+ <Entity uid="185">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="236.51157" z="213.33008"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="50174"/>
+ </Entity>
+ <Entity uid="186">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="186.95698" z="222.53113"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="187">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="218.19248" z="182.31162"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="41066"/>
+ </Entity>
+ <Entity uid="188">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="244.6052" z="209.29834"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="47081"/>
+ </Entity>
+ <Entity uid="189">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="214.50922" z="250.34629"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="46445"/>
+ </Entity>
+ <Entity uid="190">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="166.09076" z="165.38892"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56443"/>
+ </Entity>
+ <Entity uid="191">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="148.29004" z="196.75351"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="30578"/>
+ </Entity>
+ <Entity uid="192">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="99.7923" z="146.25318"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="17773"/>
+ </Entity>
+ <Entity uid="193">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="107.77678" z="227.56638"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="19051"/>
+ </Entity>
+ <Entity uid="194">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="179.3169" z="49.7893"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="61"/>
+ </Entity>
+ <Entity uid="195">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="255.35276" z="23.49827"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="26582"/>
+ </Entity>
+ <Entity uid="196">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="302.51703" z="88.46801"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="44678"/>
+ </Entity>
+ <Entity uid="199">
+ <Template>units/spart_support_female_citizen</Template>
+ <Player>2</Player>
+ <Position x="290.43445" z="23.92124"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="35716"/>
+ </Entity>
+ <Entity uid="200">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="109.43511" z="77.0546"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="63168"/>
+ </Entity>
+ <Entity uid="201">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="93.04517" z="86.69541"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="29946"/>
+ </Entity>
+ <Entity uid="202">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="102.81905" z="95.08095"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="205">
+ <Template>structures/maur_civil_centre</Template>
+ <Player>1</Player>
+ <Position x="301.95817" z="163.82706"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="53258"/>
+ </Entity>
+ <Entity uid="206">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="201.16895" z="253.78647"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56785"/>
+ </Entity>
+ <Entity uid="207">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="204.79084" z="257.46131"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56785"/>
+ </Entity>
+ <Entity uid="208">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="208.73603" z="260.35712"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56785"/>
+ </Entity>
+ <Entity uid="209">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="197.16544" z="247.61756"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56785"/>
+ </Entity>
+ <Entity uid="210">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="70.0528" z="242.62473"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="211">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="37.11925" z="275.8459"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="212">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="108.74282" z="281.38636"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="19051"/>
+ </Entity>
+ <Entity uid="213">
+ <Template>actor|props/special/common/waypoint_flag_factions.xml</Template>
+ <Position x="67.8909" z="311.90143"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="19051"/>
+ </Entity>
+ <Entity uid="214">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="94.46123" z="283.9875"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="13444"/>
+ </Entity>
+ <Entity uid="215">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="97.13433" z="281.7209"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21724"/>
+ </Entity>
+ <Entity uid="216">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="99.69603" z="279.54874"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58556"/>
+ </Entity>
+ <Entity uid="217">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="102.45533" z="276.74885"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="17674"/>
+ </Entity>
+ <Entity uid="218">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="104.9764" z="274.15372"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58466"/>
+ </Entity>
+ <Entity uid="219">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="107.85462" z="271.7132"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="50214"/>
+ </Entity>
+ <Entity uid="220">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="110.84353" z="269.17884"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="23750"/>
+ </Entity>
+ <Entity uid="221">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="94.19433" z="280.8613"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="6138"/>
+ </Entity>
+ <Entity uid="222">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="96.7375" z="278.451"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54643"/>
+ </Entity>
+ <Entity uid="223">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="99.27411" z="275.92234"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="224">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="102.01227" z="273.35071"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="413"/>
+ </Entity>
+ <Entity uid="225">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="104.61889" z="270.76868"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="654"/>
+ </Entity>
+ <Entity uid="226">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="107.83507" z="267.7957"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="10163"/>
+ </Entity>
+ <Entity uid="227">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="110.99157" z="265.11921"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54336"/>
+ </Entity>
+ <Entity uid="228">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="111.51264" z="261.27842"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="15247"/>
+ </Entity>
+ <Entity uid="229">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="107.8984" z="264.56785"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8123"/>
+ </Entity>
+ <Entity uid="230">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="104.86733" z="267.70603"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="50192"/>
+ </Entity>
+ <Entity uid="231">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="102.1906" z="270.32071"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="23933"/>
+ </Entity>
+ <Entity uid="232">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="99.15328" z="273.24436"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="25576"/>
+ </Entity>
+ <Entity uid="233">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="96.43897" z="275.77982"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="14139"/>
+ </Entity>
+ <Entity uid="234">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="94.21474" z="277.90119"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="41520"/>
+ </Entity>
+ <Entity uid="235">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="104.89683" z="295.56464"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="41520"/>
+ </Entity>
+ <Entity uid="236">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="107.12107" z="293.44327"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="14139"/>
+ </Entity>
+ <Entity uid="237">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="109.83538" z="290.90781"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="25576"/>
+ </Entity>
+ <Entity uid="238">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="112.87269" z="287.98417"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="23933"/>
+ </Entity>
+ <Entity uid="239">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="115.54943" z="285.36948"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="50192"/>
+ </Entity>
+ <Entity uid="240">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="118.5805" z="282.2313"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="8123"/>
+ </Entity>
+ <Entity uid="241">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="122.19474" z="278.94187"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="15247"/>
+ </Entity>
+ <Entity uid="242">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="121.67367" z="282.78266"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="54336"/>
+ </Entity>
+ <Entity uid="243">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="118.51716" z="285.45914"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="10163"/>
+ </Entity>
+ <Entity uid="244">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="115.30098" z="288.43213"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="654"/>
+ </Entity>
+ <Entity uid="245">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="112.69436" z="291.01417"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="413"/>
+ </Entity>
+ <Entity uid="246">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="109.9562" z="293.5858"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="247">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="107.4196" z="296.11445"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="54643"/>
+ </Entity>
+ <Entity uid="248">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="104.87642" z="298.52475"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="6138"/>
+ </Entity>
+ <Entity uid="249">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="121.52562" z="286.8423"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="23750"/>
+ </Entity>
+ <Entity uid="250">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="118.53672" z="289.37665"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="50214"/>
+ </Entity>
+ <Entity uid="251">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="115.6585" z="291.81717"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="58466"/>
+ </Entity>
+ <Entity uid="252">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="113.13743" z="294.4123"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="17674"/>
+ </Entity>
+ <Entity uid="253">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="110.37812" z="297.2122"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="58556"/>
+ </Entity>
+ <Entity uid="254">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="107.81643" z="299.38434"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21724"/>
+ </Entity>
+ <Entity uid="255">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="105.14332" z="301.65094"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="13444"/>
+ </Entity>
+ <Entity uid="256">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="125.17428" z="279.748"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="41520"/>
+ </Entity>
+ <Entity uid="257">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="123.76183" z="277.01804"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="14139"/>
+ </Entity>
+ <Entity uid="258">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="122.08936" z="273.7016"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="25576"/>
+ </Entity>
+ <Entity uid="259">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="120.13488" z="269.96625"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="23933"/>
+ </Entity>
+ <Entity uid="260">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="118.37583" z="266.66361"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="50192"/>
+ </Entity>
+ <Entity uid="261">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="116.21366" z="262.87409"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="8123"/>
+ </Entity>
+ <Entity uid="262">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="114.06987" z="258.4823"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="15247"/>
+ </Entity>
+ <Entity uid="263">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="117.6104" z="260.05961"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="54336"/>
+ </Entity>
+ <Entity uid="264">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="119.2942" z="263.84012"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="10163"/>
+ </Entity>
+ <Entity uid="265">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="121.24589" z="267.76102"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="654"/>
+ </Entity>
+ <Entity uid="266">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="122.99326" z="270.98719"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="413"/>
+ </Entity>
+ <Entity uid="267">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="124.69376" z="274.33667"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="268">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="126.40954" z="277.48066"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="54643"/>
+ </Entity>
+ <Entity uid="269">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="128.00986" z="280.59772"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="6138"/>
+ </Entity>
+ <Entity uid="270">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="121.46558" z="261.34025"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="23750"/>
+ </Entity>
+ <Entity uid="271">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="123.05999" z="264.92002"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="50214"/>
+ </Entity>
+ <Entity uid="272">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="124.59534" z="268.3672"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="58466"/>
+ </Entity>
+ <Entity uid="273">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="126.37928" z="271.51493"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="17674"/>
+ </Entity>
+ <Entity uid="274">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="128.29294" z="274.94874"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="58556"/>
+ </Entity>
+ <Entity uid="275">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="129.6595" z="278.01679"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="21724"/>
+ </Entity>
+ <Entity uid="276">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="131.08545" z="281.2183"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="13444"/>
+ </Entity>
+ <Entity uid="277">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="114.72202" z="266.40778"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="54336"/>
+ </Entity>
+ <Entity uid="278">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="103.43018" z="305.64631"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="13444"/>
+ </Entity>
+ <Entity uid="279">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="102.00425" z="302.4448"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="21724"/>
+ </Entity>
+ <Entity uid="280">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="100.63767" z="299.37674"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="58556"/>
+ </Entity>
+ <Entity uid="281">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="98.72402" z="295.94294"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="17674"/>
+ </Entity>
+ <Entity uid="282">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="96.94007" z="292.7952"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="58466"/>
+ </Entity>
+ <Entity uid="283">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="95.40473" z="289.34803"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="50214"/>
+ </Entity>
+ <Entity uid="284">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="93.81032" z="285.76825"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="23750"/>
+ </Entity>
+ <Entity uid="285">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="97.0105" z="298.79203"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="286">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="95.338" z="295.4152"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="413"/>
+ </Entity>
+ <Entity uid="287">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="93.59063" z="292.18903"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="654"/>
+ </Entity>
+ <Entity uid="288">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="91.63894" z="288.26813"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="10163"/>
+ </Entity>
+ <Entity uid="289">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="89.95513" z="284.48761"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="54336"/>
+ </Entity>
+ <Entity uid="290">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="86.4146" z="282.91031"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="15247"/>
+ </Entity>
+ <Entity uid="291">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="88.5584" z="287.3021"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="8123"/>
+ </Entity>
+ <Entity uid="292">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="90.72057" z="291.09162"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="50192"/>
+ </Entity>
+ <Entity uid="293">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="102.35131" z="297.11313"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="294">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="98.44636" z="302.15339"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="295">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="100.22297" z="305.70109"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="296">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="91.75399" z="282.69007"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="297">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="91.60926" z="279.68921"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="298">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="89.37792" z="281.17963"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="40345"/>
+ </Entity>
+ <Entity uid="299">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="50.95327" z="308.0561"/>
+ <Orientation y="3.64275"/>
+ <Actor seed="15247"/>
+ </Entity>
+ <Entity uid="300">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="52.44278" z="307.01026"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="28834"/>
+ </Entity>
+ <Entity uid="301">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="53.75377" z="306.08976"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="42578"/>
+ </Entity>
+ <Entity uid="302">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="54.85178" z="305.3188"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="16450"/>
+ </Entity>
+ <Entity uid="303">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="56.09418" z="304.44648"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="57377"/>
+ </Entity>
+ <Entity uid="304">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="57.08967" z="303.7475"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="36823"/>
+ </Entity>
+ <Entity uid="305">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="58.3329" z="302.87458"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="63884"/>
+ </Entity>
+ <Entity uid="306">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="59.52201" z="302.03965"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="9450"/>
+ </Entity>
+ <Entity uid="307">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="61.34264" z="300.7613"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="46832"/>
+ </Entity>
+ <Entity uid="308">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="63.53142" z="299.2245"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="9404"/>
+ </Entity>
+ <Entity uid="309">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="66.02125" z="297.47626"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="12909"/>
+ </Entity>
+ <Entity uid="310">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="67.69877" z="296.29841"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8102"/>
+ </Entity>
+ <Entity uid="311">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="68.86612" z="295.47876"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54654"/>
+ </Entity>
+ <Entity uid="312">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="62.6601" z="299.82746"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="32967"/>
+ </Entity>
+ <Entity uid="313">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="64.77782" z="298.27186"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="53116"/>
+ </Entity>
+ <Entity uid="314">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="60.27758" z="300.9073"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="61395"/>
+ </Entity>
+ <Entity uid="315">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="62.5118" z="309.48072"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="27800"/>
+ </Entity>
+ <Entity uid="316">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="63.50264" z="308.35593"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="30612"/>
+ </Entity>
+ <Entity uid="317">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="64.20629" z="307.88053"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="14420"/>
+ </Entity>
+ <Entity uid="318">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="65.10946" z="307.2974"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="32392"/>
+ </Entity>
+ <Entity uid="319">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="66.13093" z="306.28098"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="14963"/>
+ </Entity>
+ <Entity uid="320">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="66.91781" z="305.85694"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="21763"/>
+ </Entity>
+ <Entity uid="321">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="67.80614" z="305.31061"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="59866"/>
+ </Entity>
+ <Entity uid="322">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="68.65952" z="304.81629"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="40719"/>
+ </Entity>
+ <Entity uid="323">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="69.86922" z="304.02643"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="35948"/>
+ </Entity>
+ <Entity uid="324">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="70.84334" z="303.08802"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="35302"/>
+ </Entity>
+ <Entity uid="325">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="71.87186" z="302.39112"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="3297"/>
+ </Entity>
+ <Entity uid="326">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="73.00805" z="301.56473"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="4680"/>
+ </Entity>
+ <Entity uid="327">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="73.79863" z="301.02878"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="20883"/>
+ </Entity>
+ <Entity uid="328">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="74.54925" z="300.54847"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="12640"/>
+ </Entity>
+ <Entity uid="329">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="75.21677" z="299.69632"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="42537"/>
+ </Entity>
+ <Entity uid="330">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="76.26575" z="298.35721"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="65159"/>
+ </Entity>
+ <Entity uid="331">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="76.98096" z="297.44416"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="10828"/>
+ </Entity>
+ <Entity uid="332">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="77.74386" z="296.47028"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="24424"/>
+ </Entity>
+ <Entity uid="333">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="78.36371" z="295.679"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="61371"/>
+ </Entity>
+ <Entity uid="334">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="78.93589" z="294.94855"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="35371"/>
+ </Entity>
+ <Entity uid="335">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="79.74645" z="293.9138"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="1005"/>
+ </Entity>
+ <Entity uid="336">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="75.77483" z="298.6648"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="59282"/>
+ </Entity>
+ <Entity uid="337">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="51.18438" z="307.1743"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="6293"/>
+ </Entity>
+ <Entity uid="338">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="50.1051" z="308.5521"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="54669"/>
+ </Entity>
+ <Entity uid="339">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="49.19982" z="308.80588"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="55570"/>
+ </Entity>
+ <Entity uid="340">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="48.20208" z="309.85627"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="12597"/>
+ </Entity>
+ <Entity uid="341">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="46.61724" z="310.12333"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="15768"/>
+ </Entity>
+ <Entity uid="342">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="45.43012" z="310.78043"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="1134"/>
+ </Entity>
+ <Entity uid="343">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="44.38561" z="310.64222"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56649"/>
+ </Entity>
+ <Entity uid="344">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="42.83" z="310.79108"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="32685"/>
+ </Entity>
+ <Entity uid="345">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="42.11101" z="310.71402"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="42456"/>
+ </Entity>
+ <Entity uid="346">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="41.35587" z="310.31498"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="41087"/>
+ </Entity>
+ <Entity uid="347">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="40.51606" z="310.0575"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="61165"/>
+ </Entity>
+ <Entity uid="348">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="39.69665" z="309.80628"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="59049"/>
+ </Entity>
+ <Entity uid="349">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="39.11811" z="309.45771"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="30847"/>
+ </Entity>
+ <Entity uid="350">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="37.86055" z="308.10114"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="62614"/>
+ </Entity>
+ <Entity uid="351">
+ <Template>units/spart_infantry_javelinist_b</Template>
+ <Player>1</Player>
+ <Position x="43.51003" z="310.5286"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="39129"/>
+ </Entity>
+ <Entity uid="352">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="82.7549" z="103.94994"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="353">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="100.76827" z="146.49372"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="354">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="149.26026" z="195.9813"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="355">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="108.71425" z="226.7664"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="356">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="165.4586" z="165.79068"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="357">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="109.17454" z="280.715"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="358">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="68.65073" z="311.06974"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="359">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="214.69037" z="250.07444"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="360">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="244.65384" z="209.50562"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="361">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="179.16755" z="49.91823"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="362">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="278.94672" z="141.0105"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="363">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="302.45875" z="88.63253"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="364">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="255.47968" z="22.65091"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="365">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="332.82953" z="230.64237"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="366">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="105.13955" z="123.6118"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="45585"/>
+ </Entity>
+ <Entity uid="367">
+ <Template>units/maur_support_female_citizen</Template>
+ <Player>1</Player>
+ <Position x="90.91407" z="138.28257"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="4903"/>
+ </Entity>
+ <Entity uid="368">
+ <Template>units/maur_support_female_citizen</Template>
+ <Player>1</Player>
+ <Position x="91.60128" z="137.63764"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56611"/>
+ </Entity>
+ <Entity uid="369">
+ <Template>units/maur_support_female_citizen</Template>
+ <Player>1</Player>
+ <Position x="92.09063" z="136.83777"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56292"/>
+ </Entity>
+ <Entity uid="370">
+ <Template>units/maur_support_female_citizen</Template>
+ <Player>1</Player>
+ <Position x="92.88012" z="136.09687"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="44620"/>
+ </Entity>
+ <Entity uid="371">
+ <Template>units/maur_support_female_citizen</Template>
+ <Player>1</Player>
+ <Position x="93.35381" z="135.65232"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="47066"/>
+ </Entity>
+ <Entity uid="372">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="189.28925" z="220.4015"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="373">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="110.20167" z="91.79204"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="374">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="114.30687" z="85.49352"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="375">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="115.85663" z="73.69336"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="376">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="111.79397" z="64.49341"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="377">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="106.88592" z="57.87948"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="378">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="107.898" z="58.35094"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="379">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="102.56771" z="55.92226"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="380">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="101.38032" z="57.76226"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="381">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="88.80338" z="80.78415"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="382">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="85.26334" z="79.57323"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="383">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="82.11007" z="78.8916"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="384">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="103.33787" z="57.29607"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="385">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="103.26822" z="57.87948"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="386">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="103.60804" z="59.0662"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="387">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="103.5248" z="59.79133"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="388">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="103.56608" z="60.28031"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="389">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="103.90128" z="60.77382"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="62415"/>
+ </Entity>
+ <Entity uid="390">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="162.47272" z="325.90351"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="391">
+ <Template>foundation|structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="170.39988" z="334.92743"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="49149"/>
+ </Entity>
+ <Entity uid="392">
+ <Template>gaia/fauna_chicken</Template>
+ <Player>1</Player>
+ <Position x="199.3602" z="323.73578"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="15723"/>
+ </Entity>
+ <Entity uid="393">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="175.24616" z="303.81006"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="394">
+ <Template>structures/maur_civil_centre</Template>
+ <Player>1</Player>
+ <Position x="193.08902" z="369.9571"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8658"/>
+ </Entity>
+ <Entity uid="395">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="230.78193" z="319.36368"/>
+ <Orientation y="3.2774"/>
+ <Actor seed="37347"/>
+ </Entity>
+ <Entity uid="396">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="240.86271" z="305.57941"/>
+ <Orientation y="3.27741"/>
+ <Actor seed="37347"/>
+ </Entity>
+ <Entity uid="397">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="225.7303" z="303.62302"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="398">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="248.91565" z="324.6703"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="399">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="145.42533" z="343.02836"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="400">
+ <Template>gaia/flora_bush_berry</Template>
+ <Player>1</Player>
+ <Position x="156.80604" z="360.47648"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8778"/>
+ </Entity>
+ <Entity uid="401">
+ <Template>structures/maur_market</Template>
+ <Player>1</Player>
+ <Position x="247.78837" z="465.83191"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="56973"/>
+ </Entity>
+ <Entity uid="402">
+ <Template>structures/maur_market</Template>
+ <Player>1</Player>
+ <Position x="343.0363" z="266.44574"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="10021"/>
+ </Entity>
+ <Entity uid="403">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="298.7859" z="308.2422"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="49433"/>
+ </Entity>
+ <Entity uid="404">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="309.3412" z="317.75733"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="64150"/>
+ </Entity>
+ <Entity uid="405">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="317.88569" z="329.58295"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="43442"/>
+ </Entity>
+ <Entity uid="406">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="324.30011" z="342.27"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="9142"/>
+ </Entity>
+ <Entity uid="407">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="311.73139" z="352.90595"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="58990"/>
+ </Entity>
+ <Entity uid="408">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="305.42536" z="344.1583"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="26360"/>
+ </Entity>
+ <Entity uid="409">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="292.84614" z="326.97944"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="25688"/>
+ </Entity>
+ <Entity uid="410">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="287.36478" z="324.41456"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="20565"/>
+ </Entity>
+ <Entity uid="411">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="282.82917" z="339.47413"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="16200"/>
+ </Entity>
+ <Entity uid="412">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="291.91498" z="346.06644"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="33488"/>
+ </Entity>
+ <Entity uid="413">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="296.02833" z="354.90741"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="41461"/>
+ </Entity>
+ <Entity uid="414">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="300.1566" z="357.95047"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="19518"/>
+ </Entity>
+ <Entity uid="415">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="303.1753" z="379.5766"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="34917"/>
+ </Entity>
+ <Entity uid="416">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="290.7129" z="384.20331"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="35097"/>
+ </Entity>
+ <Entity uid="417">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="289.07398" z="365.24726"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="24110"/>
+ </Entity>
+ <Entity uid="418">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="270.73581" z="355.9163"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="16268"/>
+ </Entity>
+ <Entity uid="419">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="269.77643" z="367.38431"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="63088"/>
+ </Entity>
+ <Entity uid="420">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="275.44272" z="373.9405"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="8770"/>
+ </Entity>
+ <Entity uid="421">
+ <Template>units/maur_support_trader</Template>
+ <Player>1</Player>
+ <Position x="281.0394" z="395.01688"/>
+ <Orientation y="2.35621"/>
+ <Actor seed="23073"/>
+ </Entity>
+ <Entity uid="422">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="345.84263" z="103.35616"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="423">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="170.37449" z="335.6861"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ <Entity uid="426">
+ <Template>other/obelisk</Template>
+ <Player>1</Player>
+ <Position x="306.32566" z="27.91451"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="61423"/>
+ </Entity>
+ <Entity uid="427">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="316.68445" z="38.4499"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="428">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="325.30619" z="31.26777"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="429">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="345.90961" z="103.1294"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="430">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="377.74369" z="92.24572"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="431">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="383.42154" z="86.796"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="432">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="367.5583" z="84.06911"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="433">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="373.39695" z="77.88918"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="434">
+ <Template>gaia/flora_tree_cretan_date_palm_tall</Template>
+ <Player>1</Player>
+ <Position x="381.55201" z="78.87566"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="2647"/>
+ </Entity>
+ <Entity uid="435">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="378.23035" z="70.90313"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="436">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="387.04218" z="71.55874"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="437">
+ <Template>structures/maur_house</Template>
+ <Player>1</Player>
+ <Position x="389.24646" z="81.32763"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="56518"/>
+ </Entity>
+ <Entity uid="438">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="375.42475" z="85.0598"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="439">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="373.94166" z="86.57753"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="440">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="372.4004" z="88.15477"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="441">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="370.78223" z="89.81075"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="442">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="369.10245" z="91.46508"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="65509"/>
+ </Entity>
+ <Entity uid="443">
+ <Template>units/maur_infantry_archer_b</Template>
+ <Player>1</Player>
+ <Position x="160.8505" z="324.71143"/>
+ <Orientation y="2.35622"/>
+ <Actor seed="21638"/>
+ </Entity>
+ </Entities>
+ <Paths/>
+</Scenario>
\ No newline at end of file
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,10 @@
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(),
+ "tryingToMove" : cmpUnitMotion.IsTryingToMove()
};
return ret;
Index: binaries/data/mods/public/simulation/components/RallyPoint.js
===================================================================
--- binaries/data/mods/public/simulation/components/RallyPoint.js
+++ binaries/data/mods/public/simulation/components/RallyPoint.js
@@ -23,6 +23,8 @@
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ let cmpOurPosition = Engine.QueryInterface(this.entity, IID_Position);
// We must not affect the simulation state here (modifications of the
// RallyPointRenderer are allowed though), so copy the state
@@ -49,13 +51,31 @@
if (!targetPosition)
continue;
- if (this.pos[i].x == targetPosition.x && this.pos[i].z == targetPosition.y)
+ // if the target has a static obstruction, move the rallypoint position closer to us
+ // keep this in sync with unit_actions.js
+ let position = {};
+ let entityTemplateName = cmpTemplateManager.GetCurrentTemplateName(this.data[i].target);
+ let targetTemplate = cmpTemplateManager.GetTemplate(entityTemplateName);
+ if (targetTemplate.Obstruction && targetTemplate.Obstruction.Static)
+ {
+ let ourPosition = cmpOurPosition.GetPosition2D();
+ let size = Math.min(+targetTemplate.Obstruction.Static["@width"], +targetTemplate.Obstruction.Static["@depth"]);
+ let vector = new Vector2D(targetPosition.x-ourPosition.x,targetPosition.y-ourPosition.y);
+ let pos = new Vector2D(targetPosition.x, targetPosition.y);
+ pos = pos.sub(vector.normalize().mult(size * 0.49));
+ position.x = pos.x;
+ position.y = pos.y;
+ }
+ else
+ position = targetPosition;
+
+ if (this.pos[i].x == position.x && this.pos[i].z == position.y)
continue;
- ret[i] = { "x": targetPosition.x, "z": targetPosition.y };
+ ret[i] = { "x": position.x, "z": position.y };
var cmpRallyPointRenderer = Engine.QueryInterface(this.entity, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
- cmpRallyPointRenderer.UpdatePosition(i, targetPosition);
+ cmpRallyPointRenderer.UpdatePosition(i, position);
}
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 =
@@ -219,14 +221,14 @@
}
// Move a tile outside the building
let range = 4;
- if (this.MoveToTargetRangeExplicit(msg.data.target, range, range))
+ if (this.MoveToTargetRangeExplicit(msg.data.target, range))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
- // We are already at the target, or can't move at all
+ // We can't reach the target
this.FinishOrder();
}
},
@@ -276,7 +278,7 @@
if (!this.order.data.max)
this.MoveToPoint(this.order.data.x, this.order.data.z);
else
- this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max);
+ this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min + this.order.data.max) / 2.0);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
@@ -339,7 +341,7 @@
}
else
{
- // We are already at the target, or can't move at all
+ // We can't reach the target
this.StopMoving();
this.FinishOrder();
}
@@ -372,7 +374,7 @@
}
else
{
- // We are already at the target, or can't move at all
+ // We can't reach the target
this.StopMoving();
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
}
@@ -385,7 +387,7 @@
return;
}
- if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+ if (this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange, true))
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
@@ -395,7 +397,7 @@
// We use the distance between the entities to account for ranged attacks
var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1))
+ if (cmpUnitMotion.SetNewDestinationAsEntity(this.order.data.target, distance, true))
{
// We've started fleeing from the given target
if (this.IsAnimal())
@@ -405,7 +407,7 @@
}
else
{
- // We are already at the target, or can't move at all
+ // We can't reach the target
this.StopMoving();
this.FinishOrder();
}
@@ -617,7 +619,7 @@
}
else
{
- // We are already at the target, or can't move at all,
+ // We can't reach the target.
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
@@ -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
@@ -694,7 +696,7 @@
}
else
{
- // We are already at the target, or can't move at all,
+ // We can't reach the target.
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
@@ -827,7 +829,7 @@
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
- if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
+ if (this.MoveToTargetRangeExplicit(this.order.data.target, (this.order.data.min+this.order.data.max)/2.0))
this.SetNextState("WALKING");
else
this.FinishOrder();
@@ -841,7 +843,7 @@
},
"Order.WalkToPointRange": function(msg) {
- if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max))
+ if (this.MoveToPointRange(this.order.data.x, this.order.data.z, (this.order.data.min + this.order.data.max)/2.0))
this.SetNextState("WALKING");
else
this.FinishOrder();
@@ -1071,7 +1073,7 @@
cmpFormation.MoveMembersIntoFormation(true, true);
},
- "MoveCompleted": function(msg) {
+ "MoveSucess": function(msg) {
if (this.FinishOrder())
this.CallMemberFunction("ResetFinishOrder", []);
},
@@ -1364,14 +1366,14 @@
}
// Move a tile outside the building
let range = 4;
- if (this.MoveToTargetRangeExplicit(msg.data.target, range, range))
+ if (this.MoveToTargetRangeExplicit(msg.data.target, range))
{
// We've started walking to the given point
this.SetNextState("WALKINGTOPOINT");
}
else
{
- // We are already at the target, or can't move at all
+ // We can't reach the target.
this.FinishOrder();
}
},
@@ -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() {
@@ -1593,6 +1593,11 @@
},
"Timer": function(msg) {
+ // bit of a sanity check, but this happening would most likely mean a bug somewhere.
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ if (cmpUnitMotion && cmpUnitMotion.IsTryingToMove())
+ warn("Entity " + this.entity + " is in the idle state but trying to move");
+
if (!this.isIdle)
{
this.isIdle = true;
@@ -1603,10 +1608,10 @@
"WALKING": {
"enter": function () {
- this.SelectAnimation("move");
},
"MoveCompleted": function() {
+ this.StopMoving();
this.FinishOrder();
},
},
@@ -1614,10 +1619,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) {
@@ -1629,6 +1633,7 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
this.FinishOrder();
},
},
@@ -1649,7 +1654,6 @@
}
this.StartTimer(0, 1000);
- this.SelectAnimation("move");
},
"leave": function() {
@@ -1662,6 +1666,7 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
if (this.orderQueue.length == 1)
this.PushOrder("Patrol",this.patrolStartPosOrder);
@@ -1679,10 +1684,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;
},
@@ -1696,31 +1700,33 @@
return;
}
this.SetHeldPositionOnEntity(this.isGuardOf);
- },
- "leave": function(msg) {
- this.SetMoveSpeed(this.GetWalkSpeed());
- this.StopTimer();
- },
-
- "MoveStarted": function(msg) {
// Adapt the speed to the one of the target if needed
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange))
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ if (cmpObstructionManager.IsInTargetRange(this.entity, 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);
}
}
},
+ "leave": function(msg) {
+ this.SetMoveSpeed(WALKING_SPEED);
+ this.StopTimer();
+ },
+
"MoveCompleted": function() {
- this.SetMoveSpeed(this.GetWalkSpeed());
- if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+ this.StopMoving();
+ this.SetMoveSpeed(WALKING_SPEED);
+ if (!this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange))
this.SetNextState("GUARDING");
},
},
@@ -1747,7 +1753,8 @@
return;
}
// then check is the target has moved
- if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
+ // TODO: this should call isInRange, not this.
+ if (this.MoveToTargetRangeExplicit(this.isGuardOf, this.guardRange))
this.SetNextState("ESCORTING");
else
{
@@ -1774,23 +1781,17 @@
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() {
// When we've run far enough, stop fleeing
+ this.StopMoving();
this.FinishOrder();
},
@@ -1811,15 +1812,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();
},
@@ -1834,10 +1834,19 @@
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
+ else if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
+ {
+ this.StopMoving();
+ // If the unit needs to unpack, do so
+ if (this.CanUnpack())
+ this.SetNextState("UNPACKING");
+ else
+ this.SetNextState("ATTACKING");
+ }
},
"MoveCompleted": function() {
-
+ this.StopMoving();
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// If the unit needs to unpack, do so
@@ -1970,6 +1979,7 @@
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(0);
this.StopTimer();
+ this.SelectAnimation("idle");
},
"Timer": function(msg) {
@@ -2096,16 +2106,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 +2119,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();
},
@@ -2136,9 +2143,19 @@
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
+ else if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
+ {
+ this.StopMoving();
+ // If the unit needs to unpack, do so
+ if (this.CanUnpack())
+ this.SetNextState("UNPACKING");
+ else
+ this.SetNextState("ATTACKING");
+ }
},
"MoveCompleted": function() {
+ this.StopMoving();
this.SetNextState("ATTACKING");
},
},
@@ -2147,7 +2164,6 @@
"GATHER": {
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
@@ -2207,10 +2223,12 @@
}
return true;
}
+ this.StartTimer(0, 500);
return false;
},
"MoveCompleted": function(msg) {
+ this.StopMoving();
if (msg.data.error)
{
// We failed to reach the target
@@ -2258,7 +2276,16 @@
this.SetNextState("GATHERING");
},
+ "Timer": function() {
+ if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
+ {
+ this.StopMoving();
+ this.SetNextState("GATHERING");
+ }
+ },
+
"leave": function() {
+ this.StopTimer();
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
@@ -2271,10 +2298,10 @@
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
- this.SelectAnimation("move");
},
"MoveCompleted": function(msg) {
+ this.StopMoving();
var resourceType = this.order.data.type;
var resourceTemplate = this.order.data.template;
@@ -2368,6 +2395,7 @@
// off to a different target.)
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
+ this.FaceTowardsTarget(this.gatheringTarget);
var typename = "gather_" + this.order.data.type.specific;
this.SelectAnimation(typename, false, 1.0, typename);
}
@@ -2385,7 +2413,8 @@
delete this.gatheringTarget;
// Show the carried resource, if we've gathered anything.
- this.SetGathererAnimationOverride();
+ this.SelectAnimation("idle");
+ this.SetAnimationVariant();
},
"Timer": function(msg) {
@@ -2455,10 +2484,9 @@
// the old one. So try to get close to the old resource's
// last known position
- var maxRange = 8; // get close but not too close
+ var range = 4; // get close but not too close
if (this.order.data.lastPos &&
- this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
- 0, maxRange))
+ this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, range))
{
this.SetNextState("APPROACHING");
return;
@@ -2533,7 +2561,6 @@
"APPROACHING": {
"enter": function () {
- this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
@@ -2554,6 +2581,7 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
this.SetNextState("HEALING");
},
},
@@ -2584,6 +2612,7 @@
},
"leave": function() {
+ this.SelectAnimation("idle");
this.StopTimer();
},
@@ -2634,7 +2663,6 @@
},
"CHASING": {
"enter": function () {
- this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
@@ -2653,6 +2681,7 @@
}
},
"MoveCompleted": function () {
+ this.StopMoving();
this.SetNextState("HEALING");
},
},
@@ -2662,19 +2691,41 @@
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function () {
- this.SelectAnimation("move");
+ this.StartTimer(0, 1000);
},
- "MoveCompleted": function() {
- // Switch back to idle animation to guarantee we won't
- // get stuck with the carry animation after stopping moving
- this.SelectAnimation("idle");
+ "leave": function() {
+ this.StopTimer();
+ },
+ "Timer": function() {
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
- if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
+ if (!this.CanReturnResource(this.order.data.target, true))
+ {
+ this.StopMoving();
+ // The dropsite was destroyed, or we couldn't reach it, or ownership changed
+ // Look for a new one.
+
+ var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
+ var genericType = cmpResourceGatherer.GetMainCarryingType();
+ var nearby = this.FindNearestDropsite(genericType);
+ if (nearby)
+ {
+ this.FinishOrder();
+ this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
+ return;
+ }
+
+ // Oh no, couldn't find any drop sites. Give up on returning.
+ this.FinishOrder();
+ return;
+ }
+ if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
{
+ this.StopMoving();
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
+ // this ought to be redundant with the above check.
if (cmpResourceDropsite)
{
// Dump any resources we can
@@ -2684,7 +2735,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
@@ -2692,23 +2743,9 @@
return;
}
}
-
- // The dropsite was destroyed, or we couldn't reach it, or ownership changed
- // Look for a new one.
-
- var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
- var genericType = cmpResourceGatherer.GetMainCarryingType();
- var nearby = this.FindNearestDropsite(genericType);
- if (nearby)
- {
- this.FinishOrder();
- this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
- return;
- }
-
- // Oh no, couldn't find any drop sites. Give up on returning.
- this.FinishOrder();
},
+
+ "MoveCompleted": "Timer",
},
},
@@ -2720,10 +2757,10 @@
"APPROACHINGMARKET": {
"enter": function () {
- this.SelectAnimation("move");
},
"MoveCompleted": function() {
+ this.StopMoving();
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.target))
@@ -2748,10 +2785,23 @@
"REPAIR": {
"APPROACHING": {
"enter": function () {
- this.SelectAnimation("move");
+ this.StartTimer(0, 1000);
},
- "MoveCompleted": function() {
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "Timer": function() {
+ if (this.CheckTargetRange(this.order.data.target, IID_Builder))
+ {
+ this.StopMoving();
+ this.SetNextState("REPAIRING");
+ }
+ },
+ // TODO: clean this up when MoveCompleted becomes MoveSuccesHint and MoveFailure or something
+ "MoveCompleted": function(msg) {
+ this.StopMoving();
this.SetNextState("REPAIRING");
},
},
@@ -2796,6 +2846,8 @@
if (cmpBuilderList)
cmpBuilderList.AddBuilder(this.entity);
+ this.FaceTowardsTarget(this.repairTarget);
+
this.SelectAnimation("build", false, 1.0, "build");
this.StartTimer(1000, 1000);
return false;
@@ -2806,6 +2858,7 @@
if (cmpBuilderList)
cmpBuilderList.RemoveBuilder(this.entity);
delete this.repairTarget;
+ this.SelectAnimation("idle");
this.StopTimer();
},
@@ -2825,10 +2878,13 @@
// in that case, the repairTarget is deleted, and we can just return
if (!this.repairTarget)
return;
- if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
- this.SetNextState("APPROACHING");
- else if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
- this.FinishOrder(); //can't approach and isn't in reach
+ if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
+ {
+ if (this.MoveToTargetRange(this.repairTarget, IID_Builder))
+ this.SetNextState("APPROACHING");
+ else
+ this.FinishOrder(); //can't approach and isn't in reach
+ }
},
},
@@ -2852,7 +2908,7 @@
{
let dropsiteTypes = cmpResourceDropsite.GetTypes();
cmpResourceGatherer.CommitResources(dropsiteTypes);
- this.SetGathererAnimationOverride();
+ this.SetAnimationVariant();
}
// We finished building it.
@@ -2861,7 +2917,7 @@
{
if (this.CanReturnResource(msg.data.newentity, true))
{
- this.SetGathererAnimationOverride();
+ this.SetAnimationVariant();
this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false });
}
return;
@@ -2880,7 +2936,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,10 +3001,10 @@
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
},
"MoveCompleted": function() {
+ this.StopMoving();
if (this.IsUnderAlert() && this.alertGarrisoningTarget)
{
// check that we can garrison in the building we're supposed to garrison in
@@ -3026,7 +3082,7 @@
if (cmpResourceGatherer)
{
cmpResourceGatherer.CommitResources(dropsiteTypes);
- this.SetGathererAnimationOverride();
+ this.SetAnimationVariant();
}
}
@@ -3102,6 +3158,7 @@
this.StopTimer();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(false);
+ this.SelectAnimation("idle");
},
"Timer": function(msg) {
@@ -3148,10 +3205,10 @@
"PICKUP": {
"APPROACHING": {
"enter": function() {
- this.SelectAnimation("move");
},
"MoveCompleted": function() {
+ this.StopMoving();
this.SetNextState("LOADING");
},
@@ -3202,14 +3259,14 @@
"Order.LeaveFoundation": function(msg) {
// Move a tile outside the building
var range = 4;
- if (this.MoveToTargetRangeExplicit(msg.data.target, range, range))
+ if (this.MoveToTargetRangeExplicit(msg.data.target, range))
{
// We've started walking to the given point
this.SetNextState("WALKING");
}
else
{
- // We are already at the target, or can't move at all
+ // We cannot reach the target.
this.FinishOrder();
}
},
@@ -3227,7 +3284,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));
@@ -3266,6 +3322,7 @@
},
"MoveCompleted": function() {
+ this.StopMoving();
this.MoveRandomly(+this.template.RoamDistance);
},
},
@@ -3298,7 +3355,7 @@
}
},
- "MoveCompleted": function() { },
+ "MoveCompleted": function() { this.StopMoving(); },
"Timer": function(msg) {
this.SetNextState("ROAMING");
@@ -3683,6 +3740,9 @@
error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
}
+ // Safety net, in general it's better if unitAI states handle this properly.
+ this.StopMoving();
+
this.orderQueue.shift();
this.order = this.orderQueue[0];
@@ -4010,12 +4070,14 @@
//// Message handlers /////
-UnitAI.prototype.OnMotionChanged = function(msg)
+UnitAI.prototype.OnMoveSuccess = 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": "MoveCompleted", "data": { "error" : false }});
+};
+
+UnitAI.prototype.OnMoveFailure = function(msg)
+{
+ this.UnitFsm.ProcessMessage(this, { "type": "MoveCompleted", "data": { "error" : true }});
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
@@ -4076,22 +4138,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 +4327,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;
+ }
- // Special case for meat
- if (type.specific == "meat")
- typename = "carry_" + type.specific;
+ let type = cmpResourceGatherer.GetLastCarriedType();
+ if (type)
+ {
+ let typename = "carry_" + type.generic;
+
+ // 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 +4371,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,31 +4403,34 @@
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);
- return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
+ cmpUnitMotion.SetAbortIfStuck(3);
+ return cmpUnitMotion.SetNewDestinationAsPosition(x, z, 0, true);
};
-UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
+UnitAI.prototype.MoveToPointRange = function(x, z, range)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
+ cmpUnitMotion.SetAbortIfStuck(3);
+ return cmpUnitMotion.SetNewDestinationAsPosition(x, z, range, true);
};
-UnitAI.prototype.MoveToTarget = function(target)
+UnitAI.prototype.MoveToTarget = function(target, evenUnreachable = false)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, 0, evenUnreachable);
};
-UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
+UnitAI.prototype.MoveToTargetRange = function(target, iid, type, evenUnreachable = false)
{
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
@@ -4394,7 +4441,9 @@
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ // generally speaking, try to aim for the middle of a range.
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, evenUnreachable);
};
/**
@@ -4402,7 +4451,7 @@
* for melee attacks, this goes straight to the default range checks
* for ranged attacks, the parabolic range is used
*/
-UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
+UnitAI.prototype.MoveToTargetAttackRange = function(target, type, evenUnreachable = false)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
@@ -4417,7 +4466,7 @@
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
- return this.MoveToTargetRange(target, IID_Attack, type);
+ return this.MoveToTargetRange(target, IID_Attack, type, evenUnreachable);
if (!this.CheckTargetVisible(target))
return false;
@@ -4449,21 +4498,24 @@
// the parabole changes while walking, take something in the middle
var guessedMaxRange = (range.max + parabolicMaxRange)/2;
+// TODO: here we should give the desired range based on unit speed, our own desire to walk, and so on.
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange))
+ cmpUnitMotion.SetAbortIfStuck(9);
+ if (cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + guessedMaxRange)/2.0, false))
return true;
// if that failed, try closer
- return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange));
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + Math.min(range.max, parabolicMaxRange))/2.0, evenUnreachable);
};
-UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
+UnitAI.prototype.MoveToTargetRangeExplicit = function(target, range, evenUnreachable = false)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, min, max);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, range, evenUnreachable);
};
UnitAI.prototype.MoveToGarrisonRange = function(target)
@@ -4477,13 +4529,14 @@
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+ cmpUnitMotion.SetAbortIfStuck(5);
+ return cmpUnitMotion.SetNewDestinationAsEntity(target, (range.min + range.max)/2.0, false); // presumably we always want to know if we can't garrison here
};
UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
{
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.IsInPointRange(x, z, min, max);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max);
};
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
@@ -4492,9 +4545,8 @@
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
-
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max);
};
/**
@@ -4542,14 +4594,14 @@
if (maxRangeSq < 0)
return false;
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq));
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, Math.sqrt(maxRangeSq));
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.IsInTargetRange(target, min, max);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max);
};
UnitAI.prototype.CheckGarrisonRange = function(target)
@@ -4563,8 +4615,8 @@
if (cmpObstruction)
range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
+ let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
+ return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max);
};
/**
@@ -5122,7 +5174,7 @@
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
- this.MoveToTargetRange(target, IID_Heal);
+ this.MoveToTargetRange(target, IID_Heal, true);
else
this.WalkToTarget(target, queued);
return;
@@ -5693,8 +5745,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(speed);
};
UnitAI.prototype.SetHeldPosition = function(x, z)
@@ -5999,7 +6051,7 @@
var tz = pos.z + (2*Math.random()-1)*jitter;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
- cmpMotion.MoveToPointRange(tx, tz, distance, distance);
+ cmpMotion.SetNewDestinationAsPosition(tx, tz, distance, true);
};
UnitAI.prototype.SetFacePointAfterMove = function(val)
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
@@ -241,33 +241,43 @@
cmpPosition.MoveTo(pos.x, pos.z);
};
-UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange)
+UnitMotionFlying.prototype.SetNewDestinationAsPosition = function(x, z, range)
{
this.hasTarget = true;
this.landing = false;
this.reachedTarget = false;
this.targetX = x;
this.targetZ = z;
- this.targetMinRange = minRange;
- this.targetMaxRange = maxRange;
+ this.targetMinRange = range;
+ this.targetMaxRange = range;
+
+ // 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;
};
-UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange)
+UnitMotionFlying.prototype.SetNewDestinationAsEntity = function(target, range)
{
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
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;
this.reachedTarget = false;
this.targetX = targetPos.x;
this.targetZ = targetPos.y;
- this.targetMinRange = minRange;
- this.targetMaxRange = maxRange;
+ this.targetMinRange = range;
+ this.targetMaxRange = range;
return 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/helpers/FSM.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/FSM.js
+++ binaries/data/mods/public/simulation/helpers/FSM.js
@@ -69,6 +69,10 @@
"leave": function() {
// Called when transitioning out of this state.
},
+
+ // Define a message handler that is an exact copy of another
+ // message handler defined in this Substate
+ "SomeMessageName": "AnotherMessageName",
},
// Define a new state which is an exact copy of another
@@ -194,16 +198,38 @@
{
state[key] = node[key];
}
- else if (key.match(/^[A-Z]+$/))
+ else if (typeof node[key] === "function")
+ {
+ // New message handler
+ newhandlers[key] = node[key];
+ }
+ else if (typeof node[key] === "object")
{
+ // new substate
state._refs[key] = (state._name ? state._name + "." : "") + key;
// (the rest of this will be handled later once we've grabbed
// all the event handlers)
}
- else
+ else if (typeof node[key] === "string")
{
- newhandlers[key] = node[key];
+ // this can be either a reference to a message handler, or to a state.
+ if (!node[node[key]] && !fsm.states[node[key]])
+ {
+ error("FSM node " + path.join(".") + " node " + key + " referring to unknown node/state " + node[key]);
+ return {};
+ }
+ else if (!!node[node[key]] && !!fsm.states[node[key]])
+ {
+ error("FSM node " + path.join(".") + " node " + key + " ambiguously referring to message handler or state " + node[key]);
+ return {};
+ }
+ if (!node[node[key]])
+ // new substate
+ state._refs[key] = (state._name ? state._name + "." : "") + key;
+ else
+ // New message handler
+ newhandlers[key] = node[node[key]];
}
}
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>3.0</WalkSpeed>
- <Run>
- <Speed>9.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/camel.xml</Actor>
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 @@
</UnitAI>
<UnitMotion>
<WalkSpeed>1.0</WalkSpeed>
- <Run>
- <Speed>6.0</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/chicken.xml</Actor>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>2.0</WalkSpeed>
- <Run>
- <Speed>18.0</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>20</Range>
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 @@
</Position>
<UnitMotion>
<WalkSpeed>2.0</WalkSpeed>
- <Run>
- <Speed>10.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/deer.xml</Actor>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>3.0</WalkSpeed>
- <Run>
- <Speed>10.0</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/elephant_african_forest.xml</Actor>
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 @@
<Position>
<Anchor>pitch</Anchor>
</Position>
- <UnitMotion>
- <Run>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
- </UnitMotion>
<VisualActor>
<Actor>fauna/gazelle.xml</Actor>
</VisualActor>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>4.0</WalkSpeed>
- <Run>
- <Speed>12.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/giraffe_adult.xml</Actor>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>4.0</WalkSpeed>
- <Run>
- <Speed>10.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/giraffe_baby.xml</Actor>
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 @@
<TurnRate>1.0</TurnRate>
</Position>
<UnitAI>
- <RoamDistance>1000.0</RoamDistance>
+ <RoamDistance>1000.0</RoamDistance>
</UnitAI>
<UnitMotion disable=""/>
<UnitMotionFlying>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>5.0</WalkSpeed>
- <Run>
- <Speed>12.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/horse_a.xml</Actor>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>3.0</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/lion.xml</Actor>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>3.0</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/lioness.xml</Actor>
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 @@
<UnitMotion>
<PassabilityClass>ship-small</PassabilityClass>
<WalkSpeed>4.0</WalkSpeed>
- <Run>
- <Speed>35.0</Speed>
- </Run>
</UnitMotion>
<Visibility>
<RetainInFog>false</RetainInFog>
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 @@
</ResourceSupply>
<UnitMotion>
<WalkSpeed>6.0</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/wildebeest.xml</Actor>
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 @@
</ResourceSupply>
<UnitMotion>
<WalkSpeed>6.0</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>fauna/zebra.xml</Actor>
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
@@ -66,7 +66,7 @@
</Obstruction>
<OverlayRenderer/>
<ResourceGatherer>
- <MaxDistance>2.0</MaxDistance>
+ <MaxDistance>1.0</MaxDistance>
<BaseSpeed>1.0</BaseSpeed>
<Rates>
<treasure>1</treasure>
@@ -106,13 +106,6 @@
<UnitMotion>
<FormationController>false</FormationController>
<WalkSpeed>9</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- <Range>50.0</Range>
- <RangeMin>0.0</RangeMin>
- <RegenTime>0.1</RegenTime>
- <DecayTime>0.2</DecayTime>
- </Run>
<PassabilityClass>default</PassabilityClass>
</UnitMotion>
<Visibility>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>16.5</WalkSpeed>
- <Run>
- <Speed>26.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
+ <RunMultiplier>2.5</RunMultiplier>
</UnitMotion>
<Vision>
<Range>92</Range>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>22.0</WalkSpeed>
- <Run>
- <Speed>40.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Loot>
<UnitMotion>
<WalkSpeed>20.0</WalkSpeed>
- <Run>
- <Speed>28.75</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>17.5</WalkSpeed>
- <Run>
- <Speed>28.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>17.5</WalkSpeed>
- <Run>
- <Speed>28.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>16.5</WalkSpeed>
- <Run>
- <Speed>26.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>10.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>96</Range>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>20.5</WalkSpeed>
- <Run>
- <Speed>28.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>10.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>20.5</WalkSpeed>
- <Run>
- <Speed>28.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>10.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>25.0</WalkSpeed>
- <Run>
- <Speed>40.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>23.0</WalkSpeed>
- <Run>
- <Speed>40.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
<UnitMotion>
<PassabilityClass>large</PassabilityClass>
<WalkSpeed>8.5</WalkSpeed>
- <Run>
- <Speed>14.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>10.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>100</Range>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>8.5</WalkSpeed>
- <Run>
- <Speed>17.5</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>84</Range>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>11.0</WalkSpeed>
- <Run>
- <Speed>18.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>16.0</WalkSpeed>
- <Run>
- <Speed>18.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>7.0</WalkSpeed>
- <Run>
- <Speed>13.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>11.5</WalkSpeed>
- <Run>
- <Speed>23.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>12.5</WalkSpeed>
- <Run>
- <Speed>16.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>14.5</WalkSpeed>
- <Run>
- <Speed>26.0</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
+ <RunMultiplier>2.5</RunMultiplier>
</UnitMotion>
<Vision>
<Range>30</Range>
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 @@
</UnitAI>
<UnitMotion>
<WalkSpeed>6.5</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- </Run>
+ <RunMultiplier>2.0</RunMultiplier>
</UnitMotion>
<Visibility>
<RetainInFog>true</RetainInFog>
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 @@
<UnitMotion>
<PassabilityClass>ship-small</PassabilityClass>
<WalkSpeed>11.5</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- </Run>
+ <RunMultiplier>1.0</RunMultiplier>
</UnitMotion>
</Entity>
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 @@
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>10.5</WalkSpeed>
- <Run>
- <Speed>22.5</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>88</Range>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>16.5</WalkSpeed>
- <Run>
- <Speed>26.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>16.0</RangeMin>
- </Run>
+ <RunMultiplier>2.5</RunMultiplier>
</UnitMotion>
<Vision>
<Range>100</Range>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>17.0</WalkSpeed>
- <Run>
- <Speed>28.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>16.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>8.5</WalkSpeed>
- <Run>
- <Speed>14.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>10.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>80</Range>
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 @@
<death>actor/human/death/{gender}_death.xml</death>
</SoundGroups>
</Sound>
+ <UnitMotion>
+ <RunMultiplier>1.6</RunMultiplier>
+ </UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>8.5</WalkSpeed>
- <Run>
- <Speed>17.5</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>9</WalkSpeed>
- <Run>
- <Speed>18.75</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed>9.5</WalkSpeed>
- <Run>
- <Speed>20.0</Speed>
- <Range>600.0</Range>
- <RangeMin>8.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>80</Range>
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
@@ -74,7 +74,7 @@
<RequiredXp>100</RequiredXp>
</Promotion>
<ResourceGatherer>
- <MaxDistance>2.0</MaxDistance>
+ <MaxDistance>1.0</MaxDistance>
<BaseSpeed>1.0</BaseSpeed>
<Rates>
<food.fruit>0.5</food.fruit>
@@ -119,9 +119,7 @@
</Sound>
<UnitMotion>
<WalkSpeed>9</WalkSpeed>
- <Run>
- <Speed>18.75</Speed>
- </Run>
+ <RunMultiplier>1.6</RunMultiplier>
</UnitMotion>
<Vision>
<Range>80</Range>
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 @@
</Loot>
<UnitMotion>
<WalkSpeed>6.0</WalkSpeed>
- <Run>
- <Speed>8.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Loot>
<UnitMotion>
<WalkSpeed>8.5</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Loot>
<UnitMotion>
<WalkSpeed>9.5</WalkSpeed>
- <Run>
- <Speed>16.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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>attack/weapon/arrowfly.xml</attack>
</SoundGroups>
</Sound>
+ <UnitMotion>
+ <RunMultiplier>2.0</RunMultiplier>
+ </UnitMotion>
</Entity>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>8.0</WalkSpeed>
- <Run>
- <Speed>18.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Loot>
<UnitMotion>
<WalkSpeed>13.5</WalkSpeed>
- <Run>
- <Speed>24.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Loot>
<UnitMotion>
<WalkSpeed>11.0</WalkSpeed>
- <Run>
- <Speed>24.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
<Repairable>
<RepairTimeRatio>4.0</RepairTimeRatio>
</Repairable>
+ <UnitMotion>
+ <RunMultiplier>1.0</RunMultiplier>
+ </UnitMotion>
</Entity>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>14</WalkSpeed>
- <Run>
- <Speed>18.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
<UnitMotion>
<PassabilityClass>ship-small</PassabilityClass>
<WalkSpeed>17.5</WalkSpeed>
- <Run>
- <Speed>22.0</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>60</Range>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>16</WalkSpeed>
- <Run>
- <Speed>20</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>110</Range>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>16</WalkSpeed>
- <Run>
- <Speed>20.0</Speed>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Selectable>
<UnitMotion>
<WalkSpeed>8</WalkSpeed>
- <Run>
- <Speed>12.0</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>120</Range>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>7</WalkSpeed>
- <Run>
- <Speed>10.0</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>120</Range>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>8</WalkSpeed>
- <Run>
- <Speed>11.0</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>80</Range>
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 @@
</StatusBars>
<UnitMotion>
<WalkSpeed>6.5</WalkSpeed>
- <Run>
- <Speed>10.0</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>80</Range>
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 @@
<UnitAI>
<DefaultStance>passive</DefaultStance>
</UnitAI>
+ <UnitMotion>
+ <RunMultiplier>2.0</RunMultiplier>
+ </UnitMotion>
</Entity>
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
@@ -48,7 +48,7 @@
<Formations disable=""/>
</Identity>
<ResourceGatherer>
- <MaxDistance>2.0</MaxDistance>
+ <MaxDistance>1.0</MaxDistance>
<BaseSpeed>1.0</BaseSpeed>
<Rates>
<food.fruit>1</food.fruit>
@@ -87,11 +87,6 @@
</UnitAI>
<UnitMotion>
<WalkSpeed>9.5</WalkSpeed>
- <Run>
- <Speed>16.0</Speed>
- <Range>200.0</Range>
- <RangeMin>0.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>32</Range>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>9</WalkSpeed>
- <Run>
- <Speed>12.0</Speed>
- <Range>200.0</Range>
- <RangeMin>0.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>30</Range>
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
@@ -49,7 +49,7 @@
<metal>1</metal>
</Loot>
<ResourceGatherer>
- <MaxDistance>2.0</MaxDistance>
+ <MaxDistance>1.0</MaxDistance>
<BaseSpeed>1.0</BaseSpeed>
<Rates>
<food.fish>0.5</food.fish>
@@ -85,10 +85,5 @@
</Sound>
<UnitMotion>
<WalkSpeed>8</WalkSpeed>
- <Run>
- <Speed>15.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>8.0</RangeMin>
- </Run>
</UnitMotion>
</Entity>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/athenians/siege_rock.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/athenians/siege_spear.xml</Actor>
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 @@
</Promotion>
<UnitMotion>
<WalkSpeed>15.5</WalkSpeed>
- <Run>
- <Speed>24.0</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/celts/cavalry_swordsman_b.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/carthaginians/siege_rock.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/carthaginians/siege_spear.xml</Actor>
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 @@
</Identity>
<UnitMotion>
<WalkSpeed op="add">5</WalkSpeed>
- <Run>
- <Speed op="mul">1.5</Speed>
- <Range>600.0</Range>
- <RangeMin>5.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/celts/fanatic.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/hellenes/siege_rock.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/hellenes/siege_spear.xml</Actor>
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 @@
<UnitMotion>
<PassabilityClass>large</PassabilityClass>
<WalkSpeed>8.5</WalkSpeed>
- <Run>
- <Speed>14.0</Speed>
- <Range>1000.0</Range>
- <RangeMin>10.0</RangeMin>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/mauryans/elephant_archer_b.xml</Actor>
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 @@
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>9</WalkSpeed>
- <Run>
- <Speed>12.0</Speed>
- <Range>200.0</Range>
- <RangeMin>0.0</RangeMin>
- </Run>
</UnitMotion>
<Vision>
<Range>30</Range>
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 @@
<UnitMotion>
<PassabilityClass>large</PassabilityClass>
<WalkSpeed>5.5</WalkSpeed>
- <Run>
- <Speed>10.0</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>50</Range>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/hellenes/siege_rock.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/ptolemies/siege_spear.xml</Actor>
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 @@
</Sound>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/romans/siege_rock.xml</Actor>
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 @@
</Cost>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<Vision>
<Range>88</Range>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/romans/siege_scorpio.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/hellenes/siege_rock.xml</Actor>
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 @@
</Pack>
<UnitMotion>
<WalkSpeed>0.001</WalkSpeed>
- <Run>
- <Speed>0.001</Speed>
- </Run>
</UnitMotion>
<VisualActor>
<Actor>units/athenians/siege_spear.xml</Actor>
Index: source/simulation2/MessageTypes.h
===================================================================
--- source/simulation2/MessageTypes.h
+++ source/simulation2/MessageTypes.h
@@ -317,21 +317,83 @@
};
/**
- * 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 failed; // move failed
+};
+
+/**
+ * Sent by CCmpUnitMotion as a hint when we have reached our destination
+ * The unit may start moving again on its own despite this message being sent.
+ */
+class CMessageMoveSuccess : public CMessage
+{
+public:
+ DEFAULT_MESSAGE_IMPL(MoveSuccess)
+
+ CMessageMoveSuccess()
+ {
+ }
+};
+
+/**
+ * Sent by CCmpUnitMotion when a unit has determined it has no chance
+ * of ever reaching its assigned destination. This is a catastrophic error.
+ */
+class CMessageMoveFailure : public CMessage
+{
+public:
+ DEFAULT_MESSAGE_IMPL(MoveFailure)
+
+ CMessageMoveFailure()
+ {
+ }
+};
- bool starting; // whether this is a start or end of movement
- bool error; // whether we failed to start moving (couldn't find any path)
+/**
+ * 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,11 @@
MESSAGE(PositionChanged)
MESSAGE(InterpolatedPositionChanged)
MESSAGE(TerritoryPositionChanged)
-MESSAGE(MotionChanged)
+MESSAGE(BeginMove)
+MESSAGE(FinishedMove)
+MESSAGE(MoveSuccess)
+MESSAGE(MoveFailure)
+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"
@@ -313,7 +314,7 @@
{
CFixedVector2D u(entity_pos_t::FromInt(1), entity_pos_t::Zero());
CFixedVector2D v(entity_pos_t::Zero(), entity_pos_t::FromInt(1));
- ObstructionSquare o = { x, z, u, v, clearance, clearance };
+ ObstructionSquare o = { x, z, u, v, clearance, fixed::Zero() };
return o;
}
@@ -465,6 +466,13 @@
}
}
+ virtual bool IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange);
+ virtual bool IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
+ virtual bool IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange);
+ virtual bool IsPointInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
+
+ bool AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& 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<entity_id_t>* out);
virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector<entity_id_t>* out);
@@ -658,6 +666,138 @@
REGISTER_COMPONENT_TYPE(ObstructionManager)
+////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////
+// Is In Range family of functions. Those either end up in IsPointInPointRange or AreShapesInRange
+
+bool CCmpObstructionManager::IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange)
+{
+ CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent);
+ if (!cmpPosition)
+ return false;
+
+ ObstructionSquare obstruction;
+ CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent);
+ if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction))
+ return IsPointInPointRange(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, px, pz, minRange, maxRange);
+
+ ObstructionSquare point;
+ point.x = px;
+ point.z = pz;
+ return AreShapesInRange(obstruction, point, minRange, maxRange);
+}
+
+bool CCmpObstructionManager::IsInTargetRange(entity_id_t ent, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+{
+ CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent);
+ if (!cmpPosition)
+ return false;
+
+ CmpPtr<ICmpPosition> cmpPositionTarget(GetSimContext(), target);
+ if (!cmpPositionTarget)
+ return false;
+
+ ObstructionSquare obstruction;
+ CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), ent);
+ if (!cmpObstruction || !cmpObstruction->GetObstructionSquare(obstruction))
+ return IsPointInTargetRange(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, target, minRange, maxRange);
+
+ ObstructionSquare target_obstruction;
+ CmpPtr<ICmpObstruction> cmpObstructionTarget(GetSimContext(), target);
+ if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction))
+ return IsInPointRange(ent, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y, minRange, maxRange);
+
+ return AreShapesInRange(obstruction, target_obstruction, minRange, maxRange);
+}
+
+bool CCmpObstructionManager::IsPointInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+{
+ ObstructionSquare point;
+ point.x = x;
+ point.z = z;
+
+ CmpPtr<ICmpPosition> cmpPositionTarget(GetSimContext(), target);
+ if (!cmpPositionTarget)
+ return false;
+
+ ObstructionSquare target_obstruction;
+ CmpPtr<ICmpObstruction> cmpObstructionTarget(GetSimContext(), target);
+ if (!cmpObstructionTarget || !cmpObstructionTarget->GetObstructionSquare(target_obstruction))
+ return IsPointInPointRange(x, z, cmpPositionTarget->GetPosition2D().X, cmpPositionTarget->GetPosition2D().Y, minRange, maxRange);
+
+ return AreShapesInRange(point, target_obstruction, minRange, maxRange);
+}
+
+// trivial case
+bool CCmpObstructionManager::IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange)
+{
+ CFixedVector2D pos(x, z);
+
+ entity_pos_t distance = (pos - CFixedVector2D(px, pz)).Length() - Pathfinding::NAVCELL_SIZE / 2; // be a little permissive
+
+ if (distance < minRange)
+ return false;
+ else if (maxRange >= entity_pos_t::Zero() && distance > maxRange)
+ return false;
+ else
+ return true;
+}
+
+// hard case
+bool CCmpObstructionManager::AreShapesInRange(const ObstructionSquare& source, const ObstructionSquare& target, entity_pos_t minRange, entity_pos_t maxRange)
+{
+ // In this function, we will give about a navcell worth of leeway to avoid weirdness
+ fixed navcellFix = Pathfinding::NAVCELL_SIZE * 3 / 2; // a little above √2, it's not important.
+
+ if (source.hh == fixed::Zero() && target.hh == fixed::Zero())
+ {
+ // sphere-sphere collision.
+ // Source is in range if the edge to edge distance is inferior to maxRange
+ // and the opposite edge to opposite edge distance is bigger than minRange
+ // TODO: figure out whether we actually want that
+ fixed distance = (CFixedVector2D(target.x, target.z) - CFixedVector2D(source.x, source.z)).Length();
+
+ if (distance - source.hw - target.hw - navcellFix > maxRange)
+ return false;
+ if (distance + source.hw + target.hw + navcellFix < minRange)
+ return false;
+ return true;
+ }
+ else if (source.hh != fixed::Zero() && target.hh != fixed::Zero())
+ {
+ // square to square
+ // TODO: implement this.
+ LOGWARNING("square-square range tests not yet implemented");
+ return false;
+ }
+ else
+ {
+ // to cover both remaining cases, shape a is the square one, shape b is the circular one.
+ const ObstructionSquare& a = source.hh == fixed::Zero() ? target : source;
+ const ObstructionSquare& b = source.hh == fixed::Zero() ? source : target;
+
+ CFixedVector2D relativePosition = CFixedVector2D(b.x, b.z) - CFixedVector2D(a.x, a.z);
+ fixed distance = Geometry::DistanceToSquare(relativePosition, a.u, a.v, CFixedVector2D(a.hw, a.hh), true);
+
+ // in range if the edge to edge distance is inferior to maxRange
+ // and if the opposite edge to opposite edge distance is more than Minrange
+ // This means for example that a unit is in range of a building if it is farther than clearance-buildingsize,
+ // which is generally going to be negative (and thus this returns true).
+ // NB: since calculating the opposite-edge distance of a square is annoying, we'll add min(hw,hh) instead which is OK enough I guess
+ // NB: from a game POV, this means units can easily fire on buildings, which is good, but it also means that buildings can easily fire on units
+ // Buildings are usually meant to fire from the edge, not the opposite edge, so this looks odd.
+ // I don't really see an easy way to fix this tbh. Depending on the case, the JS code should call
+ // IsPointInTargetRange with the center/correct position (so the real distance is counted)
+ // or just add the min(hw,hh) to the minRange of the building.
+ if (distance - b.hw - navcellFix > maxRange)
+ return false;
+ if (distance + b.hw + std::min(a.hw, a.hh) < minRange)
+ return false;
+ return true;
+ }
+}
+
+
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.cpp
===================================================================
--- source/simulation2/components/CCmpPathfinder.cpp
+++ source/simulation2/components/CCmpPathfinder.cpp
@@ -668,6 +668,35 @@
return m_ObstructionsDirty;
}
+bool CCmpPathfinder::MakeGoalReachable(entity_pos_t x0, entity_pos_t z0, PathGoal &goal, pass_class_t passClass)
+{
+ u16 i0, j0;
+ Pathfinding::NearestNavcell(x0, z0, i0, j0, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE);
+ if (!IS_PASSABLE(m_Grid->get(i0, j0), passClass))
+ FindNearestPassableNavcell(i0, j0, passClass);
+
+ return m_LongPathfinder.MakeGoalReachable(i0, j0, goal, passClass);
+}
+
+u32 CCmpPathfinder::FindNearestPassableNavcell(entity_pos_t x, entity_pos_t z, u16& outI, u16& outJ, pass_class_t passClass)
+{
+ Pathfinding::NearestNavcell(x, z, outI, outJ, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE);
+ u16 i0 = outI;
+ u16 j0 = outJ;
+ FindNearestPassableNavcell(outI, outJ, passClass);
+ return abs(i0 - outI) + abs(j0 - outJ);
+}
+
+void CCmpPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass)
+{
+ m_LongPathfinder.FindNearestPassableNavcell(i, j, passClass);
+}
+
+bool CCmpPathfinder::NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass)
+{
+ return m_LongPathfinder.NavcellIsReachable(i0, j0, i1, j1, passClass);
+}
+
//////////////////////////////////////////////////////////
// Async path requests:
Index: source/simulation2/components/CCmpPathfinder_Common.h
===================================================================
--- source/simulation2/components/CCmpPathfinder_Common.h
+++ source/simulation2/components/CCmpPathfinder_Common.h
@@ -237,6 +237,13 @@
virtual Grid<u16> ComputeShoreGrid(bool expandOnWater = false);
+ virtual bool MakeGoalReachable(entity_pos_t x0, entity_pos_t z0, PathGoal &goal, pass_class_t passClass);
+
+ virtual u32 FindNearestPassableNavcell(entity_pos_t x, entity_pos_t z, u16& outI, u16& outJ, pass_class_t passClass);
+ void FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass);
+
+ virtual bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass);
+
virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass, WaypointPath& ret)
{
m_LongPathfinder.ComputePath(x0, z0, goal, passClass, ret);
Index: source/simulation2/components/CCmpPathfinder_Vertex.cpp
===================================================================
--- source/simulation2/components/CCmpPathfinder_Vertex.cpp
+++ source/simulation2/components/CCmpPathfinder_Vertex.cpp
@@ -556,8 +556,13 @@
fixed rangeZMin = z0 - range;
fixed rangeZMax = z0 + range;
- // we don't actually add the "search space" edges as edges, since we may want to cross them
- // in some cases (such as if we need to go around an obstruction that's partly out of the search range)
+ // add domain edges
+ // (The edges are the opposite direction to usual, so it's an inside-out square)
+ edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMin), CFixedVector2D(rangeXMin, rangeZMax) });
+ edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMax), CFixedVector2D(rangeXMax, rangeZMax) });
+ edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMax), CFixedVector2D(rangeXMax, rangeZMin) });
+ edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMin), CFixedVector2D(rangeXMin, rangeZMin) });
+
// List of obstruction vertexes (plus start/end points); we'll try to find paths through
// the graph defined by these vertexes
@@ -614,26 +619,23 @@
vert.status = Vertex::UNEXPLORED;
vert.quadInward = QUADRANT_NONE;
vert.quadOutward = QUADRANT_ALL;
- vert.p.X = center.X - hd0.Dot(u); vert.p.Y = center.Y + hd0.Dot(v); if (aa) vert.quadInward = QUADRANT_BR; vertexes.push_back(vert);
- if (vert.p.X < rangeXMin) rangeXMin = vert.p.X;
- if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y;
- if (vert.p.X > rangeXMax) rangeXMax = vert.p.X;
- if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y;
- vert.p.X = center.X - hd1.Dot(u); vert.p.Y = center.Y + hd1.Dot(v); if (aa) vert.quadInward = QUADRANT_TR; vertexes.push_back(vert);
- if (vert.p.X < rangeXMin) rangeXMin = vert.p.X;
- if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y;
- if (vert.p.X > rangeXMax) rangeXMax = vert.p.X;
- if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y;
- vert.p.X = center.X + hd0.Dot(u); vert.p.Y = center.Y - hd0.Dot(v); if (aa) vert.quadInward = QUADRANT_TL; vertexes.push_back(vert);
- if (vert.p.X < rangeXMin) rangeXMin = vert.p.X;
- if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y;
- if (vert.p.X > rangeXMax) rangeXMax = vert.p.X;
- if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y;
- vert.p.X = center.X + hd1.Dot(u); vert.p.Y = center.Y - hd1.Dot(v); if (aa) vert.quadInward = QUADRANT_BL; vertexes.push_back(vert);
- if (vert.p.X < rangeXMin) rangeXMin = vert.p.X;
- if (vert.p.Y < rangeZMin) rangeZMin = vert.p.Y;
- if (vert.p.X > rangeXMax) rangeXMax = vert.p.X;
- if (vert.p.Y > rangeZMax) rangeZMax = vert.p.Y;
+
+ vert.p.X = center.X - hd0.Dot(u); vert.p.Y = center.Y + hd0.Dot(v);
+ if (aa) vert.quadInward = QUADRANT_BR;
+ if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax)
+ vertexes.push_back(vert);
+ vert.p.X = center.X - hd1.Dot(u); vert.p.Y = center.Y + hd1.Dot(v);
+ if (aa) vert.quadInward = QUADRANT_BR;
+ if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax)
+ vertexes.push_back(vert);
+ vert.p.X = center.X + hd0.Dot(u); vert.p.Y = center.Y - hd0.Dot(v);
+ if (aa) vert.quadInward = QUADRANT_TL;
+ if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax)
+ vertexes.push_back(vert);
+ vert.p.X = center.X + hd1.Dot(u); vert.p.Y = center.Y - hd1.Dot(v);
+ if (aa) vert.quadInward = QUADRANT_BL;
+ if (vert.p.X >= rangeXMin && vert.p.Y >= rangeZMin && vert.p.X <= rangeXMax && vert.p.Y <= rangeZMax)
+ vertexes.push_back(vert);
// Add the edges:
@@ -654,8 +656,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"
@@ -39,17 +40,6 @@
#include "ps/Profile.h"
#include "renderer/Scene.h"
-// For debugging; units will start going straight to the target
-// instead of calling the pathfinder
-#define DISABLE_PATHFINDER 0
-
-/**
- * When advancing along the long path, and picking a new waypoint to move
- * towards, we'll pick one that's up to this far from the unit's current
- * position (to minimise the effects of grid-constrained movement)
- */
-static const entity_pos_t WAYPOINT_ADVANCE_MAX = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*8);
-
/**
* Min/Max range to restrict short path queries to. (Larger ranges are slower,
* smaller ranges might miss some legitimate routes around large obstacles.)
@@ -58,191 +48,154 @@
static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*9);
/**
- * Minimum distance to goal for a long path request
+ * Below this distance to the goal, if we're getting obstructed, we will recreate a brand new Goal for our short-pathfinder
+ * Instead of using the one given to us by RecomputeGoalPosition.
+ * This is unsafe from a shot/long pathfinder compatibility POV, so it should not be too big.
*/
-static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
+static const entity_pos_t SHORT_PATH_GOAL_REDUX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2);
/**
- * When short-pathing, and the short-range pathfinder failed to return a path,
- * Assume we are at destination if we are closer than this distance to the target
- * And we have no target entity.
- * This is somewhat arbitrary, but setting a too big distance means units might lose sight of their end goal too much;
+ * Minimum distance to goal for a long path request
+ * Disabled, see note in RequestNewPath.
*/
-static const entity_pos_t SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2);
+static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*0);
/**
* If we are this close to our target entity/point, then think about heading
* for it in a straight line instead of pathfinding.
+ * TODO: this should probably be reintroduced
*/
static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
/**
- * If we're following a target entity,
- * we will recompute our path if the target has moved
- * more than this distance from where we last pathed to.
- */
-static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4);
-
-/**
- * If we're following as part of a formation,
- * but can't move to our assigned target point in a straight line,
- * we will recompute our path if the target has moved
- * more than this distance from where we last pathed to.
- */
-static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1);
-
-/**
- * If we're following something but it's more than this distance away along
- * our path, then don't bother trying to repath regardless of how much it has
- * moved, until we get this close to the end of our old path.
+ * See unitmotion logic for details. Higher means units will retry more often before potentially failing.
*/
-static const entity_pos_t CHECK_TARGET_MOVEMENT_AT_MAX_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*16);
+static const size_t MAX_PATH_REATTEMPS = 8;
-/**
- * If we're following something and the angle between the (straight-line) directions to its previous target
- * position and its present target position is greater than a given angle, recompute the path even far away
- * (i.e. even if CHECK_TARGET_MOVEMENT_AT_MAX_DIST condition is not fulfilled). The actual check is done
- * on the cosine of this angle, with a PI/6 angle.
- */
-static const fixed CHECK_TARGET_MOVEMENT_MIN_COS = fixed::FromInt(866)/1000;
-
-static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1);
-static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1);
+static const CColor OVERLAY_COLOR_PATH(1, 0, 0, 1);
class CCmpUnitMotion : public ICmpUnitMotion
{
-public:
- static void ClassInit(CComponentManager& componentManager)
+private:
+ struct SMotionGoal
{
- componentManager.SubscribeToMessageType(MT_Update_MotionFormation);
- componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
- componentManager.SubscribeToMessageType(MT_PathResult);
- componentManager.SubscribeToMessageType(MT_OwnershipChanged);
- componentManager.SubscribeToMessageType(MT_ValueModification);
- componentManager.SubscribeToMessageType(MT_Deserialized);
- }
+ private:
+ bool m_Valid = false;
- DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
-
- bool m_DebugOverlayEnabled;
- std::vector<SOverlayLine> m_DebugOverlayLongPathLines;
- std::vector<SOverlayLine> m_DebugOverlayShortPathLines;
+ entity_pos_t m_Range;
+ entity_id_t m_Entity;
+ CFixedVector2D m_Position;
- // Template state:
+ public:
+ SMotionGoal() : m_Valid(false) {};
- bool m_FormationController;
- fixed m_WalkSpeed, m_OriginalWalkSpeed; // in metres per second
- fixed m_RunSpeed, m_OriginalRunSpeed;
- pass_class_t m_PassClass;
- std::string m_PassClassName;
+ SMotionGoal(CFixedVector2D position, entity_pos_t range)
+ {
+ m_Entity = INVALID_ENTITY;
+ m_Range = range;
+ m_Position = position;
- // Dynamic state:
+ m_Valid = true;
+ }
- entity_pos_t m_Clearance;
- bool m_Moving;
- bool m_FacePointAfterMove;
+ SMotionGoal(entity_id_t target, entity_pos_t range)
+ {
+ m_Entity = target;
+ m_Range = range;
- enum State
- {
- /*
- * Not moving at all.
- */
- STATE_IDLE,
+ m_Valid = true;
+ }
- /*
- * Not moving at all. Will go to IDLE next turn.
- * (This one-turn delay is a hack to fix animation timings.)
- */
- STATE_STOPPING,
+ template<typename S>
+ void SerializeCommon(S& serialize)
+ {
+ serialize.Bool("valid", m_Valid);
- /*
- * Member of a formation.
- * Pathing to the target (depending on m_PathState).
- * Target is m_TargetEntity plus m_TargetOffset.
- */
- STATE_FORMATIONMEMBER_PATH,
+ serialize.NumberFixed_Unbounded("range", m_Range);
- /*
- * 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,
+ serialize.NumberU32_Unbounded("entity", m_Entity);
- STATE_MAX
- };
- u8 m_State;
+ serialize.NumberFixed_Unbounded("x", m_Position.X);
+ serialize.NumberFixed_Unbounded("y", m_Position.Y);
+ }
- enum PathState
- {
- /*
- * There is no path.
- * (This should only happen in IDLE and STOPPING.)
- */
- PATHSTATE_NONE,
+ bool IsEntity() const { return m_Entity != INVALID_ENTITY; }
+ entity_id_t GetEntity() const { return m_Entity; }
- /*
- * We have an outstanding long path request.
- * No paths are usable yet, so we can't move anywhere.
- */
- PATHSTATE_WAITING_REQUESTING_LONG,
+ bool Valid() const { return m_Valid; }
+ void Clear() { m_Valid = false; }
- /*
- * 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,
+ entity_pos_t Range() const { return m_Range; };
- /*
- * We are following our path, and have no path requests.
- * m_LongPath and m_ShortPath are valid.
- */
- PATHSTATE_FOLLOWING,
+ CFixedVector2D GetPosition() const { ENSURE(!m_Entity); return m_Position; }
+ };
- /*
- * 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,
+public:
+ static void ClassInit(CComponentManager& componentManager)
+ {
+ componentManager.SubscribeToMessageType(MT_Update_MotionUnit);
+ componentManager.SubscribeToMessageType(MT_PathResult);
+ componentManager.SubscribeToMessageType(MT_OwnershipChanged);
+ componentManager.SubscribeToMessageType(MT_ValueModification);
+ componentManager.SubscribeToMessageType(MT_Deserialized);
+ }
- /*
- * 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,
+ DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
- PATHSTATE_MAX
- };
- u8 m_PathState;
+ bool m_DebugOverlayEnabled;
+ std::vector<SOverlayLine> m_DebugOverlayPathLines;
- u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none
+ // Template state, never changed after init.
+ fixed m_TemplateWalkSpeed, m_TemplateTopSpeedRatio;
+ pass_class_t m_PassClass;
+ std::string m_PassClassName;
+ entity_pos_t m_Clearance;
- 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 destination, the unit is seen as trying to move
+ // It may not be actually moving for a variety of reasons (no path, blocked path)…
+ // this is our final destination
+ SMotionGoal m_Destination;
+ // this is a "temporary" destination. Most of the time it will be the same as m_Destination,
+ // but it doesn't have to be.
+ // Can be used to temporarily re-route or pause a unit from a given component, for whatever reason.
+ // Will also be used whenever I implemented step-by-step long paths using the hierarchical pathfinder.
+ SMotionGoal m_CurrentGoal;
+
+ // Pathfinder-compliant goal. Should be reachable (at least when it's recomputed).
+ PathGoal m_Goal;
+
+ // 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;
+ bool m_DumpPathOnResult;
+ bool m_RunShortPathValidation;
// 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 +212,8 @@
"<ref name='positiveDecimal'/>"
"</element>"
"<optional>"
- "<element name='Run'>"
- "<interleave>"
- "<element name='Speed'><ref name='positiveDecimal'/></element>"
- "<element name='Range'><ref name='positiveDecimal'/></element>"
- "<element name='RangeMin'><ref name='nonNegativeDecimal'/></element>"
- "<element name='RegenTime'><ref name='positiveDecimal'/></element>"
- "<element name='DecayTime'><ref name='positiveDecimal'/></element>"
- "</interleave>"
+ "<element name='RunMultiplier' a:help='How much faster the unit goes when running (as a multiple of walk speed)'>"
+ "<ref name='positiveDecimal'/>"
"</element>"
"</optional>"
"<element name='PassabilityClass' a:help='Identifies the terrain passability class (values are defined in special/pathfinder.xml)'>"
@@ -276,19 +223,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<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
@@ -302,18 +245,16 @@
cmpObstruction->SetUnitClearance(m_Clearance);
}
- m_State = STATE_IDLE;
- m_PathState = PATHSTATE_NONE;
-
m_ExpectedPathTicket = 0;
+ m_DumpPathOnResult = false;
+ m_RunShortPathValidation = false;
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 +264,28 @@
template<typename S>
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.Bool("dump path on result", m_DumpPathOnResult);
+ serialize.Bool("short path validation", m_RunShortPathValidation);
- 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<SerializeWaypoint>()(serialize, "path", m_Path.m_Waypoints);
serialize.NumberU8("tries", m_Tries, 0, 255);
+ serialize.NumberU8("waiting turns", m_WaitingTurns, 0, 255);
+
+ serialize.Bool("started moving", m_StartedMoving);
- SerializeVector<SerializeWaypoint>()(serialize, "long path", m_LongPath.m_Waypoints);
- SerializeVector<SerializeWaypoint>()(serialize, "short path", m_ShortPath.m_Waypoints);
+ // 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);
- SerializeGoal()(serialize, "goal", m_FinalGoal);
+ m_Destination.SerializeCommon(serialize);
+ m_CurrentGoal.SerializeCommon(serialize);
+ SerializeGoal()(serialize, "goal", m_Goal);
}
virtual void Serialize(ISerializer& serialize)
@@ -368,26 +304,15 @@
m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
}
+ // TODO: would be nice to listen to entity renamed messages, but those have no C++ interface so far.
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
- case MT_Update_MotionFormation:
- {
- if (m_FormationController)
- {
- fixed dt = static_cast<const CMessageUpdate_MotionFormation&> (msg).turnLength;
- Move(dt);
- }
- break;
- }
case MT_Update_MotionUnit:
{
- if (!m_FormationController)
- {
- fixed dt = static_cast<const CMessageUpdate_MotionUnit&> (msg).turnLength;
- Move(dt);
- }
+ fixed dt = static_cast<const CMessageUpdate_MotionUnit&> (msg).turnLength;
+ Move(dt);
break;
}
case MT_RenderSubmit:
@@ -410,24 +335,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<ICmpVisual> cmpVisualActor(GetEntityHandle());
+ if (cmpVisualActor)
+ cmpVisualActor->SetMovingSpeed(m_ActualSpeed);
+ }
+ case MT_OwnershipChanged:
+ {
CmpPtr<ICmpValueModificationManager> 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 +365,52 @@
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender);
}
- virtual bool IsMoving()
+ virtual bool IsActuallyMoving()
+ {
+ return m_StartedMoving;
+ }
+
+ virtual bool IsTryingToMove()
{
- return m_Moving;
+ return m_Destination.Valid();
}
- virtual fixed GetWalkSpeed()
+ virtual fixed GetBaseSpeed()
{
- return m_WalkSpeed;
+ return m_TechModifiedWalkSpeed;
}
- virtual fixed GetRunSpeed()
+ virtual fixed GetSpeed()
{
- return m_RunSpeed;
+ 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<ICmpVisual> cmpVisualActor(GetEntityHandle());
+ if (cmpVisualActor)
+ cmpVisualActor->SetMovingSpeed(m_ActualSpeed);
}
virtual pass_class_t GetPassabilityClass()
@@ -472,16 +431,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,105 +442,121 @@
UpdateMessageSubscriptions();
}
- 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()
- {
- m_Moving = false;
- m_ExpectedPathTicket = 0;
- m_State = STATE_STOPPING;
- m_PathState = PATHSTATE_NONE;
- m_LongPath.m_Waypoints.clear();
- m_ShortPath.m_Waypoints.clear();
- }
-
virtual entity_pos_t GetUnitClearance()
{
return m_Clearance;
}
-private:
- bool ShouldAvoidMovingUnits() const
- {
- return !m_FormationController;
- }
+ virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable);
+ virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable);
- bool IsFormationMember() const
- {
- return m_State == STATE_FORMATIONMEMBER_PATH;
- }
+ // transform a motion goal into a corresponding PathGoal
+ // called by RecomputeGoalPosition
+ PathGoal CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal);
- entity_id_t GetGroup() const
- {
- return IsFormationMember() ? m_TargetEntity : GetEntityId();
- }
+ // take an arbitrary path goal and convert it to a 2D point goal, assign it to m_Goal.
+ bool RecomputeGoalPosition(PathGoal& goal);
+
+ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
+ virtual void FaceTowardsEntity(entity_id_t ent);
- bool HasValidPath() const
+ virtual void SetAbortIfStuck(u8 shouldAbort)
{
- return m_PathState == PATHSTATE_FOLLOWING
- || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG
- || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT;
+ m_AbortIfStuck = shouldAbort;
}
- void StartFailed()
+ void StartMoving()
{
- StopMoving();
- m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started
+ m_StartedMoving = true;
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
-
- CMessageMotionChanged msg(true, true);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ cmpObstruction->SetMovingFlag(true);
}
- void MoveFailed()
+ virtual void StopMoving()
{
- StopMoving();
+ m_StartedMoving = false;
CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
if (cmpObstruction)
cmpObstruction->SetMovingFlag(false);
-
- CMessageMotionChanged msg(false, true);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
- void StartSucceeded()
+ virtual void DiscardMove()
{
- CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(true);
+ StopMoving();
+
+ m_Tries = 0;
+ m_WaitingTurns = 0;
+
+ m_ExpectedPathTicket = 0;
+ m_Path.m_Waypoints.clear();
- m_Moving = true;
+ m_CurrentGoal.Clear();
+ m_Destination.Clear();
+ }
- CMessageMotionChanged msg(true, false);
+ void MoveWillFail()
+ {
+ CMessageMoveFailure msg;
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
- void MoveSucceeded()
+ void MoveHasSucceeded()
{
- m_Moving = false;
+ CMessageMoveSuccess msg;
+ GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ }
- CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
+ virtual bool HasValidPath()
+ {
+ return !m_Path.m_Waypoints.empty();
+ }
- // No longer moving, so speed is 0.
- m_CurSpeed = fixed::Zero();
+private:
+ CFixedVector2D GetGoalPosition(const SMotionGoal& goal) const
+ {
+ ENSURE (goal.Valid());
- CMessageMotionChanged msg(false, false);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ if (goal.IsEntity())
+ {
+ CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), goal.GetEntity());
+ ENSURE(cmpPosition);
+ ENSURE(cmpPosition->IsInWorld()); // TODO: do something? Like return garrisoned building or such?
+ return cmpPosition->GetPosition2D();
+ }
+ else
+ return goal.GetPosition();
}
- bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target);
+ bool CurrentGoalHasValidPosition()
+ {
+ if (!m_CurrentGoal.Valid())
+ return false;
+
+ if (m_CurrentGoal.IsEntity())
+ {
+ CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), m_CurrentGoal.GetEntity());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return false;
+ return true;
+ }
+ else
+ return true;
+ }
+/*
+ TODO: reimplement
+ bool IsFormationMember() const
+ {
+ return m_State == STATE_FORMATIONMEMBER_PATH;
+ }
+*/
+ entity_id_t GetGroup() const
+ {
+ //return IsFormationMember() ? m_TargetEntity : GetEntityId();
+ return GetEntityId();
+ }
/**
* Handle the result of an asynchronous path query.
@@ -604,45 +569,26 @@
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.
+ * Check that our current waypoints are sensible or whether we should recompute
+ *
+ * Quick note on how clever UnitMotion should be: UnitMotion should try to reach the current target (m_CurrentGoal)
+ * as well as it can. But it should not take any particular guess on wether something CAN or SHOULD be reached.
+ * Examples: chasing a unit that's faster than us is probably stupid. This is not UnitMotion's to say, UnitMotion should try.
+ * Likewise when requesting a new path, even if it's unreachable unitMotion must try its best (but can inform unitAI that it's being obtuse)
+ * However, if a chased unit is moving, we should try to anticipate its moves by any means possible.
*/
- bool TryGoingStraightToGoalPoint(const CFixedVector2D& from);
+ void ValidateCurrentPath();
/**
- * 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.
+ * take a 2D position and return an updated one based on estimated target velocity.
*/
- 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();
+ inline void UpdatePositionForTargetVelocity(entity_id_t ent, entity_pos_t& x, entity_pos_t& z, fixed& certainty);
/**
* 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(SMotionGoal& goal);
/**
* Returns whether the length of the given path, plus the distance from
@@ -657,17 +603,14 @@
/**
* Returns an appropriate obstruction filter for use with path requests.
- * noTarget is true only when used inside tryGoingStraightToTargetEntity,
- * in which case we do not want the target obstruction otherwise it would always fail
*/
- ControlGroupMovementObstructionFilter GetObstructionFilter(bool noTarget = false) const;
+ ControlGroupMovementObstructionFilter GetObstructionFilter() const;
/**
- * Start moving to the given goal, from our current position 'from'.
- * Might go in a straight line immediately, or might start an asynchronous
- * path request.
+ * Dumps current path and request a new one asynchronously.
+ * Inside of UnitMotion, do not set evenUnreachable to false unless you REALLY know what you're doing.
*/
- void BeginPathing(const CFixedVector2D& from, const PathGoal& goal);
+ bool RequestNewPath(bool evenUnreachable = true);
/**
* Start an asynchronous long path query.
@@ -691,539 +634,428 @@
void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
{
- // reset our state for sanity.
- CmpPtr<ICmpObstruction> 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<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
+ if (m_DumpPathOnResult)
{
- 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();
- return;
+ m_Path.m_Waypoints.clear();
+ m_DumpPathOnResult = false;
}
- if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG)
- {
- 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 (!m_Destination.Valid())
+ return;
- // 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 (path.m_Waypoints.empty())
+ {
+ // 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();
- if (!HasValidPath())
- StartSucceeded();
+ // we will then deal with this on the next Move() call.
+ return;
+ }
- m_PathState = PATHSTATE_FOLLOWING;
+ // if this is a short path, verify some things
+ // Namely reject any path that takes us in another global region
+ // and any waypoint that's not passable/next to a passable cell.
+ // this will ensure minimal changes of long/short rance pathfinder discrepancies
+ if (m_RunShortPathValidation)
+ {
+ CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
+ ENSURE (cmpPathfinder);
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(true);
+ CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
+ ENSURE(cmpPosition);
- m_Moving = true;
- }
- else if (m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT || m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT)
- {
- m_ShortPath = path;
+ u16 i0, j0;
+ cmpPathfinder->FindNearestPassableNavcell(cmpPosition->GetPosition2D().X, cmpPosition->GetPosition2D().Y, i0, j0, m_PassClass);
- // If there's no waypoints then we couldn't get near the target
- if (m_ShortPath.m_Waypoints.empty())
+ m_RunShortPathValidation = false;
+ for (const Waypoint& wpt : path.m_Waypoints)
{
- // 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())
+ u16 i1, j1;
+ u32 dist = cmpPathfinder->FindNearestPassableNavcell(wpt.x, wpt.z, i1, j1, m_PassClass);
+ if (dist > 1 || !cmpPathfinder->NavcellIsReachable(i0, j0, i1, j1, m_PassClass))
{
- m_Moving = false;
- CMessageMotionChanged msg(true, true);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+ MoveWillFail();
+ // we will then deal with this on the next Move() call.
return;
}
+ }
+ }
+
+ // if we're currently moving, we have a path, so check if the first waypoint can be removed
+ // it's not impossible that we've actually reached it already.
+ if (IsActuallyMoving() && path.m_Waypoints.size() >= 2)
+ {
+ CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
+ CFixedVector2D nextWp = CFixedVector2D(path.m_Waypoints.back().x,path.m_Waypoints.back().z) - cmpPosition->GetPosition2D();
+ if (nextWp.CompareLength(GetSpeed()/2) <= 0)
+ {
+ m_Path.m_Waypoints.insert(m_Path.m_Waypoints.end(), path.m_Waypoints.begin(), path.m_Waypoints.end()-1);
+ return;
+ }
+ }
+ m_Path.m_Waypoints.insert(m_Path.m_Waypoints.end(), path.m_Waypoints.begin(), path.m_Waypoints.end());
+}
- CMessageMotionChanged msg(false, false);
- GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
+void CCmpUnitMotion::ValidateCurrentPath()
+{
+ // this should be kept in sync with RequestNewPath otherwise we'll spend our whole life repathing.
- CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return;
+ // don't validate points, they never change
+ if (!m_CurrentGoal.IsEntity())
+ return;
- CFixedVector2D pos = cmpPosition->GetPosition2D();
+ // TODO: figure out what to do when the goal dies.
+ // for now we'll keep on keeping on, but reset as if our goal was a position
+ // and send a failure message to UnitAI in case it wants to do something
+ // use position as a proxy for existence
+ CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), m_CurrentGoal.GetEntity());
+ if (!cmpTargetPosition)
+ {
+ // TODO: this should call a custom function for this
+ SMotionGoal newGoal(CFixedVector2D(m_Goal.x, m_Goal.z), m_CurrentGoal.Range());
+ m_Destination = newGoal;
+ m_CurrentGoal = newGoal;
+ RequestNewPath();
+ MoveWillFail();
+ return;
+ }
- if (ShouldConsiderOurselvesAtDestination(pos))
- return;
+ // don't validate if no path.
+ if (!HasValidPath())
+ return;
- UpdateFinalGoal();
- RequestLongPath(pos, m_FinalGoal);
- m_PathState = PATHSTATE_WAITING_REQUESTING_LONG;
- return;
- }
+ // TODO: check LOS here (instead of in UnitAI like we do now).
- // else we could, so reset our number of tries.
- m_Tries = 0;
+ // if our goal can move, then perhaps it has.
+ CmpPtr<ICmpUnitMotion> cmpTargetUnitMotion(GetSimContext(), m_CurrentGoal.GetEntity());
+ if (!cmpTargetUnitMotion)
+ return;
- // Now we've got a short path that we can follow
- if (!HasValidPath())
- StartSucceeded();
+ // Check if our current Goal's position (ie m_Goal, not m_CurrentGoal) is sensible.
- m_PathState = PATHSTATE_FOLLOWING;
+ // TODO: this will probably be called every turn if the entity tries to go to an unreachable unit
+ // In those cases, UnitAI should be warned that the unit is unreachable and tell us to do something else.
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(true);
+ CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
+ fixed certainty = m_Clearance*2;
+ UpdatePositionForTargetVelocity(m_CurrentGoal.GetEntity(), targetPos.X, targetPos.Y, certainty);
- m_Moving = true;
- }
- else
- LOGWARNING("unexpected PathResult (%u %d %d)", GetEntityId(), m_State, m_PathState);
+ CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
+ if (!cmpObstructionManager->IsPointInPointRange(m_Goal.x, m_Goal.z, targetPos.X, targetPos.Y, m_CurrentGoal.Range() - certainty, m_CurrentGoal.Range() + certainty))
+ RequestNewPath();
+}
+
+void CCmpUnitMotion::UpdatePositionForTargetVelocity(entity_id_t ent, entity_pos_t& x, entity_pos_t& z, fixed& certainty)
+{
+ CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), ent);
+ CmpPtr<ICmpUnitMotion> cmpTargetUnitMotion(GetSimContext(), ent);
+ if (!cmpTargetPosition || !cmpTargetUnitMotion || !cmpTargetUnitMotion->IsActuallyMoving())
+ return;
+
+ // So here we'll try to estimate where the unit will be by the time we reach it.
+ // This can be done perfectly but I cannot think of a non-iterative process and this seems complicated for our purposes here
+ // so just get our direct distance and do some clever things, we'll correct later on anyhow so it doesn't matter.
+ CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
+
+ // try to estimate in how much time we'll reach it.
+ fixed distance = (cmpTargetPosition->GetPosition2D() - cmpPosition->GetPosition2D()).Length();
+ fixed time = std::min(distance / GetSpeed(), fixed::FromInt(5)); // don't try from too far away or this is just dumb.
+
+ CFixedVector2D travelVector = (cmpTargetPosition->GetPosition2D() - cmpTargetPosition->GetPreviousPosition2D()).Multiply(time) * 2;
+ x += travelVector.X;
+ z += travelVector.Y;
+
+ certainty += time * 2;
}
+// TODO: this can probably be split in a few functions efficiently and it'd be cleaner.
void CCmpUnitMotion::Move(fixed dt)
{
PROFILE("Move");
- if (m_State == STATE_STOPPING)
+ // early out
+ if (!IsTryingToMove())
{
- m_State = STATE_IDLE;
- MoveSucceeded();
+ SetActualSpeed(fixed::Zero());
return;
}
- if (m_State == STATE_IDLE)
- return;
+ // 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.
- switch (m_PathState)
- {
- case PATHSTATE_NONE:
- {
- // If we're not pathing, do nothing
+ CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
+ if (!cmpPathfinder)
return;
- }
- case PATHSTATE_WAITING_REQUESTING_LONG:
- case PATHSTATE_WAITING_REQUESTING_SHORT:
- {
- // If we're waiting for a path and don't have one yet, do nothing
+ CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
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.
-
- CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return;
- CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return;
+ CFixedVector2D initialPos = cmpPosition->GetPosition2D();
- CFixedVector2D initialPos = cmpPosition->GetPosition2D();
+ // NB: unitMotion has been changed such that unitAI should NOT rely on "movecompleted" messages but do its own range checks on a timer basis.
+ // To streamline some common interactions, such as gathering from a static entity, we'll send a "hint" when we're done moving (and thus presumably arrived)
+ // but this should NOT be relied upon.
+ if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal))
+ {
+ if (m_FacePointAfterMove && CurrentGoalHasValidPosition())
+ FaceTowardsPoint(GetGoalPosition(m_CurrentGoal).X, GetGoalPosition(m_CurrentGoal).Y);
- // 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)
+ bool sendMessage = false;
+ if (ShouldConsiderOurselvesAtDestination(m_Destination))
+ // send a hint to unitAI to maintain compatibility.
+ sendMessage = true;
- // 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);
+ if (sendMessage)
+ MoveHasSucceeded();
+ }
- fixed maxSpeed = basicSpeed.Multiply(terrainSpeed);
+ // All path updates/checks should go here, before the moving loop.
+ ValidateCurrentPath();
- bool wasObstructed = false;
+ //////////////////////////////////////////////////////////////////////////////////////
+ //// AFTER THIS POINT, NO MESSAGES SHOULD BE SENT OR HORRIBLE THINGS WILL HAPPEN. ////
+ //// YOU HAVE BEEN WARNED. ////
+ //////////////////////////////////////////////////////////////////////////////////////
- // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint
+ if (!IsTryingToMove())
+ {
+ // One of the messages we sent UnitAI caused us to stop moving entirely.
+ // Tell the visual actor we're not moving this turn to avoid gliding.
+ SetActualSpeed(fixed::Zero());
+ return;
+ }
- fixed timeLeft = dt;
- fixed zero = fixed::Zero();
+ // Keep track of the current unit's position during the update
+ CFixedVector2D pos = initialPos;
- while (timeLeft > zero)
- {
- // If we ran out of path, we have to stop
- if (m_ShortPath.m_Waypoints.empty() && m_LongPath.m_Waypoints.empty())
- break;
+ // 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 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;
+ bool wasObstructed = false;
- // Spend the rest of the time heading towards the next waypoint
- timeLeft = timeLeft - (offsetLength / maxSpeed);
+ // We want to move (at most) m_Speed*dt units from pos towards the next waypoint
- if (m_ShortPath.m_Waypoints.empty())
- m_LongPath.m_Waypoints.pop_back();
- else
- m_ShortPath.m_Waypoints.pop_back();
+ fixed timeLeft = dt;
- 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
+ // 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;
- break;
- }
- }
+ CFixedVector2D target;
+ target = CFixedVector2D(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
- // Update the Position component after our movement (if we actually moved anywhere)
- if (pos != initialPos)
+ 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
{
- 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);
-
- // Calculate the mean speed over this past turn.
- m_CurSpeed = cmpPosition->GetDistanceTravelled() / dt;
+ offset.Normalize(maxdist);
+ destination = pos + offset;
}
- if (wasObstructed)
+ // 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))
{
- // Oops, we hit something (very likely another unit).
- // This is when we might easily get stuck wrongly.
+ pos = destination;
- // check if we've arrived.
- if (ShouldConsiderOurselvesAtDestination(pos))
- return;
+ timeLeft = (timeLeft.Multiply(m_Speed) - offsetLength) / m_Speed;
- // 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<ICmpObstructionManager> 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<entity_id_t> 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);
-
- // potential TODO: We could switch the short-range pathfinder for something else entirely.
- return;
+ if (destination == target)
+ m_Path.m_Waypoints.pop_back();
+ continue;
}
-
- // We successfully moved along our path, until running out of
- // waypoints or time.
-
- if (m_PathState == PATHSTATE_FOLLOWING)
+ else
{
- // If we're not currently computing any new paths:
- if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty())
- {
- 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<ICmpUnitMotion> cmpUnitMotion(GetSimContext(), m_TargetEntity);
- if (cmpUnitMotion && !cmpUnitMotion->IsMoving())
- {
- CmpPtr<ICmpObstruction> 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<ICmpUnitMotion> 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
- }
- }
-
- // 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);
+ // Error - path was obstructed
+ wasObstructed = true;
+ break;
}
}
- }
-}
-bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out)
-{
- if (m_TargetEntity == INVALID_ENTITY)
- return false;
+ if (!m_StartedMoving && wasObstructed)
+ // If this is the turn we start moving, and we're already obstructed,
+ // fail the move entirely to avoid weirdness.
+ // TODO: figure out if this is actually necessary with the other changes
+ pos = initialPos;
+
+ // Update the Position component after our movement (if we actually moved anywhere)
+ if (pos != initialPos)
+ {
+ CFixedVector2D offset = pos - initialPos;
+
+ // tell other components and visual actor we are moving.
+ if (!m_StartedMoving)
+ StartMoving();
- CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), m_TargetEntity);
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
+ // Face towards the target
+ entity_angle_t angle = atan2_approx(offset.X, offset.Y);
+ cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle);
- if (m_TargetOffset.IsZero())
- {
- // No offset, just return the position directly
- out = cmpPosition->GetPosition2D();
+ // 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);
+
+ if (!wasObstructed)
+ {
+ // everything is going smoothly, return.
+ m_Tries = 0;
+ m_WaitingTurns = 0;
+ return;
+ }
}
else
- {
- // 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;
- }
- return true;
-}
+ // TODO: this and the same call in the if above could probably be moved before the if entirely, check rounding.
+ SetActualSpeed(fixed::Zero());
-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;
-
- CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return false;
-
- // 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;
+ // we've had to stop at the end of the turn.
+ StopMoving();
- // 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 });
+ ////////////////////////////////////////////////////////////////////
+ //// From this point onwards messages are "safe" to send again. ////
+ ////////////////////////////////////////////////////////////////////
+
+ if (ShouldConsiderOurselvesAtDestination(m_CurrentGoal))
+ // If we're out of path (ie not moving) but have a valid destination (IsTryingToMove()), we'll end up here every turn.
+ // We should not repath if we actually are where we want to be (ie at destination).
+ return;
- return true;
-}
+ // 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.
-bool CCmpUnitMotion::TryGoingStraightToTargetEntity(const CFixedVector2D& from)
-{
- CFixedVector2D targetPos;
- if (!ComputeTargetPosition(targetPos))
- return false;
+ if (m_ExpectedPathTicket != 0)
+ // wait until we get our path to see where that leads us.
+ return;
- // Fail if the target is too far away
- if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0)
- return false;
+ // 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; // currently we won't wait at all
+ else
+ m_WaitingTurns = 3;
+ }
- CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return false;
+ --m_WaitingTurns;
- // 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)
+ // Try again next turn, no changes
+ if (m_WaitingTurns >= MAX_PATH_REATTEMPS)
+ return;
- // Find the point on the goal shape that we should head towards
- CFixedVector2D goalPos = goal.NearestPointOnGoal(from);
+ // already waited one turn, no changes, so try computing a short path.
+ if (m_WaitingTurns >= 3)
+ {
+ if (m_Path.m_Waypoints.empty())
+ {
+ RequestNewPath();
+ return;
+ }
+ /**
+ * Here there are two cases:
+ * 1) We are somewhat far away from the goal, in which case proceed as usual
+ * 2) We're really close to the goal.
+ * If it's (2) it's likely that we are running into units that are currently doing the same thing we want to do (gathering from the same tree…)
+ * Since the initial call to MakeGoalReachable gave us a specific 2D coordinate, and we can't reach it,
+ * We have a relatively high chance of never being able to reach that particular point.
+ * So we need to recreate the actual goal for this entity. This is a little dangerous in terms of short/long pathfinder compatibility
+ * So we'll run sanity checks on the output to try and not get stuck/go where we shouldn't.
+ */
+ PathGoal goal;
- // 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;
+ CFixedVector2D nextWptPos(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
+ if ((nextWptPos - pos).CompareLength(SHORT_PATH_GOAL_REDUX_DIST) > 0)
+ {
+ goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z };
+ m_Path.m_Waypoints.pop_back();
+ }
+ else
+ {
+ goal = CreatePathGoalFromMotionGoal(m_CurrentGoal);
+ m_DumpPathOnResult = true;
+ }
- // 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 });
+ RequestShortPath(pos, goal, true);
+ return;
+ }
- return true;
-}
+ // Last resort, compute a long path
+ if (m_WaitingTurns == 2)
+ {
+ if (m_Path.m_Waypoints.empty())
+ {
+ RequestNewPath();
+ return;
+ }
+ PathGoal goal;
+ goal = { PathGoal::POINT, m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z };
+ m_Path.m_Waypoints.pop_back();
-bool CCmpUnitMotion::CheckTargetMovement(const CFixedVector2D& from, entity_pos_t minDelta)
-{
- CFixedVector2D targetPos;
- if (!ComputeTargetPosition(targetPos))
- return false;
+ RequestLongPath(pos, goal);
+ return;
+ }
- // Fail unless the target has moved enough
- CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z);
- if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0)
- return false;
- CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
- CFixedVector2D pos = cmpPosition->GetPosition2D();
- CFixedVector2D oldDir = (oldTargetPos - pos);
- CFixedVector2D newDir = (targetPos - 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))
- return false;
+ // m_waitingTurns == 1 here
- // Fail if the target is no longer visible to this entity's owner
- // (in which case we'll continue moving to its last known location,
- // unless it comes back into view before we reach that location)
- CmpPtr<ICmpOwnership> cmpOwnership(GetEntityHandle());
- if (cmpOwnership)
+ // we tried getting a renewed path and still got stuck
+ if (m_AbortIfStuck == 0)
{
- CmpPtr<ICmpRangeManager> cmpRangeManager(GetSystemEntity());
- if (cmpRangeManager && cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN)
- return false;
+ DiscardMove();
+ MoveWillFail();
+ return;
}
- // 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;
+ --m_AbortIfStuck;
- return true;
-}
+ // Recompute a new path, but wait a few turns first
+ m_WaitingTurns = 4 + MAX_PATH_REATTEMPS;
-void CCmpUnitMotion::UpdateFinalGoal()
-{
- if (m_TargetEntity == INVALID_ENTITY)
- return;
- CmpPtr<ICmpUnitMotion> 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;
+ return;
}
-bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(const CFixedVector2D& from)
+// Only used to send a "hint" to unitAI.
+bool CCmpUnitMotion::ShouldConsiderOurselvesAtDestination(SMotionGoal& goal)
{
- if (m_TargetEntity != INVALID_ENTITY || m_FinalGoal.DistanceToPoint(from) > SHORT_PATH_GOAL_RADIUS)
- return false;
+ if (HasValidPath())
+ return false; // wait until we're done.
- StopMoving();
- MoveSucceeded();
+ CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
+ if (!cmpObstructionManager)
+ return true; // what's a sane default here?
- if (m_FacePointAfterMove)
- FaceTowardsPointFromPos(from, m_FinalGoal.x, m_FinalGoal.z);
- return true;
+ if (goal.IsEntity())
+ return cmpObstructionManager->IsInTargetRange(GetEntityId(), goal.GetEntity(), goal.Range(), goal.Range());
+ else
+ return cmpObstructionManager->IsInPointRange(GetEntityId(), goal.GetPosition().X, goal.GetPosition().Y, goal.Range(), goal.Range());
}
bool CCmpUnitMotion::PathIsShort(const WaypointPath& path, const CFixedVector2D& from, entity_pos_t minDistance) const
@@ -1273,498 +1105,269 @@
}
}
-ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool noTarget) const
+void CCmpUnitMotion::FaceTowardsEntity(entity_id_t ent)
{
- entity_id_t group = noTarget ? m_TargetEntity : GetGroup();
- return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), group);
-}
-
-
-
-void CCmpUnitMotion::BeginPathing(const CFixedVector2D& from, const PathGoal& goal)
-{
- // reset our state for sanity.
- m_ExpectedPathTicket = 0;
-
- CmpPtr<ICmpObstruction> cmpObstruction(GetEntityHandle());
- if (cmpObstruction)
- cmpObstruction->SetMovingFlag(false);
-
- m_Moving = false;
-
- m_PathState = PATHSTATE_NONE;
-
-#if DISABLE_PATHFINDER
- {
- CmpPtr<ICmpPathfinder> cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
- CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from);
- 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;
+ CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
+ if (!cmpPosition || !cmpPosition->IsInWorld())
return;
- }
- // Same thing applies to non-entity points
- if (TryGoingStraightToGoalPoint(from))
- {
- if (!HasValidPath())
- StartSucceeded();
- m_PathState = PATHSTATE_FOLLOWING;
+ CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), ent);
+ if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
return;
- }
- // Otherwise we need to compute a path.
+ CFixedVector2D pos = cmpPosition->GetPosition2D();
+ CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
- // 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);
- }
- else
+ CFixedVector2D offset = targetPos - pos;
+ if (!offset.IsZero())
{
- m_PathState = PATHSTATE_WAITING_REQUESTING_LONG;
- RequestLongPath(from, goal);
+ entity_angle_t angle = atan2_approx(offset.X, offset.Y);
+ cmpPosition->TurnTo(angle);
}
-}
-
-void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal)
-{
- CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- return;
- // this is by how much our waypoints will be apart at most.
- // this value here seems sensible enough.
- PathGoal improvedGoal = goal;
- improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1);
-
- cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass);
-
- m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId());
}
-void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits)
+ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter() const
{
- CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
- if (!cmpPathfinder)
- 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));
- 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)
- searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
-
- m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId());
+ // TODO: if we sometimes want to consider moving units, change here.
+ return ControlGroupMovementObstructionFilter(true, GetGroup());
}
-bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
+// TODO: this can be improved, it's a little limited
+// e.g. use of hierarchical pathfinder,…
+// Also adding back the "straight-line if close enough" test could be good.
+bool CCmpUnitMotion::RequestNewPath(bool evenUnreachable)
{
- return MoveToPointRange(x, z, minRange, maxRange, INVALID_ENTITY);
-}
+ ENSURE(m_ExpectedPathTicket == 0);
-bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange, entity_id_t target)
-{
- PROFILE("MoveToPointRange");
+ ENSURE(CurrentGoalHasValidPosition());
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
+ ENSURE (cmpPosition);
- CFixedVector2D pos = cmpPosition->GetPosition2D();
+ CFixedVector2D position = cmpPosition->GetPosition2D();
- PathGoal goal;
- goal.x = x;
- goal.z = z;
+ m_DumpPathOnResult = true;
- if (minRange.IsZero() && maxRange.IsZero())
- {
- // Non-ranged movement:
+ bool reachable = RecomputeGoalPosition(m_Goal);
- // Head directly for the goal
- goal.type = PathGoal::POINT;
- }
- else
+ if (!reachable && !evenUnreachable)
{
- // Ranged movement:
-
- entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length();
-
- if (distance < minRange)
- {
- // Too close to target - move outwards to a circle
- // that's slightly larger than the min range
- goal.type = PathGoal::INVERTED_CIRCLE;
- goal.hw = minRange + Pathfinding::GOAL_DELTA;
- }
- else if (maxRange >= entity_pos_t::Zero() && distance > maxRange)
- {
- // Too far from target - move inwards to a circle
- // that's slightly smaller than the max range
- goal.type = PathGoal::CIRCLE;
- goal.hw = maxRange - Pathfinding::GOAL_DELTA;
-
- // If maxRange was abnormally small,
- // collapse the circle into a point
- if (goal.hw <= entity_pos_t::Zero())
- goal.type = PathGoal::POINT;
- }
- else
- {
- // We're already in range - no need to move anywhere
- if (m_FacePointAfterMove)
- FaceTowardsPointFromPos(pos, x, z);
- return false;
- }
- }
-
- 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<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
+ // Do not submit a path request if we've been told it's not going to be used anyhow.
+ DiscardMove();
return false;
+ }
- CFixedVector2D pos = cmpPosition->GetPosition2D();
-
- bool hasObstruction = false;
- CmpPtr<ICmpObstructionManager> 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;
+ ENSURE(m_Goal.x >= fixed::Zero());
- // See if we're close enough to the target square
- if (maxRange < entity_pos_t::Zero() || distance <= maxRange)
- return true;
+ /**
+ * A (long) note on short vs long range pathfinder, their conflict, and stuck units.
+ * A long-standing issue with 0 A.D.'s pathfinding has been that the short-range pathfinder is "better" than the long-range
+ * Indeed it can find paths that the long-range one cannot, since the grid is coarser than the real vector representation.
+ * This leads to units going where they shouldn't go, notably impassable and "unreachable" areas.
+ * This has been a -real- plague. Made worse by the facts that groups of trees tended to trigger it, leading to stuck gatherers…
+ * Thus, in general, we'd want the short-range and the long-range pathfinder to coincide. But making the short-range pathfinder
+ * register all impassable navcells as edges would just be way too slow, so we can't do that, so we -cannot- fix the issue
+ * by just changing the pathfinders' behavior.
+ *
+ * All hope is not lost, however.
+ *
+ * A big part of the problem is that before the unitMotion rewrite, UnitMotion requested a path to the goal, and then the pathfinder
+ * made that goal "reachable" by calling MakeGoalReachable, which uses the same grid as the long-range pathfinder. Thus, over short ranges,
+ * the pathfinder entirely short-circuited this. Since UnitMotion now calls MakeGoalReachable on its own, it only ever requests
+ * paths to points that are indeed supposed to be reachable. This does fix a number of cases.
+ *
+ * But then, why set LONG_PATH_MIN_DIST to 0 and disable the use of short paths here? Well it turns out you still had a few edge cases.
+ *
+ * Imagine two houses next to each other, with a space between them just wide enough that there are no passable navcells,
+ * but enough space for the short-range pathfinder to return a path through them (make them in a test map if you have to).
+ * If you ask a unit to cross there, the goal won't change: it's reachable by the long-range pathfinder by going around the house.
+ * However, the distance is < LONG_PATH_MIN_DIST, so the short-range pathfinder is called, so it goes through the house. Edge case.
+ * There's a variety of similar cases that can be imagined around the idea that there exists a shorter path visible only by the short-range pathfinder.
+ * If we never use the short-pathfinder in RequestNewPath, we can safely avoid those edge cases.
+ *
+ * However, we still call the short-pathfinder when running into an obstruction to avoid units. Can't that get us stuck too?
+ * Well, it probably can. But there's a few things to consider:
+ * -It's harder to trigger it if you actually have to run into a unit
+ * -In those cases, UnitMotion requests a path to the next existing waypoint (if there are none, it calls requestnewPath to get those)
+ * and the next existing waypoint has -necessarily- been given to us by the long-range pathfinder since we're using it here
+ * -We are running sanity checks on the output (see PathResult).
+ * Thus it's far less likely that the short-range pathfinder will return us an impassable path.
+ * It -is- not entirely impossible. A freak construction with many units strategically positionned could probably reveal the bug.
+ * But it's in my opinion rare enough that this discrepancy can be considered fixed.
+ */
- return false;
- }
+ if (m_Goal.DistanceToPoint(position) < LONG_PATH_MIN_DIST)
+ RequestShortPath(position, m_Goal, true);
else
- {
- entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length();
+ RequestLongPath(position, m_Goal);
- if (distance < minRange)
- return false;
- else if (maxRange >= entity_pos_t::Zero() && distance > maxRange)
- return false;
- else
- return true;
- }
-}
-
-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 reachable;
}
-bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+PathGoal CCmpUnitMotion::CreatePathGoalFromMotionGoal(const SMotionGoal& motionGoal)
{
- PROFILE("MoveToTargetRange");
+ PathGoal goal = PathGoal();
+ goal.x = fixed::FromInt(-1); // to figure out whether it's false-unreachable or false-buggy
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
+ ENSURE(cmpPosition);
CFixedVector2D pos = cmpPosition->GetPosition2D();
- CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
- if (!cmpObstructionManager)
- return false;
+ // The point of this function is to get a reachable navcell where we want to go.
+ // It calls the hierarchical pathfinder's MakeGoalReachable function directly
+ // and analyzes to result to return something acceptable.
+ // "acceptable" means that if there is a path, once the unit has reached its destination,
+ // the ObstructionManager's "IsInPointRange/IsInTargetRange" should consider it in range.
+ // So we need to make sure MakeGoalReachable will return something in sync.
- bool hasObstruction = false;
- ICmpObstructionManager::ObstructionSquare obstruction;
- CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), target);
- if (cmpObstruction)
- hasObstruction = cmpObstruction->GetObstructionSquare(obstruction);
+ // defaut to point at position
+ goal.type = PathGoal::POINT;
+ goal.x = GetGoalPosition(motionGoal).X;
+ goal.z = GetGoalPosition(motionGoal).Y;
- if (!hasObstruction)
+ // few cases to consider.
+ if (motionGoal.IsEntity())
{
- // The target didn't have an obstruction or obstruction shape, so treat it as a point instead
-
- CmpPtr<ICmpPosition> cmpTargetPosition(GetSimContext(), target);
- if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
- return false;
-
- CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D();
-
- return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange);
- }
-
- /*
- * If we're starting outside the maxRange, we need to move closer in.
- * If we're starting inside the minRange, we need to move further out.
- * These ranges are measured from the center of this entity to the edge of the target;
- * we add the goal range onto the size of the target shape to get the goal shape.
- * (Then we extend it outwards/inwards by a little bit to be sure we'll end up
- * within the right range, in case of minor numerical inaccuracies.)
- *
- * There's a bit of a problem with large square targets:
- * the pathfinder only lets us move to goals that are squares, but the points an equal
- * distance from the target make a rounded square shape instead.
- *
- * When moving closer, we could shrink the goal radius to 1/sqrt(2) so the goal shape fits entirely
- * within the desired rounded square, but that gives an unfair advantage to attackers who approach
- * the target diagonally.
- *
- * If the target is small relative to the range (e.g. archers attacking anything),
- * then we cheat and pretend the target is actually a circle.
- * (TODO: that probably looks rubbish for things like walls?)
- *
- * If the target is large relative to the range (e.g. melee units attacking buildings),
- * then we multiply maxRange by approx 1/sqrt(2) to guarantee they'll always aim close enough.
- * (Those units should set minRange to 0 so they'll never be considered *too* close.)
- */
-
- CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
- PathGoal goal;
- goal.x = obstruction.x;
- goal.z = obstruction.z;
-
- 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);
-
- bool inside = distance.IsZero() && !Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize).IsZero();
- if ((distance < minRange && previousDistance < minRange) || inside)
- {
- // Too close to the square - need to move away
-
- // Circumscribe the square
- entity_pos_t circleRadius = halfSize.Length();
+ CmpPtr<ICmpObstruction> cmpObstruction(GetSimContext(), motionGoal.GetEntity());
+ if (cmpObstruction)
+ {
+ ICmpObstructionManager::ObstructionSquare obstruction;
+ bool hasObstruction = cmpObstruction->GetObstructionSquare(obstruction);
+ if (hasObstruction)
+ {
+ fixed certainty;
+ UpdatePositionForTargetVelocity(motionGoal.GetEntity(), obstruction.x, obstruction.z, certainty);
- entity_pos_t goalDistance = minRange + Pathfinding::GOAL_DELTA;
+ goal.type = PathGoal::CIRCLE;
+ goal.x = obstruction.x;
+ goal.z = obstruction.z;
+ goal.hw = obstruction.hw + motionGoal.Range() + m_Clearance;
- if (ShouldTreatTargetAsCircle(minRange, circleRadius))
- {
- // The target is small relative to our range, so pretend it's a circle
- goal.type = PathGoal::INVERTED_CIRCLE;
- goal.hw = circleRadius + goalDistance;
- }
- else
- {
- goal.type = PathGoal::INVERTED_SQUARE;
- goal.u = obstruction.u;
- goal.v = obstruction.v;
- goal.hw = obstruction.hw + goalDistance;
- goal.hh = obstruction.hh + goalDistance;
+ // if not a unit, treat as a square
+ if (cmpObstruction->GetUnitRadius() == fixed::Zero())
+ {
+ goal.type = PathGoal::SQUARE;
+ goal.hh = obstruction.hh + motionGoal.Range() + m_Clearance;
+ goal.u = obstruction.u;
+ goal.v = obstruction.v;
+
+ fixed distance = Geometry::DistanceToSquare(pos - CFixedVector2D(goal.x,goal.z), goal.u, goal.v, CFixedVector2D(goal.hw, goal.hh), true);
+ if (distance == fixed::Zero())
+ goal.type = PathGoal::INVERTED_SQUARE;
+ }
+ else if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0)
+ goal.type = PathGoal::INVERTED_CIRCLE;
+ }
}
+ // if no obstruction, keep treating as a point
}
- else if (maxRange < entity_pos_t::Zero() || distance < maxRange || previousDistance < maxRange)
+ if (goal.type == PathGoal::POINT && motionGoal.Range() > fixed::Zero())
{
- // We're already in range - no need to move anywhere
- FaceTowardsPointFromPos(pos, goal.x, goal.z);
- return false;
+ goal.type = PathGoal::CIRCLE;
+ goal.hw = motionGoal.Range();
+ if ((pos - CFixedVector2D(goal.x,goal.z)).CompareLength(goal.hw) <= 0)
+ goal.type = PathGoal::INVERTED_CIRCLE;
}
- else
- {
- // We might need to move closer:
- // Circumscribe the square
- entity_pos_t circleRadius = halfSize.Length();
+ return goal;
+}
- if (ShouldTreatTargetAsCircle(maxRange, circleRadius))
- {
- // The target is small relative to our range, so pretend it's a circle
+bool CCmpUnitMotion::RecomputeGoalPosition(PathGoal& goal)
+{
+ if (!CurrentGoalHasValidPosition())
+ return false; // we're not going anywhere
- // Note that the distance to the circle will always be less than
- // the distance to the square, so the previous "distance < maxRange"
- // check is still valid (though not sufficient)
- entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius;
- entity_pos_t previousCircleDistance = (pos - CFixedVector2D(previousObstruction.x, previousObstruction.z)).Length() - circleRadius;
+ goal = CreatePathGoalFromMotionGoal(m_CurrentGoal);
- if (circleDistance < maxRange || previousCircleDistance < maxRange)
- {
- // We're already in range - no need to move anywhere
- if (m_FacePointAfterMove)
- FaceTowardsPointFromPos(pos, goal.x, goal.z);
- return false;
- }
+ // We now have a correct goal.
+ // Make it reachable
- entity_pos_t goalDistance = maxRange - Pathfinding::GOAL_DELTA;
+ CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
+ ENSURE(cmpPathfinder);
- goal.type = PathGoal::CIRCLE;
- goal.hw = circleRadius + goalDistance;
- }
- else
- {
- // The target is large relative to our range, so treat it as a square and
- // get close enough that the diagonals come within range
+ CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
+ ENSURE(cmpPosition);
- entity_pos_t goalDistance = (maxRange - Pathfinding::GOAL_DELTA)*2 / 3; // multiply by slightly less than 1/sqrt(2)
+ CFixedVector2D pos = cmpPosition->GetPosition2D();
- goal.type = PathGoal::SQUARE;
- goal.u = obstruction.u;
- goal.v = obstruction.v;
- entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16); // ensure it's far enough to not intersect the building itself
- goal.hw = obstruction.hw + delta;
- goal.hh = obstruction.hh + delta;
- }
- }
+ bool reachable = cmpPathfinder->MakeGoalReachable(pos.X, pos.Y, goal, m_PassClass);
- m_State = STATE_INDIVIDUAL_PATH;
- m_TargetEntity = target;
- m_TargetOffset = CFixedVector2D();
- m_TargetMinRange = minRange;
- m_TargetMaxRange = maxRange;
- m_FinalGoal = goal;
- m_Tries = 0;
+ // TODO: ought to verify that the returned navcell is in range if it's reachable as a sanity check
- BeginPathing(pos, goal);
+ m_Goal = goal;
- return true;
+ return reachable;
}
-bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal)
{
- // This function closely mirrors MoveToTargetRange - it needs to return true
- // after that Move has completed
-
- CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return false;
-
- CFixedVector2D pos = cmpPosition->GetPosition2D();
-
- CmpPtr<ICmpObstructionManager> cmpObstructionManager(GetSystemEntity());
- if (!cmpObstructionManager)
- return false;
+ CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
+ if (!cmpPathfinder)
+ return;
- bool hasObstruction = false;
- ICmpObstructionManager::ObstructionSquare obstruction;
- CmpPtr<ICmpObstruction> 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;
+ m_RunShortPathValidation = false;
- // See if we're close enough to the target square
- if (maxRange < entity_pos_t::Zero() || distance <= maxRange || previousDistance <= maxRange)
- return true;
+ // this is by how much our waypoints will be apart at most.
+ // this value here seems sensible enough.
+ PathGoal improvedGoal = goal;
+ improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1);
- entity_pos_t circleRadius = halfSize.Length();
+ cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass);
- 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;
+ m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId());
+}
- return circleDistance <= maxRange || previousCircleDistance <= maxRange;
- }
+void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits)
+{
+ CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
+ if (!cmpPathfinder)
+ return;
- // 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<ICmpPosition> cmpTargetPosition(GetSimContext(), target);
- if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
- return false;
+ m_RunShortPathValidation = true;
- CFixedVector2D targetPos = cmpTargetPosition->GetPreviousPosition2D();
- entity_pos_t distance = (pos - targetPos).Length();
+ // 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 + 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)
+ searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
- return minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange);
- }
+ m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId());
}
-void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
-{
- CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), target);
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return;
- CFixedVector2D pos = cmpPosition->GetPosition2D();
+bool CCmpUnitMotion::SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable)
+{
+ // This sets up a new destination, scrap whatever came before.
+ DiscardMove();
- PathGoal goal;
- goal.type = PathGoal::POINT;
- goal.x = pos.X;
- goal.z = pos.Y;
+ m_Destination = SMotionGoal(CFixedVector2D(x, z), range);
+ m_CurrentGoal = m_Destination;
- 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;
+ bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition
- BeginPathing(pos, goal);
+ return reachable;
}
+bool CCmpUnitMotion::SetNewDestinationAsEntity(entity_id_t ent, entity_pos_t range, bool evenUnreachable)
+{
+ // This sets up a new destination, scrap whatever came before.
+ DiscardMove();
+ // validate entity's existence.
+ CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent);
+ if (!cmpPosition || !cmpPosition->IsInWorld())
+ return false;
+ m_Destination = SMotionGoal(ent, range);
+ m_CurrentGoal = m_Destination;
+ bool reachable = RequestNewPath(evenUnreachable); // calls RecomputeGoalPosition
+
+ return reachable;
+}
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector<SOverlayLine>& lines, CColor color)
{
@@ -1785,8 +1388,24 @@
lines.back().m_Color = color;
SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
}
- float x = cmpPosition->GetPosition2D().X.ToFloat();
- float z = cmpPosition->GetPosition2D().Y.ToFloat();
+
+ if (CurrentGoalHasValidPosition())
+ {
+ float x = GetGoalPosition(m_CurrentGoal).X.ToFloat();
+ float z = GetGoalPosition(m_CurrentGoal).Y.ToFloat();
+ lines.push_back(SOverlayLine());
+ lines.back().m_Color = CColor(0.0f, 1.0f, 0.0f, 1.0f);
+ SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.4f, lines.back(), floating);
+ }
+
+ float x = m_Goal.x.ToFloat();
+ float z = m_Goal.z.ToFloat();
+ lines.push_back(SOverlayLine());
+ lines.back().m_Color = CColor(0.0f, 1.0f, 1.0f, 1.0f);
+ SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
+
+ x = cmpPosition->GetPosition2D().X.ToFloat();
+ z = cmpPosition->GetPosition2D().Y.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
@@ -1800,12 +1419,8 @@
if (!m_DebugOverlayEnabled)
return;
- RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH);
- RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH);
-
- for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i)
- collector.Submit(&m_DebugOverlayLongPathLines[i]);
+ RenderPath(m_Path, m_DebugOverlayPathLines, OVERLAY_COLOR_PATH);
- for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
- collector.Submit(&m_DebugOverlayShortPathLines[i]);
+ for (size_t i = 0; i < m_DebugOverlayPathLines.size(); ++i)
+ collector.Submit(&m_DebugOverlayPathLines[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<std::string, std::string> 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<CStr, CStr> 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<SerializeString, SerializeString>()(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<const CMessageUpdate_Final&> (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<std::string, std::string>::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<ICmpUnitMotion> 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<ICmpPosition> cmpPosition(GetEntityHandle());
- if (!cmpPosition || !cmpPosition->IsInWorld())
- return;
-
- CmpPtr<ICmpUnitMotion> 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<std::string, std::string>::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,26 @@
virtual void RemoveShape(tag_t tag) = 0;
/**
+ * Check if the given point is in range of the other point given those parameters
+ */
+ virtual bool IsPointInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+
+ /**
+ * Check if the given point is in range of the target given those parameters
+ */
+ virtual bool IsPointInTargetRange(entity_pos_t x, entity_pos_t z, entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+
+ /**
+ * Check if the given entity is in range of the other point given those parameters
+ */
+ virtual bool IsInPointRange(entity_id_t ent, entity_pos_t px, entity_pos_t pz, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+
+ /**
+ * Check if the given entity is in range of the target given those parameters
+ */
+ virtual bool IsInTargetRange(entity_id_t ent, 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/ICmpObstructionManager.cpp
===================================================================
--- source/simulation2/components/ICmpObstructionManager.cpp
+++ source/simulation2/components/ICmpObstructionManager.cpp
@@ -24,4 +24,6 @@
BEGIN_INTERFACE_WRAPPER(ObstructionManager)
DEFINE_INTERFACE_METHOD_1("SetPassabilityCircular", void, ICmpObstructionManager, SetPassabilityCircular, bool)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpObstructionManager, SetDebugOverlay, bool)
+DEFINE_INTERFACE_METHOD_5("IsInPointRange", bool, ICmpObstructionManager, IsInPointRange, entity_id_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
+DEFINE_INTERFACE_METHOD_4("IsInTargetRange", bool, ICmpObstructionManager, IsInTargetRange, entity_id_t, entity_id_t, entity_pos_t, entity_pos_t)
END_INTERFACE_WRAPPER(ObstructionManager)
Index: source/simulation2/components/ICmpPathfinder.h
===================================================================
--- source/simulation2/components/ICmpPathfinder.h
+++ source/simulation2/components/ICmpPathfinder.h
@@ -89,6 +89,24 @@
virtual Grid<u16> ComputeShoreGrid(bool expandOnWater = false) = 0;
/**
+ * Transform an arbitrary PathGoal into a reachable Point PathGoal, see Hierarchical Pathfinder for details
+ * Return true if the goal was reachable originally, false otherwise.
+ */
+ virtual bool MakeGoalReachable(entity_pos_t x0, entity_pos_t z0, PathGoal &goal, pass_class_t passClass) = 0;
+
+ /**
+ * Gives the closest passable navcell from the given position.
+ * Returns how many navcells away (manhattan) that navcell is.
+ */
+ virtual u32 FindNearestPassableNavcell(entity_pos_t x, entity_pos_t z, u16& outI, u16& outJ, pass_class_t passClass) = 0;
+
+ /**
+ * Returns true if navcell (i0, j0) has the same global region ID as navcell (i1, j1).
+ * i.e. you can reach one from the other.
+ */
+ virtual bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass) = 0;
+
+ /**
* Compute a tile-based path from the given point to the goal, and return the set of waypoints.
* The waypoints correspond to the centers of horizontally/vertically adjacent tiles
* along the path.
Index: source/simulation2/components/ICmpUnitMotion.h
===================================================================
--- source/simulation2/components/ICmpUnitMotion.h
+++ source/simulation2/components/ICmpUnitMotion.h
@@ -36,80 +36,97 @@
public:
/**
- * Attempt to walk into range of a to a given point, or as close as possible.
- * The range is measured from the center of the unit.
- * If the unit is already in range, or cannot move anywhere at all, or if there is
- * some other error, then returns false.
- * Otherwise, returns true and sends a MotionChanged message after starting to move,
- * and sends another MotionChanged after finishing moving.
- * If maxRange is negative, then the maximum range is treated as infinity.
+ * Resets motion and assigns a new 2D position as destination.
+ * Returns false if the position is unreachable (or if the move could not be completed for any other reason).
+ * Otherwise, returns true.
+ * If evenUnreachable is false, and the point is unreachable, then the unit will not start moving.
+ * Otherwise, the unit will try to go to another position as close as possible to the destination.
+ */
+ virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool evenUnreachable) = 0;
+
+ /**
+ * Resets motion and assigns a new entity as destination.
+ * Returns false if the entity is unreachable (or if the move could not be completed for any other reason).
+ * Otherwise, returns true.
+ * If evenUnreachable is false, and the point is unreachable, then the unit will not start moving.
+ * Otherwise, the unit will try to go to another position as close as possible to the destination.
*/
- virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+ virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool evenUnreachable) = 0;
/**
- * Determine wether the givven point is within the given range, using the same measurement
- * as MoveToPointRange.
+ * Turn to look towards the given point.
*/
- virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0;
/**
- * Determine whether the target is within the given range, using the same measurement
- * as MoveToTargetRange.
+ * Turn to look towards the given entity.
*/
- virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+ virtual void FaceTowardsEntity(entity_id_t ent) = 0;
/**
- * Attempt to walk into range of a given target entity, or as close as possible.
- * The range is measured between approximately the edges of the unit and the target, so that
- * maxRange=0 is not unreachably close to the target.
- * If the unit is already in range, or cannot move anywhere at all, or if there is
- * some other error, then returns false.
- * Otherwise, returns true and sends a MotionChanged message after starting to move,
- * and sends another MotionChanged after finishing moving.
- * If maxRange is negative, then the maximum range is treated as infinity.
+ * Determine whether to abort or retry X times if pathing fails.
+ * Generally safer to let it abort and inform us.
*/
- virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+ virtual void SetAbortIfStuck(u8 shouldAbort) = 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.
+ * Stops the unit. Does not clear the destination
+ * so the unit may start moving again next turn.
+ * Mostly used internally but exposed if anybody wants to stop for whatever reason.
*/
- virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0;
+ virtual void StopMoving() = 0;
/**
- * Turn to look towards the given point.
+ * Stop moving, clear any destination, path, and ticket pending.
+ * Basically resets the unit's motion.
+ * Won't send any message.
*/
- virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0;
+ virtual void DiscardMove() = 0;
/**
- * Stop moving immediately.
+ * Asks wether the unit has a path to follow
*/
- virtual void StopMoving() = 0;
+ virtual bool HasValidPath() = 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
@@ -23,67 +23,70 @@
#include "simulation2/scripting/ScriptComponent.h"
BEGIN_INTERFACE_WRAPPER(UnitMotion)
-DEFINE_INTERFACE_METHOD_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
-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_4("SetNewDestinationAsPosition", bool, ICmpUnitMotion, SetNewDestinationAsPosition, entity_pos_t, entity_pos_t, entity_pos_t, bool)
+DEFINE_INTERFACE_METHOD_3("SetNewDestinationAsEntity", bool, ICmpUnitMotion, SetNewDestinationAsEntity, entity_id_t, entity_pos_t, bool)
DEFINE_INTERFACE_METHOD_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t)
+DEFINE_INTERFACE_METHOD_1("FaceTowardsEntity", void, ICmpUnitMotion, FaceTowardsEntity, entity_id_t)
+DEFINE_INTERFACE_METHOD_1("SetAbortIfStuck", void, ICmpUnitMotion, SetAbortIfStuck, u8)
DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving)
-DEFINE_INTERFACE_METHOD_0("GetCurrentSpeed", fixed, ICmpUnitMotion, GetCurrentSpeed)
+DEFINE_INTERFACE_METHOD_0("DiscardMove", void, ICmpUnitMotion, DiscardMove)
+DEFINE_INTERFACE_METHOD_0("HasValidPath", bool, ICmpUnitMotion, HasValidPath)
+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)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool)
END_INTERFACE_WRAPPER(UnitMotion)
+
class CCmpUnitMotionScripted : public ICmpUnitMotion
{
public:
DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted)
- virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
+ virtual bool SetNewDestinationAsPosition(entity_pos_t x, entity_pos_t z, entity_pos_t range, bool UNUSED(evenUnreachable))
{
- return m_Script.Call<bool>("MoveToPointRange", x, z, minRange, maxRange);
+ return m_Script.Call<bool>("SetNewDestinationAsPosition", x, z, range, true);
}
- virtual bool IsInPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
+ virtual bool SetNewDestinationAsEntity(entity_id_t target, entity_pos_t range, bool UNUSED(evenUnreachable))
{
- return m_Script.Call<bool>("IsInPointRange", x, z, minRange, maxRange);
+ return m_Script.Call<bool>("SetNewDestinationAsEntity", target, range, true);
}
- virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
{
- return m_Script.Call<bool>("IsInTargetRange", target, minRange, maxRange);
+ m_Script.CallVoid("FaceTowardsPoint", x, z);
}
- virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
+ virtual void FaceTowardsEntity(entity_id_t ent)
{
- return m_Script.Call<bool>("MoveToTargetRange", target, minRange, maxRange);
+ m_Script.CallVoid("FaceTowardsEntity", ent);
}
- virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z)
+ virtual void DiscardMove()
{
- m_Script.CallVoid("MoveToFormationOffset", target, x, z);
+ m_Script.CallVoid("DiscardMove");
}
- virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
+ virtual void StopMoving()
{
- m_Script.CallVoid("FaceTowardsPoint", x, z);
+ m_Script.CallVoid("CompleteMove");
}
- virtual void StopMoving()
+ virtual void SetAbortIfStuck(u8 shouldAbort)
{
- m_Script.CallVoid("StopMoving");
+ m_Script.CallVoid("SetAbortIfStuck", shouldAbort);
}
- virtual fixed GetCurrentSpeed()
+ virtual fixed GetActualSpeed()
{
- return m_Script.Call<fixed>("GetCurrentSpeed");
+ return m_Script.Call<fixed>("GetActualSpeed");
}
virtual void SetSpeed(fixed speed)
@@ -91,19 +94,34 @@
m_Script.CallVoid("SetSpeed", speed);
}
- virtual bool IsMoving()
+ virtual fixed GetTopSpeedRatio()
+ {
+ return m_Script.Call<fixed>("GetTopSpeedRatio");
+ }
+
+ virtual bool HasValidPath()
{
- return m_Script.Call<bool>("IsMoving");
+ return m_Script.Call<bool>("HasValidPath");
}
- virtual fixed GetWalkSpeed()
+ virtual bool IsActuallyMoving()
{
- return m_Script.Call<fixed>("GetWalkSpeed");
+ return m_Script.Call<bool>("IsActuallyMoving");
}
- virtual fixed GetRunSpeed()
+ virtual bool IsTryingToMove()
{
- return m_Script.Call<fixed>("GetRunSpeed");
+ return m_Script.Call<bool>("IsTryingToMove");
+ }
+
+ virtual fixed GetSpeed()
+ {
+ return m_Script.Call<fixed>("GetSpeed");
+ }
+
+ virtual fixed GetBaseSpeed()
+ {
+ return m_Script.Call<fixed>("GetBaseSpeed");
}
virtual void SetFacePointAfterMove(bool facePointAfterMove)
@@ -116,6 +134,11 @@
return m_Script.Call<pass_class_t>("GetPassabilityClass");
}
+ virtual fixed GetSpeedRatio()
+ {
+ return fixed::FromInt(1);
+ }
+
virtual std::string GetPassabilityClassName()
{
return m_Script.Call<std::string>("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/components/tests/test_Pathfinder.h
===================================================================
--- source/simulation2/components/tests/test_Pathfinder.h
+++ source/simulation2/components/tests/test_Pathfinder.h
@@ -17,8 +17,11 @@
#include "simulation2/system/ComponentTest.h"
+#define TEST
+
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpPathfinder.h"
+#include "simulation2/components/CCmpPathfinder_Common.h"
#include "graphics/MapReader.h"
#include "graphics/Terrain.h"
@@ -64,6 +67,129 @@
TS_ASSERT_EQUALS((Pathfinding::NAVCELL_SIZE >> 1).ToInt_RoundToZero(), Pathfinding::NAVCELL_SIZE_LOG2);
}
+ void hierarchical_globalRegions_testmap(std::wstring map)
+ {
+ CTerrain terrain;
+
+ CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain);
+ sim2.LoadDefaultScripts();
+ sim2.ResetState();
+
+ CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself
+
+ LDR_BeginRegistering();
+ mapReader->LoadMap(map,
+ sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue,
+ &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ &sim2, &sim2.GetSimContext(), -1, false);
+ LDR_EndRegistering();
+ TS_ASSERT_OK(LDR_NonprogressiveLoad());
+
+ sim2.Update(0);
+
+ CmpPtr<ICmpPathfinder> cmpPathfinder(sim2, SYSTEM_ENTITY);
+
+ pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default");
+ HierarchicalPathfinder& hier = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.GetHierarchicalPathfinder();
+
+ std::map<HierarchicalPathfinder::RegionID, HierarchicalPathfinder::GlobalRegionID> globalRegions = hier.m_GlobalRegions[obstructionsMask];
+
+ for (u8 cj = 0; cj < hier.m_ChunksH; cj += 2)
+ for (u8 ci = 0; ci < hier.m_ChunksW; ci += 2)
+ for(u16 i : hier.GetChunk(ci, cj, obstructionsMask).m_RegionsID)
+ {
+ std::set<HierarchicalPathfinder::RegionID> reachables;
+ hier.FindReachableRegions(HierarchicalPathfinder::RegionID{ci, cj, i}, reachables, obstructionsMask);
+ HierarchicalPathfinder::GlobalRegionID ID = globalRegions[HierarchicalPathfinder::RegionID{ci, cj, i}];
+ for (HierarchicalPathfinder::RegionID region : reachables)
+ TS_ASSERT_EQUALS(ID, globalRegions[region]);
+ }
+ }
+
+ void test_hierarchical_globalRegions()
+ {
+ // This test validates that the hierarchical's pathfinder global regions are in accordance with its regions
+ // IE it asserts that, for any two regions A and B of the hierarchical pathfinder, if one can find a path from A to B
+ // then A and B have the same global region.
+ std::vector<std::wstring> maps = { L"maps/scenarios/Peloponnese.pmp", L"maps/skirmishes/Corinthian Isthmus (2).pmp", L"maps/skirmishes/Greek Acropolis (2).pmp" };
+
+// disable in debug mode, creating the simulation and running the initial turn is too slow and tends to OOM in debug mode.
+#ifndef DEBUG
+ for (std::wstring t : maps)
+ hierarchical_globalRegions_testmap(t);
+#endif
+ }
+
+ void hierarchical_update_testmap(std::wstring map)
+ {
+ CTerrain terrain;
+
+ CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain);
+ sim2.LoadDefaultScripts();
+ sim2.ResetState();
+
+ CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself
+
+ LDR_BeginRegistering();
+ mapReader->LoadMap(map,
+ sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue,
+ &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ &sim2, &sim2.GetSimContext(), -1, false);
+ LDR_EndRegistering();
+ TS_ASSERT_OK(LDR_NonprogressiveLoad());
+
+ sim2.Update(0);
+
+ CmpPtr<ICmpPathfinder> cmpPathfinder(sim2, SYSTEM_ENTITY);
+
+ pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default");
+ HierarchicalPathfinder& hier = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.GetHierarchicalPathfinder();
+
+ // make copies
+ const auto pristine_GR = hier.m_GlobalRegions;
+ const auto pristine_Chunks = hier.m_Chunks;
+ const HierarchicalPathfinder::EdgesMap pristine_Edges = hier.m_Edges.at(obstructionsMask);
+
+ Grid<NavcellData>* pathfinderGrid = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.m_Grid;
+
+ Grid<u8> dirtyGrid(hier.m_ChunksW * HierarchicalPathfinder::CHUNK_SIZE,hier.m_ChunksH * HierarchicalPathfinder::CHUNK_SIZE);
+ srand(1234);
+
+ size_t tries = 20;
+ for (size_t i = 0; i < tries; ++i)
+ {
+ // Dirty a random one
+ dirtyGrid.reset();
+ u8 ci = rand() % (hier.m_ChunksW-10) + 8;
+ u8 cj = rand() % (hier.m_ChunksH-10) + 8;
+ dirtyGrid.set(ci * HierarchicalPathfinder::CHUNK_SIZE + 4, cj * HierarchicalPathfinder::CHUNK_SIZE + 4, 1);
+
+ hier.Update(pathfinderGrid, dirtyGrid);
+
+ // Formally speaking we should rather validate that regions exist with the same pixels, but so far
+ // re-initing regions will keep the same IDs for the same pixels so this is OK.
+ TS_ASSERT_EQUALS(hier.m_Chunks.at(obstructionsMask), pristine_Chunks.at(obstructionsMask));
+ // same here
+ TS_ASSERT_EQUALS(pristine_Edges, hier.m_Edges.at(obstructionsMask));
+
+ // TODO: ought to test global regions, but those should be OK if the connections are OK
+ // and glboal regions ID can change on update making it annoying.
+ }
+ }
+
+ void test_hierarchical_update()
+ {
+ // This test validates that the "Update" function of the hierarchical pathfinder
+ // ends up in a correct state (by comparing it with the clean, "Recompute"-d state).
+ std::vector<std::wstring> maps = { L"maps/scenarios/Peloponnese.pmp", L"maps/skirmishes/Corinthian Isthmus (2).pmp", L"maps/skirmishes/Greek Acropolis (2).pmp" };
+
+// disable in debug mode, creating the simulation and running the initial turn is too slow and tends to OOM in debug mode.
+#ifndef DEBUG
+ for (std::wstring t : maps)
+ hierarchical_update_testmap(t);
+#endif
+ }
+
void test_performance_DISABLED()
{
CTerrain terrain;
@@ -285,6 +411,333 @@
stream << "</svg>\n";
}
+ static const size_t scale = 1;
+
+ void MakeGoalReachable_testIteration(CStr& map, u16 sx, u16 sz, u16 gx, u16 gz)
+ {
+ int colors[26][3] = {
+ { 255, 0, 0 },
+ { 0, 255, 0 },
+ { 0, 0, 255 },
+ { 255, 255, 0 },
+ { 255, 0, 255 },
+ { 0, 255, 255 },
+ { 255, 255, 255 },
+
+ { 127, 0, 0 },
+ { 0, 127, 0 },
+ { 0, 0, 127 },
+ { 127, 127, 0 },
+ { 127, 0, 127 },
+ { 0, 127, 127 },
+ { 127, 127, 127},
+
+ { 255, 127, 0 },
+ { 127, 255, 0 },
+ { 255, 0, 127 },
+ { 127, 0, 255},
+ { 0, 255, 127 },
+ { 0, 127, 255},
+ { 255, 127, 127},
+ { 127, 255, 127},
+ { 127, 127, 255},
+
+ { 127, 255, 255 },
+ { 255, 127, 255 },
+ { 255, 255, 127 },
+ };
+
+ // Load up a map, dump hierarchical regions
+ // From a few positions test making a few positions reachable.
+ // Check performance and output results as svg files so user can verify sanity.
+
+ CTerrain terrain;
+
+ CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain);
+ sim2.LoadDefaultScripts();
+ sim2.ResetState();
+
+ CMapReader* mapReader = new CMapReader(); // it'll call "delete this" itself
+
+ LDR_BeginRegistering();
+ mapReader->LoadMap(map.FromUTF8(),
+ sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue,
+ &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ &sim2, &sim2.GetSimContext(), -1, false);
+ LDR_EndRegistering();
+ TS_ASSERT_OK(LDR_NonprogressiveLoad());
+
+ sim2.Update(0);
+
+ map.Replace(".pmp",""); map.Replace("/","");
+ CStr path("MGR_" + map + "_" + CStr::FromUInt(sx) + "_" + CStr::FromUInt(sz) + "_" + CStr::FromUInt(gx) + "_" + CStr::FromUInt(gz) + ".html");
+ std::cout << path << std::endl;
+ std::ofstream stream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
+
+ CmpPtr<ICmpObstructionManager> cmpObstructionManager(sim2, SYSTEM_ENTITY);
+ CmpPtr<ICmpPathfinder> cmpPathfinder(sim2, SYSTEM_ENTITY);
+
+ pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default");
+ const Grid<NavcellData>& obstructions = cmpPathfinder->GetPassabilityGrid();
+
+ // Dump as canvas. This is terrible code but who cares.
+ stream << "<!DOCTYPE html>\n";
+ stream << "<head>\n"
+ "<style>\n"
+ "canvas { position:absolute; left:0px; top: 0px; }"
+ "canvas #hier { opacity:0.5; }"
+ "input[type=\"checkbox\"] { height: 50px; width:50px; background:red; -webkit-appearance:none; }input[type=\"checkbox\"]:checked { background:blue; }"
+ "p.pixel { z-index:20000; display:block; position:absolute; width: " << scale << "px; height: " << scale << "px;}"
+ "</style>\n"
+ "</head>\n"
+ "<body>";
+
+ stream << "<p style='z-index: 200; background: white; position: absolute;transform:scale(2); transform-origin:0 0;'>\n";
+ stream << "<input type='checkbox' checked='true' id='gridcb' onchange=\"var checked = document.getElementById('gridcb').checked; document.getElementById('grid').style.display = checked ? 'block' :' none';\">Display Grid</input>";
+ stream << "<input type='checkbox' checked='true' id='hiercb' onchange=\"var checked = document.getElementById('hiercb').checked; document.getElementById('hier').style.display = checked ? 'block' :' none';\">Display Hierarchical grid </input>";
+ stream << "<input type='checkbox' checked='true' id='hier2cb' onchange=\"var checked = document.getElementById('hier2cb').checked; document.getElementById('hier2').style.display = checked ? 'block' :' none';\">Display Global Regions </input>";
+ stream << "<input type='checkbox' checked='true' id='pathcb' onchange=\"var checked = document.getElementById('pathcb').checked; document.getElementById('path').style.display = checked ? 'block' :' none';\">Display Path search </input>";
+ stream << "<input type='checkbox' checked='true' id='path2cb' onchange=\"var checked = document.getElementById('path2cb').checked; document.getElementById('path2').style.display = checked ? 'block' :' none';\">Display path lookups </input>";
+ stream << "<input id='scaleinput' type='number' value='1' onchange=\"var nscale = +document.getElementById('scaleinput').value; scale=nscale; printGrid(scale);printHier(scale);\">";
+ stream << "<input id='totoro' type='range' min='0' max='100' value='1' oninput=\"var nscale = +document.getElementById('totoro').value; printPath(scale, nscale);\">\n";
+ stream << "</p>";
+
+ stream << "<canvas id='grid' width='" << obstructions.m_W*scale << "' height='" << obstructions.m_H*scale << "'></canvas>";
+
+ // set up grid
+ stream << "<script>"
+ "var scale = " << scale << ";\n"
+ "function printGrid(scale) {\n"
+ "var grid = document.getElementById('grid');\n"
+ "grid.width = scale * " << obstructions.m_W << ";"
+ "grid.height = scale * " << obstructions.m_H << ";"
+ "var context = grid.getContext('2d');"
+ "context.clearRect(0, 0, grid.width, grid.height);"
+ "context.fillStyle = '#000000';\n";
+ for (u16 j = 0; j < obstructions.m_H; ++j)
+ for (u16 i = 0; i < obstructions.m_W; i++)
+ if (obstructions.get(i, j) & obstructionsMask)
+ {
+ u16 i1 = i;
+ for (; i1 < obstructions.m_W; i1++)
+ if (!(obstructions.get(i1, j) & obstructionsMask))
+ break;
+ stream << "context.fillRect(" << i << " * scale," << j << " * scale," << (i1 - i) << " * scale, scale);\n";
+ i = i1 - 1;
+ }
+ stream << "};";
+ stream << "printGrid(scale)";
+ stream << "</script>\n";
+
+ // Dump hierarchical regions on another one.
+ stream << "<canvas id='hier' width='" << obstructions.m_W*scale << "' height='" << obstructions.m_H*scale << "'></canvas>";
+ stream << "<canvas id='hier2' width='" << obstructions.m_W*scale << "' height='" << obstructions.m_H*scale << "'></canvas>";
+
+ HierarchicalPathfinder& hier = ((CCmpPathfinder*)cmpPathfinder.operator->())->m_LongPathfinder.GetHierarchicalPathfinder();
+
+ stream << "<script>"
+ "function printHier(scale) {\n"
+ "var hier = document.getElementById('hier');\n"
+ "hier.width = scale * " << obstructions.m_W << ";"
+ "hier.height = scale * " << obstructions.m_H << ";"
+ "var context = hier.getContext('2d');"
+ "context.clearRect(0, 0, hier.width, hier.height);"
+ "context.fillStyle = '#000000';\n";
+ for (u16 j = 0; j < obstructions.m_H; ++j)
+ for (u16 i = 0; i < obstructions.m_W; i++)
+ {
+ u16 st = hier.Get(i, j, obstructionsMask).r;
+ u16 ci = i / hier.CHUNK_SIZE;
+ u16 cj = j / hier.CHUNK_SIZE;
+ u16 i1 = i;
+ for (; i1 < obstructions.m_W; ++i1)
+ if (hier.Get(i1, j, obstructionsMask).r != st || i1 / hier.CHUNK_SIZE != ci)
+ break;
+
+ if (st == 0)
+ stream << "context.fillStyle = 'rgba(0,0,0,1.0)';\n";
+ else if (st == 0xFFFF)
+ stream << "context.fillStyle = 'rgba(255,0,255,0.5)';\n";
+ else
+ {
+ size_t c = (st + ci*5 + cj*7) % ARRAY_SIZE(colors);
+ stream << "context.fillStyle = 'rgba("<<colors[c][0]<<","<<colors[c][1]<<","<<colors[c][2]<<",1.0)';\n";
+ }
+ stream << "context.fillRect(" << i << " * scale," << j << " * scale," << (i1 - i) << " * scale, scale);\n";
+ i = i1 - 1;
+ }
+ stream << "};";
+ stream << "printHier(scale);\n"
+ "function printHier2(scale) {\n"
+ "var hier2 = document.getElementById('hier2');\n"
+ "hier2.width = scale * " << obstructions.m_W << ";"
+ "hier2.height = scale * " << obstructions.m_H << ";"
+ "var context = hier2.getContext('2d');"
+ "context.clearRect(0, 0, hier2.width, hier2.height);"
+ "context.fillStyle = '#000000';\n";
+ for (u16 j = 0; j < obstructions.m_H; ++j)
+ for (u16 i = 0; i < obstructions.m_W; i++)
+ {
+ u32 globalID = hier.GetGlobalRegion(i,j,obstructionsMask);
+ u16 i1 = i;
+ for (; i1 < obstructions.m_W; ++i1)
+ if (hier.GetGlobalRegion(i1,j,obstructionsMask) != globalID)
+ break;
+
+ if (globalID == 0)
+ stream << "context.fillStyle = 'rgba(0,0,0,1.0)';\n";
+ else
+ {
+ size_t c = globalID % ARRAY_SIZE(colors);
+ stream << "context.fillStyle = 'rgba("<<colors[c][0]<<","<<colors[c][1]<<","<<colors[c][2]<<",1.0)';\n";
+ }
+ stream << "context.fillRect(" << i << " * scale," << j << " * scale," << (i1 - i) << " * scale, scale);\n";
+ i = i1 - 1;
+ }
+ stream << "};";
+ stream << "printHier2(scale)";
+ stream << "</script>\n";
+
+ // Ok let's check out MakeGoalReachable
+ // pick a point
+ fixed X,Z;
+ X = fixed::FromInt(sx);
+ Z = fixed::FromInt(sz);
+ u16 gridSize = obstructions.m_W;
+ // Convert the start coordinates to tile indexes
+ u16 i0, j0;
+ Pathfinding::NearestNavcell(X, Z, i0, j0, gridSize, gridSize);
+
+ // Dump as HTML so that it's on top and add fancy shadows so it's easy to see.
+ stream << "<p class='pixel' style='z-index:500000; border-radius:100%;box-shadow: 0 0 0 10px green, 0 0 0 12px black; left:" << i0 * scale << "px; top:" << j0 * scale << "px;'></p>";
+
+ hier.FindNearestPassableNavcell(i0, j0, obstructionsMask);
+ stream << "<p class='pixel' style='border-radius:100%;box-shadow: 0 0 0 20px blue, 0 0 0 22px red; left:" << i0 * scale << "px; top:" << j0 * scale << "px;'></p>";
+
+ // Make the goal reachable. This includes shortening the path if the goal is in a non-passable
+ // region, transforming non-point goals to reachable point goals, etc.
+
+ PathGoal goal;
+ goal.type = PathGoal::POINT;
+ goal.x = fixed::FromInt(gx);
+ goal.z = fixed::FromInt(gz);
+ goal.u = CFixedVector2D(fixed::FromInt(1), fixed::Zero());
+ goal.v = CFixedVector2D(fixed::Zero(),fixed::FromInt(1));
+ goal.hh = fixed::FromInt(0);
+ goal.hw = fixed::FromInt(0);
+
+ u16 i1, j1;
+ Pathfinding::NearestNavcell(goal.x, goal.z, i1, j1, gridSize, gridSize);
+ stream << "<p class='pixel' style='z-index:500000; border-radius:100%;box-shadow: 0 0 0 10px red, 0 0 0 12px white; left:" << i1 * scale << "px; top:" << j1 * scale << "px;'></p>";
+
+ stream << "<canvas id='path' width='" << obstructions.m_W*scale << "' height='" << obstructions.m_H*scale << "'></canvas>";
+ stream << "<canvas id='path2' width='" << obstructions.m_W*scale << "' height='" << obstructions.m_H*scale << "'></canvas>";
+
+ stream << "<script>"
+ "var maxStep = 0;"
+ "function printPath(scale, step) {\n"
+ "var path = document.getElementById('path');\n"
+ "path.width = scale * " << obstructions.m_W << ";"
+ "path.height = scale * " << obstructions.m_H << ";"
+ "var context = path.getContext('2d');"
+ "context.clearRect(0, 0, path.width, path.height);"
+ "var path2 = document.getElementById('path2');\n"
+ "path2.width = scale * " << obstructions.m_W << ";"
+ "path2.height = scale * " << obstructions.m_H << ";"
+ "var context2 = path2.getContext('2d');"
+ "context2.clearRect(0, 0, path2.width, path2.height);";
+
+ PathGoal goalCopy = goal;
+ hier.MakeGoalReachable(i0, j0, goalCopy, obstructionsMask);//, stream);
+
+ stream << "};";
+ stream << "printPath(scale,10000);";
+ stream << "document.getElementById('totoro').max = maxStep";
+ stream << "</script>\n";
+
+ Pathfinding::NearestNavcell(goalCopy.x, goalCopy.z, i1, j1, gridSize, gridSize);
+ stream << "<p class='pixel' style='border-radius:100%;box-shadow: 0 0 0 20px green, 0 0 0 25px red; left:" << i1 * scale << "px; top:" << j1 * scale << "px;'></p>";
+ stream << "</body>\n";
+ stream.close();
+
+ // Perf test. This is a little primitive, but should work well enough to give an idea of the algo.
+ double t = timer_Time();
+
+ srand(1234);
+ for (size_t j = 0; j < 10000; ++j)
+ {
+ PathGoal oldGoal = goal;
+ hier.MakeGoalReachable(i0, j0, goal, obstructionsMask);
+ goal = oldGoal;
+ }
+
+ t = timer_Time() - t;
+ printf("\nPoint Goal: [%f]\n", t);
+
+ goal.type = PathGoal::CIRCLE;
+ goal.hh = fixed::FromInt(40);
+ goal.hw = fixed::FromInt(40);
+
+ t = timer_Time();
+
+ srand(1234);
+ for (size_t j = 0; j < 10000; ++j)
+ {
+ PathGoal oldGoal = goal;
+ hier.MakeGoalReachable(i0, j0, goal, obstructionsMask);
+ goal = oldGoal;
+ }
+
+ t = timer_Time() - t;
+ printf("\nCircle Goal: [%f]\n", t);
+ }
+
+ void test_MakeGoalReachable_performance_DISABLED()
+ {
+ struct test
+ {
+ CStr map;
+ u16 sx;
+ u16 sz;
+ u16 gx;
+ u16 gz;
+ };
+ /*
+ * Initially this was done to compare A* to the earlier flood-fill method, which has since been removed.
+ * Compare performance on a few cases:
+ * - short path, good case for the flood fill (it finds immediately the point/circle and stops)
+ * - short path, bad case for the flood fill (it will not find the correct region right away, so it's literally about 100x slower than the former)
+ * - long path around the bend, close to worst-case for A*
+ * - Long unreachable path, but the "closest point" is reachable in almost a straight direction.
+ * - Inverse of the former (the region to fill is much smaller)
+ * - large island, A* still has a lot to fill here
+ * - straight with obstructions
+ * - straight, fewer obstructions
+ * - bad case (U shape around the start containing A*)
+ * - bad case: U shape + unreachable. We need to return something reasonably close, not in the first U
+ * - bad calse: two U shapes tripping A*
+ */
+ std::vector<test> maps = {
+ { "maps/scenarios/Peloponnese.pmp", 600, 800, 800, 800 },
+ { "maps/scenarios/Peloponnese.pmp", 600, 800, 600, 900 },
+ { "maps/scenarios/Peloponnese.pmp", 600, 800, 770, 1400 },
+ { "maps/scenarios/Peloponnese.pmp", 1000, 300, 1500, 1450 },
+ { "maps/scenarios/Peloponnese.pmp", 1500, 1450, 1000, 300 },
+ { "maps/skirmishes/Corsica and Sardinia (4).pmp", 300, 1300, 1300, 300 },
+ { "maps/skirmishes/Alpine_Mountains_(3).pmp", 200, 200, 800, 800 },
+ { "maps/skirmishes/Corinthian Isthmus (2).pmp", 200, 200, 800, 800 },
+ { "maps/skirmishes/Mediterranean Cove (2).pmp", 200, 200, 800, 800 },
+ { "maps/skirmishes/Dueling Cliffs (3v3).pmp", 200, 200, 800, 800 },
+ { "maps/skirmishes/Dueling Cliffs (3v3).pmp", 350, 200, 900, 900 },
+ { "maps/skirmishes/Dueling Cliffs (3v3).pmp", 200, 200, 950, 950 },
+ };
+
+ for (auto t : maps)
+ {
+ MakeGoalReachable_testIteration(t.map, t.sx, t.sz, t.gx, t.gz);
+ }
+ }
+
void DumpPath(std::ostream& stream, int i0, int j0, int i1, int j1, CmpPtr<ICmpPathfinder>& cmpPathfinder)
{
entity_pos_t x0 = entity_pos_t::FromInt(i0);
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/helpers/HierarchicalPathfinder.h
===================================================================
--- source/simulation2/helpers/HierarchicalPathfinder.h
+++ source/simulation2/helpers/HierarchicalPathfinder.h
@@ -24,10 +24,13 @@
#include "Render.h"
#include "graphics/SColor.h"
+#include <boost/container/flat_map.hpp>
+#include <boost/container/flat_set.hpp>
+
/**
* Hierarchical pathfinder.
*
- * It doesn't find shortest paths, but deals with connectivity.
+ * Deals with connectivity (can point A reach point B?)
*
* The navcell-grid representation of the map is split into fixed-size chunks.
* Within a chunk, each maximal set of adjacently-connected passable navcells
@@ -35,18 +38,31 @@
* Each region is a vertex in the hierarchical pathfinder's graph.
* When two regions in adjacent chunks are connected by passable navcells,
* the graph contains an edge between the corresponding two vertexes.
- * (There will never be an edge between two regions in the same chunk.)
+ * (by design, there can never be an edge between two regions in the same chunk.)
+ *
+ * Those fixed-size chunks are used to efficiently compute "global regions" by effectively flood-filling.
+ * Those can then be used to immediately determine if two reachables points are connected
+ *
+ * The main use of this class is to convert an arbitrary PathGoal to a reachable navcell
+ * This happens in MakeGoalReachable, which implements A* over the chunks.
+ * Currently, the resulting path is unused.
*
- * Since regions are typically fairly large, it is possible to determine
- * connectivity between any two navcells by mapping them onto their appropriate
- * region and then doing a relatively small graph search.
*/
+#ifdef TEST
+class TestCmpPathfinder;
+#endif
+
class HierarchicalOverlay;
class HierarchicalPathfinder
{
+#ifdef TEST
+ friend class TestCmpPathfinder;
+#endif
public:
+ typedef u32 GlobalRegionID;
+
struct RegionID
{
u8 ci, cj; // chunk ID
@@ -54,7 +70,7 @@
RegionID(u8 ci, u8 cj, u16 r) : ci(ci), cj(cj), r(r) { }
- bool operator<(RegionID b) const
+ bool operator<(const RegionID& b) const
{
// Sort by chunk ID, then by per-chunk region ID
if (ci < b.ci)
@@ -68,7 +84,7 @@
return r < b.r;
}
- bool operator==(RegionID b) const
+ bool operator==(const RegionID& b) const
{
return ((ci == b.ci) && (cj == b.cj) && (r == b.r));
}
@@ -89,18 +105,25 @@
bool IsChunkDirty(int ci, int cj, const Grid<u8>& dirtinessGrid) const;
RegionID Get(u16 i, u16 j, pass_class_t passClass);
+ GlobalRegionID GetGlobalRegion(u16 i, u16 j, pass_class_t passClass);
/**
- * Updates @p goal so that it's guaranteed to be reachable from the navcell
+ * Updates @p goal to a point goal guaranteed to be reachable from the original navcell
* @p i0, @p j0 (which is assumed to be on a passable navcell).
*
- * If the goal is not reachable, it is replaced with a point goal nearest to
- * the goal center.
+ * If the goal is not reachable, it is replaced with an acceptable point goal
+ * This function does not necessarily return the closest navcell to the goal
+ * but the one with the lowest f score of the A* algorithm.
+ * This means it is usually a tradeoff between walking time and distance to the goal.
*
* In the case of a non-point reachable goal, it is replaced with a point goal
* at the reachable navcell of the goal which is nearest to the starting navcell.
+ * TODO: since A* is used, it could return the reachable navcell nearest to the penultimate region visited.
+ * which is probably better (imagine a path that must bend around).
+ *
+ * @returns true if the goal was reachable, false otherwise.
*/
- void MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass);
+ bool MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass);
/**
* Updates @p i, @p j (which is assumed to be an impassable navcell)
@@ -125,12 +148,12 @@
private:
static const u8 CHUNK_SIZE = 96; // number of navcells per side
- // TODO PATHFINDER: figure out best number. Probably 64 < n < 128
+ // TODO: figure out best number. Probably 64 < n < 128
struct Chunk
{
u8 m_ChunkI, m_ChunkJ; // chunk ID
- u16 m_NumRegions; // number of local region IDs (starting from 1)
+ std::vector<u16> m_RegionsID; // IDs of local region, without 0
u16 m_Regions[CHUNK_SIZE][CHUNK_SIZE]; // local region ID per navcell
cassert(CHUNK_SIZE*CHUNK_SIZE/2 < 65536); // otherwise we could overflow m_NumRegions with a checkerboard pattern
@@ -144,16 +167,41 @@
void RegionNavcellNearest(u16 r, int iGoal, int jGoal, int& iBest, int& jBest, u32& dist2Best) const;
bool RegionNearestNavcellInGoal(u16 r, u16 i0, u16 j0, const PathGoal& goal, u16& iOut, u16& jOut, u32& dist2Best) const;
+
+#ifdef TEST
+ bool operator==(const Chunk& b) const
+ {
+ return (m_ChunkI == b.m_ChunkI && m_ChunkJ == b.m_ChunkJ && m_RegionsID == b.m_RegionsID && memcmp(&m_Regions, &b.m_Regions, sizeof(u16) * CHUNK_SIZE * CHUNK_SIZE) == 0);
+ }
+#endif
};
typedef std::map<RegionID, std::set<RegionID> > EdgesMap;
- void FindEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges);
+ void RecomputeAllEdges(pass_class_t passClass, EdgesMap& edges);
+ void UpdateEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges);
void FindReachableRegions(RegionID from, std::set<RegionID>& reachable, pass_class_t passClass);
void FindPassableRegions(std::set<RegionID>& regions, pass_class_t passClass);
+ void FindGoalRegions(u16 gi, u16 gj, const PathGoal& goal, std::set<RegionID>& regions, pass_class_t passClass);
+
+ /*
+ * Helpers for the A* implementation of MakeGoalReachable.
+ * We reuse flat_XX containers to have good cache locality and avoid the cost of allocating memory. Flat_XX implementa map/set as a sorted vector
+ */
+ boost::container::flat_map<RegionID, HierarchicalPathfinder::RegionID> m_Astar_Predecessor;
+ boost::container::flat_map<RegionID, int> m_Astar_GScore;
+ boost::container::flat_map<RegionID, int> m_Astar_FScore;
+ boost::container::flat_set<RegionID> m_Astar_ClosedNodes;
+ boost::container::flat_set<RegionID> m_Astar_OpenNodes;
+
+ inline int DistBetween(const RegionID& a, const RegionID& b)
+ {
+ return (abs(a.ci - b.ci) + abs(a.cj - b.cj)) * CHUNK_SIZE - 30;
+ };
+
/**
* Updates @p iGoal and @p jGoal to the navcell that is the nearest to the
* initial goal coordinates, in one of the given @p regions.
@@ -174,6 +222,9 @@
std::map<pass_class_t, EdgesMap> m_Edges;
+ std::map<pass_class_t, std::map<RegionID, GlobalRegionID>> m_GlobalRegions;
+ std::vector<GlobalRegionID> m_AvailableGlobalRegionIDs; // TODO: actually push back deleted global regions here.
+
// Passability classes for which grids will be updated when calling Update
std::map<std::string, pass_class_t> m_PassClassMasks;
Index: source/simulation2/helpers/HierarchicalPathfinder.cpp
===================================================================
--- source/simulation2/helpers/HierarchicalPathfinder.cpp
+++ source/simulation2/helpers/HierarchicalPathfinder.cpp
@@ -111,13 +111,12 @@
}
// Directly point the root ID
- m_NumRegions = 0;
for (u16 i = 1; i < regionID+1; ++i)
{
- if (connect[i] == i)
- ++m_NumRegions;
- else
+ if (connect[i] != i)
connect[i] = RootID(i, connect);
+ if (std::find(m_RegionsID.begin(),m_RegionsID.end(), connect[i]) == m_RegionsID.end())
+ m_RegionsID.push_back(connect[i]);
}
// Replace IDs by the root ID
@@ -224,6 +223,8 @@
{
case PathGoal::POINT:
{
+ if (gi/CHUNK_SIZE != m_ChunkI || gj/CHUNK_SIZE != m_ChunkJ)
+ return false;
if (m_Regions[gj-m_ChunkJ * CHUNK_SIZE][gi-m_ChunkI * CHUNK_SIZE] == r)
{
iOut = gi;
@@ -366,6 +367,10 @@
m_Chunks.clear();
m_Edges.clear();
+ // reset global regions
+ m_AvailableGlobalRegionIDs.clear();
+ m_AvailableGlobalRegionIDs.push_back(1);
+
for (auto& passClassMask : allPassClasses)
{
pass_class_t passClass = passClassMask.second;
@@ -381,16 +386,35 @@
}
// Construct the search graph over the regions
-
EdgesMap& edges = m_Edges[passClass];
+ RecomputeAllEdges(passClass, edges);
- for (int cj = 0; cj < m_ChunksH; ++cj)
- {
- for (int ci = 0; ci < m_ChunksW; ++ci)
- {
- FindEdges(ci, cj, passClass, edges);
- }
- }
+ // Spread global regions.
+ std::map<RegionID, GlobalRegionID>& globalRegion = m_GlobalRegions[passClass];
+ globalRegion.clear();
+ for (u8 cj = 0; cj < m_ChunksH; ++cj)
+ for (u8 ci = 0; ci < m_ChunksW; ++ci)
+ for (u16 rid : GetChunk(ci, cj, passClass).m_RegionsID)
+ {
+ RegionID reg{ci,cj,rid};
+ if (globalRegion.find(reg) == globalRegion.end())
+ {
+ GlobalRegionID ID = m_AvailableGlobalRegionIDs.back();
+ m_AvailableGlobalRegionIDs.pop_back();
+ if (m_AvailableGlobalRegionIDs.empty())
+ m_AvailableGlobalRegionIDs.push_back(ID+1);
+
+ globalRegion.insert({ reg, ID });
+ // avoid creating an empty link if possible, FindReachableRegions uses [] which calls the default constructor
+ if (edges.find(reg) != edges.end())
+ {
+ std::set<RegionID> reachable;
+ FindReachableRegions(reg, reachable, passClass);
+ for (const RegionID& region : reachable)
+ globalRegion.insert({ region, ID });
+ }
+ }
+ }
}
if (m_DebugOverlay)
@@ -405,9 +429,10 @@
{
PROFILE3("Hierarchical Update");
- for (int cj = 0; cj < m_ChunksH; ++cj)
+ std::vector<RegionID> updated;
+ for (u8 cj = 0; cj < m_ChunksH; ++cj)
{
- for (int ci = 0; ci < m_ChunksW; ++ci)
+ for (u8 ci = 0; ci < m_ChunksW; ++ci)
{
if (!IsChunkDirty(ci, cj, dirtinessGrid))
continue;
@@ -415,25 +440,79 @@
{
pass_class_t passClass = passClassMask.second;
Chunk& a = m_Chunks[passClass].at(ci + cj*m_ChunksW);
+
+ // Clean up edges and global region ID
+ EdgesMap& edgeMap = m_Edges[passClass];
+ for (u16 i : a.m_RegionsID)
+ {
+ RegionID reg{ci, cj, i};
+ m_GlobalRegions[passClass].erase(reg);
+ for (const RegionID& neighbor : edgeMap[reg])
+ {
+ edgeMap[neighbor].erase(reg);
+ if (edgeMap[neighbor].empty())
+ edgeMap.erase(neighbor);
+ }
+ edgeMap.erase(reg);
+ }
+
+ // recompute
a.InitRegions(ci, cj, grid, passClass);
+
+ for (u16 i : a.m_RegionsID)
+ updated.push_back(RegionID{ci, cj, i});
+
+ // add back edges
+ UpdateEdges(ci, cj, passClass, edgeMap);
}
}
}
- // TODO: Also be clever with edges
- m_Edges.clear();
- for (const std::pair<std::string, pass_class_t>& passClassMask : m_PassClassMasks)
- {
- pass_class_t passClass = passClassMask.second;
- EdgesMap& edges = m_Edges[passClass];
+ // Add back global region ID
+ // To try and be clever we'll run a custom flood-fill that stops as soon as it runs into something we know,
+ // and if nothing then we'll create a new global region.
+ // It also keeps track of all connected regions with no IDs in case of contiguous dirtiness (likely) to be faster if possible.
+ // This probably scales poorly with a large enough update?
- for (int cj = 0; cj < m_ChunksH; ++cj)
+ for (const RegionID& reg : updated)
+ for (const std::pair<std::string, pass_class_t>& passClassMask : m_PassClassMasks)
{
- for (int ci = 0; ci < m_ChunksW; ++ci)
+ std::set<RegionID> visited;
+ std::vector<RegionID> open;
+ std::vector<RegionID> updating = { reg };
+ open.push_back(reg);
+
+ GlobalRegionID ID = 0;
+ std::map<RegionID, GlobalRegionID>& globalRegion = m_GlobalRegions[passClassMask.second];
+ EdgesMap& edgeMap = m_Edges[passClassMask.second];
+ // avoid creating empty edges.
+ bool unlinked = edgeMap.find(reg) == edgeMap.end();
+ while (!open.empty() && ID == 0 && !unlinked)
{
- FindEdges(ci, cj, passClass, edges);
+ RegionID curr = open.back();
+ open.pop_back();
+ for (const RegionID& region : edgeMap[curr])
+ if (visited.insert(region).second)
+ {
+ open.push_back(region);
+ if (globalRegion.find(region) != globalRegion.end())
+ {
+ ID = globalRegion.at(region);
+ break;
+ }
+ else
+ updating.push_back(region);
+ }
}
- }
+ if (ID == 0)
+ {
+ ID = m_AvailableGlobalRegionIDs.back();
+ m_AvailableGlobalRegionIDs.pop_back();
+ if (m_AvailableGlobalRegionIDs.empty())
+ m_AvailableGlobalRegionIDs.push_back(ID+1);
+ }
+ for (const RegionID& reg : updating)
+ globalRegion[reg] = ID;
}
if (m_DebugOverlay)
@@ -462,22 +541,15 @@
}
/**
- * Find edges between regions in this chunk and the adjacent below/left chunks.
+ * Connect a chunk's regions to their neighbors. Not optimised for global recomputing.
+ * TODO: reduce code duplication with below
*/
-void HierarchicalPathfinder::FindEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges)
+void HierarchicalPathfinder::UpdateEdges(u8 ci, u8 cj, pass_class_t passClass, EdgesMap& edges)
{
std::vector<Chunk>& chunks = m_Chunks[passClass];
Chunk& a = chunks.at(cj*m_ChunksW + ci);
- // For each edge between chunks, we loop over every adjacent pair of
- // navcells in the two chunks. If they are both in valid regions
- // (i.e. are passable navcells) then add a graph edge between those regions.
- // (We don't need to test for duplicates since EdgesMap already uses a
- // std::set which will drop duplicate entries.)
- // But as set.insert can be quite slow on large collection, and that we usually
- // try to insert the same values, we cache the previous one for a fast test.
-
if (ci > 0)
{
Chunk& b = chunks.at(cj*m_ChunksW + (ci-1));
@@ -499,6 +571,27 @@
}
}
+ if (ci < m_ChunksW-1)
+ {
+ Chunk& b = chunks.at(cj*m_ChunksW + (ci+1));
+ RegionID raPrev(0,0,0);
+ RegionID rbPrev(0,0,0);
+ for (int j = 0; j < CHUNK_SIZE; ++j)
+ {
+ RegionID ra = a.Get(CHUNK_SIZE-1, j);
+ RegionID rb = b.Get(0, j);
+ if (ra.r && rb.r)
+ {
+ if (ra == raPrev && rb == rbPrev)
+ continue;
+ edges[ra].insert(rb);
+ edges[rb].insert(ra);
+ raPrev = ra;
+ rbPrev = rb;
+ }
+ }
+ }
+
if (cj > 0)
{
Chunk& b = chunks.at((cj-1)*m_ChunksW + ci);
@@ -520,6 +613,94 @@
}
}
+ if (cj < m_ChunksH - 1)
+ {
+ Chunk& b = chunks.at((cj+1)*m_ChunksW + ci);
+ RegionID raPrev(0,0,0);
+ RegionID rbPrev(0,0,0);
+ for (int i = 0; i < CHUNK_SIZE; ++i)
+ {
+ RegionID ra = a.Get(i, CHUNK_SIZE-1);
+ RegionID rb = b.Get(i, 0);
+ if (ra.r && rb.r)
+ {
+ if (ra == raPrev && rb == rbPrev)
+ continue;
+ edges[ra].insert(rb);
+ edges[rb].insert(ra);
+ raPrev = ra;
+ rbPrev = rb;
+ }
+ }
+ }
+}
+
+/**
+ * Find edges between regions in all chunks, in an optimised manner (only look at top/left)
+ */
+void HierarchicalPathfinder::RecomputeAllEdges(pass_class_t passClass, EdgesMap& edges)
+{
+ std::vector<Chunk>& chunks = m_Chunks[passClass];
+
+ edges.clear();
+
+ for (int cj = 0; cj < m_ChunksH; ++cj)
+ {
+ for (int ci = 0; ci < m_ChunksW; ++ci)
+ {
+ Chunk& a = chunks.at(cj*m_ChunksW + ci);
+
+ // For each edge between chunks, we loop over every adjacent pair of
+ // navcells in the two chunks. If they are both in valid regions
+ // (i.e. are passable navcells) then add a graph edge between those regions.
+ // (We don't need to test for duplicates since EdgesMap already uses a
+ // std::set which will drop duplicate entries.)
+ // But as set.insert can be quite slow on large collection, and that we usually
+ // try to insert the same values, we cache the previous one for a fast test.
+
+ if (ci > 0)
+ {
+ Chunk& b = chunks.at(cj*m_ChunksW + (ci-1));
+ RegionID raPrev(0,0,0);
+ RegionID rbPrev(0,0,0);
+ for (int j = 0; j < CHUNK_SIZE; ++j)
+ {
+ RegionID ra = a.Get(0, j);
+ RegionID rb = b.Get(CHUNK_SIZE-1, j);
+ if (ra.r && rb.r)
+ {
+ if (ra == raPrev && rb == rbPrev)
+ continue;
+ edges[ra].insert(rb);
+ edges[rb].insert(ra);
+ raPrev = ra;
+ rbPrev = rb;
+ }
+ }
+ }
+
+ if (cj > 0)
+ {
+ Chunk& b = chunks.at((cj-1)*m_ChunksW + ci);
+ RegionID raPrev(0,0,0);
+ RegionID rbPrev(0,0,0);
+ for (int i = 0; i < CHUNK_SIZE; ++i)
+ {
+ RegionID ra = a.Get(i, 0);
+ RegionID rb = b.Get(i, CHUNK_SIZE-1);
+ if (ra.r && rb.r)
+ {
+ if (ra == raPrev && rb == rbPrev)
+ continue;
+ edges[ra].insert(rb);
+ edges[rb].insert(ra);
+ raPrev = ra;
+ rbPrev = rb;
+ }
+ }
+ }
+ }
+ }
}
/**
@@ -557,7 +738,7 @@
xz.push_back(b.Y.ToFloat());
m_DebugOverlayLines.emplace_back();
- m_DebugOverlayLines.back().m_Color = CColor(1.0, 1.0, 1.0, 1.0);
+ m_DebugOverlayLines.back().m_Color = CColor(1.0, 0.0, 0.0, 1.0);
SimRender::ConstructLineOnGround(*m_SimContext, xz, m_DebugOverlayLines.back(), true);
}
}
@@ -571,75 +752,283 @@
return m_Chunks[passClass][cj*m_ChunksW + ci].Get(i % CHUNK_SIZE, j % CHUNK_SIZE);
}
-void HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass)
+HierarchicalPathfinder::GlobalRegionID HierarchicalPathfinder::GetGlobalRegion(u16 i, u16 j, pass_class_t passClass)
{
- PROFILE2("MakeGoalReachable");
+ RegionID region = Get(i, j, passClass);
+ if (region.r == 0)
+ return (GlobalRegionID)0;
+ return m_GlobalRegions[passClass][region];
+}
+
+#define OUTPUT 0
+
+#if OUTPUT
+ #include <fstream>
+#endif
+
+#if OUTPUT
+// also change the header
+bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass, std::ofstream& stream)
+#else
+bool HierarchicalPathfinder::MakeGoalReachable(u16 i0, u16 j0, PathGoal& goal, pass_class_t passClass)
+#endif
+{
+ /*
+ * Relatively straightforward implementation of A* on the hierarchical pathfinder graph.
+ * Since this isn't a grid, we cannot use JPS (though I'm fairly sure it could sort of be extended to work, but it's probably not trivial/worth it)
+ * Uses flat_set and flat_map over std::set and std::map since testing proves that reusing the memory ends up being more efficient
+ * The main optimisations are:
+ * - picking the best item directly from the open list when we can be sure we know which one it is (see fasttrack)
+ * - checking whether the goal is reachable or not, and if it isn't stopping early to avoid slowly flood-filling everything
+ *
+ * Since we'd like to return the best possible navcell, if the goal is reachable, we'll stop A* once we've reached any goal region
+ * since then presumably other reachable goal-regions would be reachable following roughtly the same path.
+ * Then we'll loop over goal regions to get the best navcell and reconstruct the path from there.
+ *
+ * NB: since the path is currently unused, I skip the A* part for reachable goals.
+ */
RegionID source = Get(i0, j0, passClass);
- // Find everywhere that's reachable
- std::set<RegionID> reachableRegions;
- FindReachableRegions(source, reachableRegions, passClass);
+ u16 gi, gj;
+ Pathfinding::NearestNavcell(goal.x, goal.z, gi, gj, m_W, m_H);
- // Check whether any reachable region contains the goal
- // And get the navcell that's the closest to the point
+ // determine if we will be able to reach the goal.
+ // If not, we can stop A* earlier by being clever.
+ std::set<RegionID> goalRegions;
+ FindGoalRegions(gi, gj, goal, goalRegions, passClass);
+
+ std::vector<RegionID> reachableGoalRegions;
+
+ GlobalRegionID startID = GetGlobalRegion(i0, j0, passClass);
+ bool reachable = false;
+ for (const RegionID& r : goalRegions)
+ if (m_GlobalRegions[passClass][r] == startID)
+ {
+ reachable = true;
+ reachableGoalRegions.push_back(r);
+ }
- u16 bestI = 0;
- u16 bestJ = 0;
- u32 dist2Best = std::numeric_limits<u32>::max();
+#if OUTPUT
+ stream << "context.fillStyle = 'rgba(1,0,1,1)';\n";
+ for (const RegionID& r : goalRegions)
+ {
+ entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (r.ci * CHUNK_SIZE);
+ entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (r.cj * CHUNK_SIZE);
+ stream << "context2.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n";
+ }
+#endif
+
+ // In general, our maps are relatively open, so it's usually a better bet to be biaised towards minimal distance over path length.
+ int (*DistEstimate)(u16, u16, u16, u16) = [](u16 regI, u16 regJ, u16 gi, u16 gj) -> int { return (regI - gi)*(regI - gi) + (regJ - gj)*(regJ - gj); };
+ // However, run unbiaised euclidian if we know the goal is unreachable, since we want to get as close as possible efficiently.
+ // multiply by 20 because we want distance to goal to matter a lot
+ if (!reachable)
+ DistEstimate = [](u16 regI, u16 regJ, u16 gi, u16 gj) -> int {
+ return 20 * isqrt64((regI - gi)*(regI - gi) + (regJ - gj)*(regJ - gj));
+ };
+
+ m_Astar_ClosedNodes.clear();
+ m_Astar_OpenNodes.clear();
+ m_Astar_OpenNodes.insert(source);
+
+ m_Astar_Predecessor.clear();
+
+ m_Astar_GScore.clear();
+ m_Astar_GScore[source] = 0;
+
+ m_Astar_FScore.clear();
+ m_Astar_FScore[source] = DistEstimate(source.ci * CHUNK_SIZE + CHUNK_SIZE/2, source.cj * CHUNK_SIZE + CHUNK_SIZE/2, gi, gj);
+
+ RegionID current {0,0,0};
+
+ u16 bestI, bestJ;
+ u32 dist2;
+
+ u32 timeSinceLastFScoreImprovement = 0;
+#if OUTPUT
+ int step = 0;
+#endif
+
+ RegionID fastTrack = source;
+ int currentFScore = m_Astar_FScore[source];
+ int secondBestFScore = currentFScore;
+ int globalBestFScore = currentFScore;
+
+ EdgesMap& edgeMap = m_Edges[passClass];
+
+ // NB: to re-enable A* for the reachable case (if you want to use the path), remove the "!reachable" part of this check
+ while (!reachable && !m_Astar_OpenNodes.empty())
+ {
+ // Since we are not using a fancy open list, we have to go through all nodes each time
+ // So when we are sure that we know the best node (because the last run gave us a node better than us, which was already the best
+ // we can fast-track and not sort but just pick that one instead.
+ // In cases where the obvious path is the best, we hardly ever sort and it's a little faster
+ if (fastTrack.r)
+ {
+ current = fastTrack;
+ currentFScore = m_Astar_FScore[current];
+ secondBestFScore = currentFScore;
+ }
+ else
+ {
+ auto iter = m_Astar_OpenNodes.begin();
+ current = *iter;
+ currentFScore = m_Astar_FScore[current];
+ secondBestFScore = currentFScore;
+ while (++iter != m_Astar_OpenNodes.end())
+ {
+ int score = m_Astar_FScore[*iter];
+ if (score < currentFScore)
+ {
+ current = *iter;
+ secondBestFScore = currentFScore;
+ currentFScore = score;
+ }
+ }
+ }
- for (const RegionID& region : reachableRegions)
- {
- // Skip region if its chunk doesn't contain the goal area
- entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (region.ci * CHUNK_SIZE);
- entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (region.cj * CHUNK_SIZE);
+ m_Astar_OpenNodes.erase(current);
+ m_Astar_ClosedNodes.emplace(current);
+ if (reachable)
+ m_Astar_FScore.erase(current);
+ m_Astar_GScore.erase(current);
+
+ // Stop heuristic in case we know we cannot reach the goal.
+ // Indeed this would cause A* to become an inacceptably slow flood fill.
+ // We keep track of our best fScore, we'll early-exit if we're too far from it
+ // or we haven't found a better path in a while.
+ // This will cause us to return largely suboptimal paths now and then,
+ // but then again those should be rare and the player can just re-order a move.
+ if (!reachable)
+ {
+ if (currentFScore < globalBestFScore)
+ {
+ globalBestFScore = currentFScore;
+ timeSinceLastFScoreImprovement = 0;
+ }
+ else if ( (++timeSinceLastFScoreImprovement > 3 && currentFScore > globalBestFScore * 2) || timeSinceLastFScoreImprovement > m_ChunksW)
+ break;
+ }
+
+ entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (current.ci * CHUNK_SIZE);
+ entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (current.cj * CHUNK_SIZE);
entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE;
entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE;
- if (!goal.RectContainsGoal(x0, z0, x1, z1))
- continue;
- u16 i,j;
- u32 dist2;
+#if OUTPUT
+ stream << "context.fillStyle = 'rgba(" <<DistEstimate(current, gi, gj) / 10000 << ",255,"<< (fastTrack.r > 0 ? 255 : 0) <<",0.8)';\n maxStep = " << step+1 << ";\n";
+ stream << "if (step >= " << step << ") context.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n";
+#endif
+
+ fastTrack = RegionID{0,0,0};
+
+ // TODO: we should get those first and then validate here, instead of recomputing for each.
+ if (goal.RectContainsGoal(x0, z0, x1, z1))
+ if (GetChunk(current.ci, current.cj, passClass).RegionNearestNavcellInGoal(current.r, i0, j0, goal, bestI, bestJ, dist2))
+ break;
- // If the region contains the goal area, the goal is reachable
- // Remember the best point for optimization.
- if (GetChunk(region.ci, region.cj, passClass).RegionNearestNavcellInGoal(region.r, i0, j0, goal, i, j, dist2))
- {
- // If it's a point, no need to move it, we're done
- if (goal.type == PathGoal::POINT)
- return;
- if (dist2 < dist2Best)
+ int currScore = m_Astar_GScore[current];
+ for (const RegionID& neighbor : edgeMap[current])
+ {
+ if (m_Astar_ClosedNodes.find(neighbor) != m_Astar_ClosedNodes.end())
+ continue;
+ int temp_m_Astar_GScore = currScore + DistBetween(neighbor, current);
+ auto iter = m_Astar_OpenNodes.emplace(neighbor);
+ if (!iter.second && temp_m_Astar_GScore >= m_Astar_GScore[neighbor])
+ continue;
+#if OUTPUT
+ x0 = Pathfinding::NAVCELL_SIZE * (neighbor.ci * CHUNK_SIZE);
+ z0 = Pathfinding::NAVCELL_SIZE * (neighbor.cj * CHUNK_SIZE);
+ stream << "context2.fillStyle = 'rgba(255,255,0,0.3)';\n";
+ stream << "if (step >= " << step << ") context2.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n";
+#endif
+
+ m_Astar_GScore[neighbor] = temp_m_Astar_GScore;
+ // no default constructor so we'll use this hack in the meantime
+ auto alreadyThere = m_Astar_Predecessor.emplace( boost::container::flat_map<RegionID, RegionID>::value_type{ neighbor, current });
+ alreadyThere.first->second = current;
+ int score;
+ // if the goal is reachable, we don't care much about fscore precision so pick the center
+ if (reachable)
+ score = temp_m_Astar_GScore + DistEstimate(neighbor.ci * CHUNK_SIZE + CHUNK_SIZE/2, neighbor.cj * CHUNK_SIZE + CHUNK_SIZE/2, gi, gj);
+ else
{
- bestI = i;
- bestJ = j;
- dist2Best = dist2;
+ // if it's unreachable, it's more important however. So when we're close, get the "region center".
+ entity_pos_t x0 = Pathfinding::NAVCELL_SIZE * (neighbor.ci * CHUNK_SIZE);
+ entity_pos_t z0 = Pathfinding::NAVCELL_SIZE * (neighbor.cj * CHUNK_SIZE);
+ entity_pos_t x1 = x0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE;
+ entity_pos_t z1 = z0 + Pathfinding::NAVCELL_SIZE * CHUNK_SIZE;
+ if (goal.RectContainsGoal(x0, z0, x1, z1))
+ {
+ int ri, rj;
+ GetChunk(neighbor.ci, neighbor.cj, passClass).RegionCenter(neighbor.r, ri, rj);
+ score = temp_m_Astar_GScore + DistEstimate((u16)ri, (u16)rj, gi, gj);
+ }
+ else
+ score = temp_m_Astar_GScore + DistEstimate(neighbor.ci * CHUNK_SIZE + CHUNK_SIZE/2, neighbor.cj * CHUNK_SIZE + CHUNK_SIZE/2, gi, gj);
}
+ if (score < secondBestFScore)
+ {
+ secondBestFScore = score;
+ fastTrack = neighbor;
+ }
+ m_Astar_FScore[neighbor] = score;
}
+#if OUTPUT
+ step++;
+#endif
}
- // If the goal area wasn't reachable,
- // find the navcell that's nearest to the goal's center
- if (dist2Best == std::numeric_limits<u32>::max())
- {
- u16 iGoal, jGoal;
- Pathfinding::NearestNavcell(goal.x, goal.z, iGoal, jGoal, m_W, m_H);
-
- FindNearestNavcellInRegions(reachableRegions, iGoal, jGoal, passClass);
-
- // Construct a new point goal at the nearest reachable navcell
- PathGoal newGoal;
- newGoal.type = PathGoal::POINT;
- Pathfinding::NavcellCenter(iGoal, jGoal, newGoal.x, newGoal.z);
- goal = newGoal;
- return;
+#if OUTPUT
+ fixed x0 = Pathfinding::NAVCELL_SIZE * (current.ci * CHUNK_SIZE);
+ fixed z0 = Pathfinding::NAVCELL_SIZE * (current.cj * CHUNK_SIZE);
+ stream << "context.fillStyle = 'rgba(255,0,0,0.6)';\n";
+ stream << "if (step >= " << step << ") context.fillRect(" << x0.ToInt_RoundToZero() << " * scale," << z0.ToInt_RoundToZero() << " * scale," << (int)CHUNK_SIZE << " * scale," << (int)CHUNK_SIZE << " * scale);\n";
+#endif
+
+ if (!reachable)
+ {
+ // I don't believe this is possible, nor should it.
+ ENSURE(!m_Astar_ClosedNodes.empty());
+
+ // Pick best and roll with that.
+ current = *std::min_element(m_Astar_ClosedNodes.begin(), m_Astar_ClosedNodes.end(),
+ [this](const RegionID& a, const RegionID& b) -> bool { return m_Astar_FScore[a] < m_Astar_FScore[b]; });
+
+ std::set<RegionID> set = { current };
+ Pathfinding::NearestNavcell(goal.x, goal.z, bestI, bestJ, m_W, m_H);
+
+ FindNearestNavcellInRegions(set, bestI, bestJ, passClass);
+ }
+ else
+ {
+ u32 bestDist;
+ // loop through reachable goal regions and get the best navcell.
+ // TODO: we probably could skip some of those if our gScore/fScore were good enough.
+ for (const RegionID& region : reachableGoalRegions)
+ {
+ u16 ri, rj;
+ u32 dist;
+ // TODO: using the A* path, we should consider from predecessor and not from source
+ GetChunk(region.ci, region.cj, passClass).RegionNearestNavcellInGoal(region.r, i0, j0, goal, ri, rj, dist);
+ if (dist < bestDist)
+ {
+ bestI = ri;
+ bestJ = rj;
+ bestDist = dist;
+ }
+ }
}
- ENSURE(dist2Best != std::numeric_limits<u32>::max());
PathGoal newGoal;
newGoal.type = PathGoal::POINT;
Pathfinding::NavcellCenter(bestI, bestJ, newGoal.x, newGoal.z);
goal = newGoal;
+
+ return reachable;
}
+
void HierarchicalPathfinder::FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass)
{
std::set<RegionID> regions;
@@ -710,15 +1099,16 @@
// collecting all the regions that are reachable via edges
std::vector<RegionID> open;
+ open.reserve(64);
open.push_back(from);
reachable.insert(from);
+ EdgesMap& edgeMap = m_Edges[passClass];
while (!open.empty())
{
RegionID curr = open.back();
open.pop_back();
-
- for (const RegionID& region : m_Edges[passClass][curr])
+ for (const RegionID& region : edgeMap[curr])
// Add to the reachable set; if this is the first time we added
// it then also add it to the open list
if (reachable.insert(region).second)
@@ -731,12 +1121,43 @@
// Construct a set of all regions of all chunks for this pass class
for (const Chunk& chunk : m_Chunks[passClass])
{
- // region 0 is impassable tiles
- for (int r = 1; r <= chunk.m_NumRegions; ++r)
+ for (u16 r : chunk.m_RegionsID)
regions.insert(RegionID(chunk.m_ChunkI, chunk.m_ChunkJ, r));
}
}
+void HierarchicalPathfinder::FindGoalRegions(u16 gi, u16 gj, const PathGoal& goal, std::set<HierarchicalPathfinder::RegionID>& regions, pass_class_t passClass)
+{
+ if (goal.type == PathGoal::POINT)
+ {
+ RegionID region = Get(gi, gj, passClass);
+ if (region.r > 0)
+ regions.insert(region);
+ return;
+ }
+
+ // For non-point cases, we'll test each region inside the bounds of the goal.
+ // we might occasionally test a few too many for circles but it's not too bad.
+ // Note that this also works in the Inverse-circle / Inverse-square case
+ // Since our ranges are inclusive, we will necessarily test at least the perimeter/outer bound of the goal.
+ // If we find a navcell, great, if not, well then we'll be surrounded by an impassable barrier.
+ // Since in the Inverse-XX case we're supposed to start inside, then we can't ever reach the goal so it's good enough.
+ // It's not worth it to skip the "inner" regions since we'd need ranges above CHUNK_SIZE for that to start mattering
+ // (and even then not always) and that just doesn't happen for Inverse-XX goals
+ int size = (std::max(goal.hh, goal.hw) * 3 / 2).ToInt_RoundToInfinity();
+
+ u16 a,b; u32 c; // unused params for RegionNearestNavcellInGoal
+
+ for (u8 sz = (gj - size) / CHUNK_SIZE; sz <= (gj + size) / CHUNK_SIZE; ++sz)
+ for (u8 sx = (gi - size) / CHUNK_SIZE; sx <= (gi + size) / CHUNK_SIZE; ++sx)
+ {
+ Chunk& chunk = GetChunk(sx, sz, passClass);
+ for (u16 i : chunk.m_RegionsID)
+ if (chunk.RegionNearestNavcellInGoal(i, 0, 0, goal, a, b, c))
+ regions.insert(RegionID{sx, sz, i});
+ }
+}
+
void HierarchicalPathfinder::FillRegionOnGrid(const RegionID& region, pass_class_t passClass, u16 value, Grid<u16>& grid)
{
ENSURE(grid.m_W == m_W && grid.m_H == m_H);
Index: source/simulation2/helpers/LongPathfinder.h
===================================================================
--- source/simulation2/helpers/LongPathfinder.h
+++ source/simulation2/helpers/LongPathfinder.h
@@ -164,6 +164,10 @@
LongPathfinder();
~LongPathfinder();
+#ifdef TEST
+ HierarchicalPathfinder& GetHierarchicalPathfinder() { return m_PathfinderHier; }
+#endif
+
void SetDebugOverlay(bool enabled);
void SetHierDebugOverlay(bool enabled, const CSimContext *simContext)
@@ -239,6 +243,16 @@
void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal,
pass_class_t passClass, std::vector<CircularRegion> excludedRegions, WaypointPath& path);
+ bool MakeGoalReachable(u16 i0, u16 j0, PathGoal &goal, pass_class_t passClass) { return m_PathfinderHier.MakeGoalReachable(i0, j0, goal, passClass); };
+
+ void FindNearestPassableNavcell(u16& i, u16& j, pass_class_t passClass) { return m_PathfinderHier.FindNearestPassableNavcell(i, j, passClass); };
+
+ bool NavcellIsReachable(u16 i0, u16 j0, u16 i1, u16 j1, pass_class_t passClass)
+ {
+ return m_PathfinderHier.GetGlobalRegion(i0, j0, passClass) == m_PathfinderHier.GetGlobalRegion(i1, j1, passClass);
+ };
+
+
Grid<u16> GetConnectivityGrid(pass_class_t passClass)
{
return m_PathfinderHier.GetConnectivityGrid(passClass);
Index: source/simulation2/helpers/PathGoal.h
===================================================================
--- source/simulation2/helpers/PathGoal.h
+++ source/simulation2/helpers/PathGoal.h
@@ -28,6 +28,7 @@
* part of the goal.
* Also, it can be an 'inverted' circle/square, where any point outside
* the shape is part of the goal.
+ * In both cases, points on the range (ie at the frontier) are considered inside.
*/
class PathGoal
{
Index: source/simulation2/helpers/Pathfinding.h
===================================================================
--- source/simulation2/helpers/Pathfinding.h
+++ source/simulation2/helpers/Pathfinding.h
@@ -129,6 +129,7 @@
* between translation units.
* TODO: figure out whether this is actually needed. It was added back in r8751 (in 2010) for unclear reasons
* and it does not seem to really improve behavior today
+ * Note by Wraitii to wraitii: you just removed this in UnitMotion, delete it if it ends up being unecessary as expected.
*/
const entity_pos_t GOAL_DELTA = NAVCELL_SIZE/8;
Index: source/simulation2/scripting/MessageTypeConversions.cpp
===================================================================
--- source/simulation2/scripting/MessageTypeConversions.cpp
+++ source/simulation2/scripting/MessageTypeConversions.cpp
@@ -267,20 +267,74 @@
////////////////////////////////
-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);
+}
+
+////////////////////////////////
+
+JS::Value CMessageMoveSuccess::ToJSVal(ScriptInterface& scriptInterface) const
+{
+ TOJSVAL_SETUP();
+ return JS::ObjectValue(*obj);
+}
+
+CMessage* CMessageMoveSuccess::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val)
+{
+ FROMJSVAL_SETUP();
+ return new CMessageMoveSuccess();
+}
+
+////////////////////////////////
+
+JS::Value CMessageMoveFailure::ToJSVal(ScriptInterface& scriptInterface) const
+{
+ TOJSVAL_SETUP();
+ return JS::ObjectValue(*obj);
+}
+
+CMessage* CMessageMoveFailure::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val)
+{
+ FROMJSVAL_SETUP();
+ return new CMessageMoveFailure();
}
////////////////////////////////
Index: source/tools/atlas/GameInterface/ActorViewer.cpp
===================================================================
--- source/tools/atlas/GameInterface/ActorViewer.cpp
+++ source/tools/atlas/GameInterface/ActorViewer.cpp
@@ -375,7 +375,7 @@
{
CmpPtr<ICmpUnitMotion> 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<ICmpUnitMotion> cmpUnitMotion(m.Simulation2, m.Entity);
if (cmpUnitMotion)
- speed = cmpUnitMotion->GetRunSpeed().ToFloat();
+ speed = cmpUnitMotion->GetBaseSpeed().ToFloat() * cmpUnitMotion->GetTopSpeedRatio().ToFloat();
else
speed = 12.f; // typical unit speed

File Metadata

Mime Type
text/plain
Expires
Sun, Sep 29, 3:22 AM (21 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3386720
Default Alt Text
D13.id197.diff (400 KB)

Event Timeline