Index: binaries/data/mods/public/art/actors/units/britons/chariot_javelinist_c_m.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/britons/chariot_javelinist_c_m.xml
+++ binaries/data/mods/public/art/actors/units/britons/chariot_javelinist_c_m.xml
@@ -18,7 +18,6 @@
-
Index: binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml
===================================================================
--- binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml
+++ binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml
@@ -17,7 +17,6 @@
-
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
@@ -199,7 +199,7 @@
},
"getActionInfo": function(entState, targetState)
{
- if (!entState.attack || !targetState.hitpoints)
+ if (!targetState.hitpoints)
return false;
return {
@@ -1020,7 +1020,12 @@
let count = 0;
for (let entState of entStates)
if (entState.garrisonHolder)
- count += entState.garrisonHolder.entities.length;
+ for (let entity of entState.garrisonHolder.entities)
+ {
+ let state = GetEntityState(entity);
+ if (state.canGarrison.unloadable)
+ ++count;
+ }
if (!count)
return false;
@@ -1118,7 +1123,7 @@
"getInfo": function(entStates)
{
if (entStates.every(entState => {
- if (!entState.unitAI || !entState.turretParent)
+ if (!entState.unitAI || !entState.turretParent || !entState.canGarrison.unloadable)
return true;
let parent = GetEntityState(entState.turretParent);
return !parent || !parent.garrisonHolder || parent.garrisonHolder.entities.indexOf(entState.id) == -1;
Index: binaries/data/mods/public/simulation/components/Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/Attack.js
+++ binaries/data/mods/public/simulation/components/Attack.js
@@ -329,12 +329,13 @@
*/
Attack.prototype.GetFullAttackRange = function()
{
- let ret = { "min": Infinity, "max": 0 };
+ let ret = { "min": Infinity, "max": 0, "elevationBonus": Infinity };
for (let type of this.GetAttackTypes())
{
let range = this.GetRange(type);
ret.min = Math.min(ret.min, range.min);
ret.max = Math.max(ret.max, range.max);
+ ret.elevationBonus = Math.min(ret.elevationBonus, range.elevationBonus);
}
return ret;
};
Index: binaries/data/mods/public/simulation/components/GarrisonHolder.js
===================================================================
--- binaries/data/mods/public/simulation/components/GarrisonHolder.js
+++ binaries/data/mods/public/simulation/components/GarrisonHolder.js
@@ -36,6 +36,16 @@
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
@@ -72,6 +82,7 @@
{
let points = this.template.VisibleGarrisonPoints;
for (let point in points)
+ {
this.visibleGarrisonPoints.push({
"offset": {
"x": +points[point].X,
@@ -79,8 +90,38 @@
"z": +points[point].Z
},
"angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null,
- "entity": null
+ "entity": null,
+ "template": points[point].Template,
+ "forced": points[point].Forced && points[point].Forced == "true"
});
+ }
+ }
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "AutogarrisonTurrets", 100, null);
+};
+
+GarrisonHolder.prototype.AutogarrisonTurrets = function()
+{
+ if (!this.visibleGarrisonPoints)
+ return;
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership)
+ return;
+
+ let owner = cmpOwnership.GetOwner();
+ let points = this.visibleGarrisonPoints;
+ for (let point in points)
+ {
+ if (!points[point].template)
+ continue;
+
+ let filter = points[point].forced ? "forcedgarrison|" : "";
+ let ent = Engine.AddEntity(filter + points[point].template);
+ let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpOwnership)
+ cmpOwnership.SetOwner(owner);
+ this.Garrison(ent, points[point], points[point].forced);
}
};
@@ -105,6 +146,11 @@
return this.entities;
};
+GarrisonHolder.prototype.GetVisibleGarrisonPoints = function()
+{
+ return this.visibleGarrisonPoints;
+};
+
/**
* @return {Array} unit classes which can be garrisoned inside this
* particular entity. Obtained from the entity's template.
@@ -184,13 +230,13 @@
* If vgpEntity is given, this visualGarrisonPoint will be used for the entity.
* @return {boolean} Whether the entity was garrisonned.
*/
-GarrisonHolder.prototype.Garrison = function(entity, vgpEntity)
+GarrisonHolder.prototype.Garrison = function(entity, vgpEntity, forced)
{
let cmpPosition = Engine.QueryInterface(entity, IID_Position);
if (!cmpPosition)
return false;
- if (!this.PerformGarrison(entity))
+ if (!this.PerformGarrison(entity, forced))
return false;
let visibleGarrisonPoint = vgpEntity;
@@ -223,7 +269,10 @@
cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset);
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
+ {
cmpUnitAI.SetTurretStance();
+ cmpUnitAI.SetGarrisoned();
+ }
}
else
cmpPosition.MoveOutOfWorld();
@@ -234,13 +283,13 @@
/**
* @return {boolean} Whether the entity was garrisonned.
*/
-GarrisonHolder.prototype.PerformGarrison = function(entity)
+GarrisonHolder.prototype.PerformGarrison = function(entity, forced)
{
if (!this.HasEnoughHealth())
return false;
// Check if the unit is allowed to be garrisoned inside the building
- if (!this.IsAllowedToGarrison(entity))
+ if (!forced && !this.IsAllowedToGarrison(entity))
return false;
// Check the capacity
@@ -431,7 +480,7 @@
*/
GarrisonHolder.prototype.Unload = function(entity, forced)
{
- return this.PerformEject([entity], forced);
+ return this.IsUnloadable(entity) && this.PerformEject([entity], forced);
};
/**
@@ -477,7 +526,7 @@
{
let entities = this.entities.filter(ent => {
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
- return cmpOwnership && cmpOwnership.GetOwner() == owner;
+ return cmpOwnership && cmpOwnership.GetOwner() == owner && this.IsUnloadable(ent);
});
return this.PerformEject(entities, forced);
};
@@ -489,7 +538,7 @@
*/
GarrisonHolder.prototype.UnloadAll = function(forced)
{
- return this.PerformEject(this.entities.slice(), forced);
+ return this.PerformEject(this.entities.slice().filter(ent => this.IsUnloadable(ent)), forced);
};
/**
@@ -681,7 +730,7 @@
*/
GarrisonHolder.prototype.IsEjectable = function(entity)
{
- if (!this.entities.find(ent => ent == entity))
+ if (!this.IsUnloadable(entity))
return false;
let ejectableClasses = this.template.EjectClassesOnDestroy._string;
@@ -691,6 +740,20 @@
};
/**
+ * Whether an entity is unloadable.
+ * @param {number} entity - The entity-ID to be tested.
+ * @return {boolean} - Whether the entity is unloadable.
+ */
+GarrisonHolder.prototype.IsUnloadable = function(entity)
+{
+ if (!this.entities.find(ent => ent == entity))
+ return false;
+
+ let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable);
+ return cmpGarrisonable && cmpGarrisonable.IsUnloadable();
+};
+
+/**
* Initialise the garrisoned units.
*/
GarrisonHolder.prototype.OnGlobalInitGame = function(msg)
Index: binaries/data/mods/public/simulation/components/Garrisonable.js
===================================================================
--- binaries/data/mods/public/simulation/components/Garrisonable.js
+++ binaries/data/mods/public/simulation/components/Garrisonable.js
@@ -1,11 +1,37 @@
function Garrisonable() {}
-Garrisonable.prototype.Schema = "";
+Garrisonable.prototype.Schema =
+ "Controls the garrisonability of an entity." +
+ "" +
+ "true" +
+ "" +
+ "" +
+ "" +
+ "";
Garrisonable.prototype.Init = function()
{
+ this.SetUnloadable(this.template.Unloadable && this.template.Unloadable == "true");
};
-Garrisonable.prototype.Serialize = null;
+/**
+ * Gets the ability to ungarrison.
+ *
+ * @return {boolean} Whether ungarrisoning is allowed.
+ */
+Garrisonable.prototype.IsUnloadable = function()
+{
+ return this.Unloadable;
+};
+
+/**
+ * Sets the ability to ungarrison.
+ *
+ * @param {boolean} bool - Whether ungarrisoning is allowed.
+ */
+Garrisonable.prototype.SetUnloadable = function(bool)
+{
+ this.Unloadable = bool;
+};
Engine.RegisterComponentType(IID_Garrisonable, "Garrisonable", Garrisonable);
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
@@ -363,7 +363,11 @@
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
- ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
+ let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
+ if (cmpGarrisonable)
+ ret.canGarrison = {
+ "unloadable": cmpGarrisonable.IsUnloadable()
+ }
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
@@ -1829,8 +1833,8 @@
GuiInterface.prototype.CanAttack = function(player, data)
{
- let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
- return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
+ let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI);
+ return cmpUnitAI && cmpUnitAI.CanAttack(data.target, data.types || undefined);
};
/*
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
@@ -1513,23 +1513,36 @@
"WALKING": {
"enter": function() {
- if (!this.MoveTo(this.order.data))
+ if (!this.MoveTo(this.order.data, this.order.data.iid || undefined))
{
this.FinishOrder();
return true;
}
+ this.StartTimer(0, 1000);
},
"leave": function() {
this.StopMoving();
+ this.StopTimer();
+ },
+
+ "Timer": function() {
+ if (this.CheckRange(this.order.data, this.order.data.iid || undefined))
+ {
+ this.DelegateOrder(this.order.data);
+ this.FinishOrder();
+ }
},
"MovementUpdate": function(msg) {
// If it looks like the path is failing, and we are close enough (3 tiles)
// stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably.
if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) ||
- this.CheckRange(this.order.data))
+ this.CheckRange(this.order.data, this.order.data.iid || undefined))
+ {
+ this.DelegateOrder(this.order.data);
this.FinishOrder();
+ }
},
},
@@ -4293,12 +4306,11 @@
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
- var cmpRanged = Engine.QueryInterface(this.entity, iid);
- if (!cmpRanged)
+ let range = this.GetRange(iid, type);
+ if (!range)
return false;
- var range = cmpRanged.GetRange(type);
- var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
@@ -4327,8 +4339,9 @@
if (!this.CheckTargetVisible(target))
return false;
- let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- let range = cmpAttack.GetRange(type);
+ let range = this.GetRange(IID_Attack, type);
+ if (!range)
+ return false;
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
@@ -4409,10 +4422,9 @@
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
- var cmpRanged = Engine.QueryInterface(this.entity, iid);
- if (!cmpRanged)
+ let range = this.GetRange(iid, type);
+ if (!range)
return false;
- var range = cmpRanged.GetRange(type);
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
@@ -4446,8 +4458,9 @@
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
- let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- let range = cmpAttack.GetRange(type);
+ let range = this.GetRange(IID_Attack, type);
+ if (!range)
+ return false;
let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
@@ -4576,8 +4589,9 @@
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
{
- var cmpRanged = Engine.QueryInterface(this.entity, iid);
- var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type);
+ let range = this.GetRange(iid, type);
+ if (!range)
+ return false;
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
@@ -5093,6 +5107,18 @@
"allowCapture": allowCapture,
};
+ // This is the case when an entity that has visibly garrisoned entities that can
+ // attack, but the entity itself can't attack.
+ let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack)
+ if (!cmpAttack)
+ {
+ order.type = "Attack";
+ order.iid = IID_Attack;
+
+ this.AddOrder("WalkToTarget", order, queued);
+ return;
+ }
+
this.RememberTargetPosition(order);
this.AddOrder("Attack", order, queued);
@@ -5618,14 +5644,13 @@
UnitAI.prototype.GetQueryRange = function(iid)
{
- var ret = { "min": 0, "max": 0 };
+ let ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
{
- var cmpRanged = Engine.QueryInterface(this.entity, iid);
- if (!cmpRanged)
+ let range = this.GetRange(iid);
+ if (!range)
return ret;
- var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
- var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
ret.min = range.min;
@@ -5633,32 +5658,31 @@
}
else if (this.GetStance().respondChase)
{
- var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
- var range = cmpVision.GetRange();
+ let range = cmpVision.GetRange();
ret.max = range;
}
else if (this.GetStance().respondHoldGround)
{
- var cmpRanged = Engine.QueryInterface(this.entity, iid);
- if (!cmpRanged)
+ let range = this.GetRange(iid);
+ if (!range)
return ret;
- var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
- var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
- var vision = cmpVision.GetRange();
+ let vision = cmpVision.GetRange();
ret.max = Math.min(range.max + vision / 2, vision);
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
{
- var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
+ let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
- var range = cmpVision.GetRange();
+ let range = cmpVision.GetRange();
ret.max = range;
}
return ret;
@@ -5743,7 +5767,7 @@
//// Helper functions ////
-UnitAI.prototype.CanAttack = function(target)
+UnitAI.prototype.CanAttack = function(target, types)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
@@ -5751,7 +5775,68 @@
return true;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- return cmpAttack && cmpAttack.CanAttack(target);
+ if (cmpAttack)
+ return cmpAttack.CanAttack(target, types);
+
+ let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder)
+ return false;
+
+ let visibleGarrisonPoints = cmpGarrisonHolder.GetVisibleGarrisonPoints();
+ if (!visibleGarrisonPoints)
+ return false;
+
+ for (let point of visibleGarrisonPoints)
+ {
+ if (!point.entity)
+ continue;
+
+ let cmpAttack = Engine.QueryInterface(point.entity, IID_Attack);
+ if (cmpAttack && cmpAttack.CanAttack(target, types))
+ return true;
+ }
+
+ return false;
+};
+
+UnitAI.prototype.GetRange = function(iid, type)
+{
+ let cmpRanged = Engine.QueryInterface(this.entity, iid);
+ if (cmpRanged)
+ return iid === IID_Attack ? (type ? cmpRanged.GetRange(type) : cmpRanged.GetFullAttackRange()) : cmpRanged.GetRange();
+
+ let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ if (cmpGarrisonHolder)
+ {
+ let result = {
+ "min": 0,
+ "max": -1,
+ "elevationBonus": 0
+ };
+ let visibleGarrisonPoints = cmpGarrisonHolder.GetVisibleGarrisonPoints();
+ if (!visibleGarrisonPoints)
+ return false;
+
+ let atLeastOne = false;
+ for (let point of visibleGarrisonPoints)
+ {
+ if (!point.entity)
+ continue;
+
+ cmpRanged = Engine.QueryInterface(point.entity, iid);
+ if (cmpRanged)
+ {
+ atLeastOne = true;
+ let range = iid === IID_Attack ? (type ? cmpRanged.GetRange(type) : cmpRanged.GetFullAttackRange()) : cmpRanged.GetRange();
+ result.min = Math.min(result.min, range.min);
+ result.max = Math.max(result.max, range.max) + point.offset.z;
+ result.elevationBonus = Math.min(result.elevationBonus, range.elevationBonus);
+ }
+ }
+ if (atLeastOne)
+ return result;
+ }
+ return false;
};
UnitAI.prototype.CanGarrison = function(target)
@@ -6079,6 +6164,25 @@
});
};
+/**
+ * Add order to UnitAI components of all visibly garrisoned members.
+ */
+UnitAI.prototype.DelegateOrder = function(order)
+{
+ if (!order || !order.type)
+ return;
+
+ let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder)
+ return;
+
+ cmpGarrisonHolder.GetVisibleGarrisonPoints().forEach(point => {
+ let cmpUnitAI = Engine.QueryInterface(point.entity, IID_UnitAI);
+ if (cmpUnitAI)
+ cmpUnitAI.AddOrder(order.type, order, false);
+ });
+};
+
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_Attack.js
+++ binaries/data/mods/public/simulation/components/tests/test_Attack.js
@@ -149,7 +149,7 @@
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]);
- TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 });
+ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80, "elevationBonus": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
Index: binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js
+++ binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js
@@ -72,7 +72,9 @@
"GetOwner": () => friendlyPlayer
});
- AddMock(i, IID_Garrisonable, {});
+ AddMock(i, IID_Garrisonable, {
+ "IsUnloadable": () => true
+ });
AddMock(i, IID_Position, {
"GetHeightOffset": () => 0,
Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
+++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
@@ -581,7 +581,6 @@
"needsRepair": false,
"needsHeal": true,
"builder": true,
- "canGarrison": false,
"visibility": "visible",
"isBarterMarket":true,
"resourceTrickle": {
Index: binaries/data/mods/public/simulation/templates/special/filter/forcedgarrison.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/templates/special/filter/forcedgarrison.xml
@@ -0,0 +1,9 @@
+
+
+
+ true
+
+
+ false
+
+
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
@@ -28,7 +28,9 @@
2.5
-
+
+ true
+
corpse
Index: binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
+++ binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml
@@ -1,20 +1,36 @@
-
- units/heroes/brit_hero_boudicca
-
+
5.0
brit
- Chariot
+ Chariot -Hero
Boudicca (Chariot)
Boadicea
female
units/brit_hero_boudicca.png
+
+ 1
+ Infantry
+ 0
+ Infantry
+ 0
+ 2
+
+
+ units/brit_hero_boudicca_sword
+ true
+ 0
+ 1.4
+ -2.5
+ 0
+
+
+
units/britons/hero_chariot_javelinist_boudicca_m.xml