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
@@ -155,7 +155,7 @@
},
"getActionInfo": function(entState, targetState)
{
- if (!entState.attack || !targetState.hitpoints)
+ if (!targetState.hitpoints)
return false;
return {
@@ -201,7 +201,7 @@
},
"getActionInfo": function(entState, targetState)
{
- if (!entState.attack || !targetState.hitpoints)
+ if (!targetState.hitpoints)
return false;
return {
@@ -310,21 +310,17 @@
},
"getActionInfo": function(entState, targetState)
{
- if (!entState.heal ||
- !hasClass(targetState, "Unit") || !targetState.needsHeal ||
+ if (!hasClass(targetState, "Unit") || !targetState.needsHeal ||
!playerCheck(entState, targetState, ["Player", "Ally"]) ||
entState.id == targetState.id) // Healers can't heal themselves.
return false;
- let unhealableClasses = entState.heal.unhealableClasses;
- if (MatchesClassList(targetState.identity.classes, unhealableClasses))
- return false;
-
- let healableClasses = entState.heal.healableClasses;
- if (!MatchesClassList(targetState.identity.classes, healableClasses))
- return false;
-
- return { "possible": true };
+ return {
+ "possible": Engine.GuiInterfaceCall("CanHeal", {
+ "entity": entState.id,
+ "target": targetState.id
+ })
+ };
},
"actionCheck": function(target, selection)
{
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
@@ -159,15 +159,17 @@
/**
* @param {number} entity - The entityID to garrison.
* @param {boolean} renamed - Whether the entity was renamed.
+ * @param {boolean} forced - Whether the entity needs to be forcefully garrisoned
+ * (i.e. ignoring prerequisites).
*
* @return {boolean} - Whether the entity was garrisoned.
*/
-GarrisonHolder.prototype.Garrison = function(entity, renamed = false)
+GarrisonHolder.prototype.Garrison = function(entity, renamed = false, forced = false)
{
- if (!this.IsAllowedToGarrison(entity))
+ if (!forced && !this.IsAllowedToGarrison(entity))
return false;
- if (!this.HasEnoughHealth())
+ if (!forced && !this.HasEnoughHealth())
return false;
if (!this.timer && this.GetHealRate() > 0)
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
@@ -1866,7 +1866,21 @@
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
- return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
+ if (cmpAttack)
+ return cmpAttack.CanAttack(data.target, data.types || undefined);
+
+ let cmpTurretHolder = Engine.QueryInterface(data.entity, IID_TurretHolder);
+ return cmpTurretHolder && cmpTurretHolder.CanAnySubunitPerform("Attack", data.target, data.types || undefined);
+};
+
+GuiInterface.prototype.CanHeal = function(player, data)
+{
+ let cmpHeal = Engine.QueryInterface(data.entity, IID_Heal);
+ if (cmpHeal)
+ return cmpHeal.CanHeal(data.target);
+
+ let cmpTurretHolder = Engine.QueryInterface(data.entity, IID_TurretHolder);
+ return cmpTurretHolder && cmpTurretHolder.CanAnySubunitPerform("Heal", data.target, data.types || undefined);
};
/*
@@ -2017,6 +2031,7 @@
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
+ "CanHeal": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
Index: binaries/data/mods/public/simulation/components/TurretHolder.js
===================================================================
--- binaries/data/mods/public/simulation/components/TurretHolder.js
+++ binaries/data/mods/public/simulation/components/TurretHolder.js
@@ -20,11 +20,54 @@
},
"allowedClasses": points[point].AllowedClasses,
"angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null,
- "entity": null
+ "entity": null,
+ "template": points[point].Template,
+ "fixed": points[point].Fixed && points[point].Fixed == "true"
});
}
/**
+ * Add a subunit as specified in the template.
+ * This function creates an entity and places it on the turret point.
+ *
+ * @param {Object} turretPoint - A turret point to (re)create the predefined subunit for.
+ *
+ * @return {boolean} - Whether the turret creation has succeeded.
+ */
+ CreateSubunit(turretPoint)
+ {
+ // This position is already occupied.
+ if (turretPoint.entity)
+ return false;
+
+ let ent = Engine.AddEntity(turretPoint.template);
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (cmpOwnership)
+ {
+ let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(cmpOwnership.GetOwner());
+ }
+
+ if (turretPoint.fixed)
+ return this.OccupyTurretPoint(ent, turretPoint) || Engine.DestroyEntity(ent);
+
+ // For now, require a GarrisonHolder if it is not a fixed turret.
+ // That is needed to be able to unload.
+ let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder)
+ {
+ error("For now we require a GarrisonHolder to use non-fixed subunits on entity " + this.entity + ".");
+ this.LeaveTurretPoint(ent, turretPoint);
+ Engine.DestroyEntity(ent);
+ return false;
+ }
+
+ return cmpGarrisonHolder.Garrison(ent, false, true) || Engine.DestroyEntity(ent);
+ }
+
+ /**
* @return {Object[]} - An array of the turret points this entity has.
*/
GetTurretPoints()
@@ -33,12 +76,25 @@
}
/**
+ * @return {number[]} - An array of the entityIDs of the subunits this entity has.
+ */
+ GetSubunits()
+ {
+ let subunits = [];
+ for (let turretPoint of this.turretPoints)
+ if (turretPoint.entity)
+ subunits.push(turretPoint.entity);
+
+ return subunits;
+ }
+
+ /**
* @param {number} entity - The entity to check for.
* @param {Object} turretPoint - The turret point to use.
*
* @return {boolean} - Whether the entity is allowed to occupy the specified turret point.
*/
- AllowedToOccupyTurret(entity, turretPoint)
+ AllowedToOccupyTurretPoint(entity, turretPoint)
{
if (!turretPoint || turretPoint.entity)
return false;
@@ -60,8 +116,11 @@
*
* @return {boolean} - Whether the occupation was successful.
*/
- OccupyTurret(entity, requestedTurretPoint)
+ OccupyTurretPoint(entity, requestedTurretPoint)
{
+ if (this.OccupiesTurretPoint(entity))
+ return false;
+
let cmpPositionOccupant = Engine.QueryInterface(entity, IID_Position);
if (!cmpPositionOccupant)
return false;
@@ -70,28 +129,26 @@
if (!cmpPositionSelf)
return false;
- if (this.OccupiesTurret(entity))
- return false;
-
let turretPoint;
if (requestedTurretPoint)
{
- if (this.AllowedToOccupyTurret(entity, requestedTurretPoint))
+ if (this.AllowedToOccupyTurretPoint(entity, requestedTurretPoint))
turretPoint = requestedTurretPoint;
}
else
- turretPoint = this.turretPoints.find(turret => !turret.entity && this.AllowedToOccupyTurret(entity, turret));
+ turretPoint = this.turretPoints.find(turret => this.AllowedToOccupyTurretPoint(entity, turret));
if (!turretPoint)
return false;
turretPoint.entity = entity;
- // Angle of turrets:
+
+ // Angle of subunits:
// Renamed entities (turretPoint != undefined) should keep their angle.
// Otherwise if an angle is given in the turretPoint, use it.
// If no such angle given (usually walls for which outside/inside not well defined), we keep
- // the current angle as it was used for garrisoning and thus quite often was from inside to
- // outside, except when garrisoning from outWorld where we take as default PI.
+ // the current angle as it was used for occupying and thus quite often was from inside to
+ // outside, except when occupying from outOfWorld where we take as default PI.
if (!turretPoint && turretPoint.angle != null)
cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + turretPoint.angle);
else if (!turretPoint && !cmpPosition.IsInWorld())
@@ -121,13 +178,13 @@
}
/**
- * Remove the entity from a turret.
+ * Remove the entity from a turret point.
* @param {number} entity - The specific entity to eject.
- * @param {Object} turret - Optionally the turret to abandon.
+ * @param {Object} requestedTurretPoint - Optionally the turret point to abandon.
*
* @return {boolean} - Whether the entity was occupying a/the turret before.
*/
- LeaveTurret(entity, requestedTurretPoint)
+ LeaveTurretPoint(entity, requestedTurretPoint)
{
let turretPoint;
if (requestedTurretPoint)
@@ -169,11 +226,11 @@
/**
* @param {number} entity - The entity's id.
- * @param {Object} turret - Optionally the turret to check.
+ * @param {Object} requestedTurretPoint - Optionally the turret point to check.
*
* @return {boolean} - Whether the entity is positioned on a turret of this entity.
*/
- OccupiesTurret(entity, requestedTurretPoint)
+ OccupiesTurretPoint(entity, requestedTurretPoint)
{
return requestedTurretPoint ? requestedTurretPoint.entity == entity :
this.turretPoints.some(turretPoint => turretPoint.entity == entity);
@@ -183,12 +240,65 @@
* @param {number} entity - The entity's id.
* @return {Object} - The turret this entity is positioned on, if applicable.
*/
- GetOccupiedTurret(entity)
+ GetOccupiedTurretPoint(entity)
{
return this.turretPoints.find(turretPoint => turretPoint.entity == entity);
}
/**
+ * Get the range of the subunits given the IID and optionally type.
+ * @param {number} iid - The IID to get the range for.
+ * @param {string} type - [Optional]
+ * @return {Object | undefined} - The range in the form
+ * { "min": number, "max": number }
+ * Object."elevationBonus": number may be present when iid == IID_Attack.
+ * Returns undefined when the entity does not have the requested component.
+ */
+ GetSubunitsRange(iid, type)
+ {
+ let result = {
+ "min": Infinity,
+ "max": -1,
+ "elevationBonus": Infinity
+ };
+
+ for (let point of this.turretPoints)
+ {
+ if (!point.entity)
+ continue;
+
+ let component = Engine.QueryInterface(point.entity, iid);
+ if (!component)
+ continue;
+
+ let range = component.GetRange(type);
+ 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 || 0);
+ }
+
+ return result.max == -1 ? undefined : result;
+ }
+
+ /**
+ * Checks whether one of this entities subunits can perform the given action on the given target.
+ * Restricted to entities with UnitAI for now.
+ *
+ * @param {string} action - The desired action to check.
+ * @param {number} target - The entity ID of the target to perform the action upon.
+ * @param {string[]} types - An optional array of the desired types to use (e.g. "Capture" for attack).
+ *
+ * @return {boolean} Whether at least one subunit can perform the given action.
+ */
+ CanAnySubunitPerform(action, target, types)
+ {
+ return this.GetSubunits().some(entity => {
+ let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
+ return cmpUnitAI && cmpUnitAI["Can" + action](target, types);
+ });
+ };
+
+ /**
* We process EntityRenamed here because we need to be sure that we receive
* it after it is processed by GarrisonHolder.js.
* ToDo: Make this not needed by fully separating TurretHolder from GarrisonHolder.
@@ -200,13 +310,48 @@
*/
SwapEntities(from, to)
{
- let turretPoint = this.GetOccupiedTurret(from);
+ let turretPoint = this.GetOccupiedTurretPoint(from);
if (turretPoint)
- this.LeaveTurret(from, turretPoint);
+ this.LeaveTurretPoint(from, turretPoint);
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.IsGarrisoned(to))
- this.OccupyTurret(to, turretPoint);
+ this.OccupyTurretPoint(to, turretPoint);
+ }
+
+ OnOwnershipChanged(msg)
+ {
+ // If we died, take fixed subunits with us.
+ // Other ownership changes are handled by GarrisonHolder.js.
+ if (msg.to == INVALID_PLAYER)
+ for (let point of this.turretPoints)
+ if (point.entity != null && point.fixed)
+ {
+ let cmpHealth = Engine.QueryInterface(point.entity, IID_Health);
+ if (cmpHealth)
+ cmpHealth.Kill();
+ else
+ Engine.DestroyEntity(point.entity);
+ }
+ else
+ {
+ // If the turret point is forced and the garrisonholder did not die,
+ // transfer the turrets ownership as well.
+ for (let point of this.turretPoints)
+ if (point.entity != null && point.fixed)
+ {
+ let cmpEntOwnership = Engine.QueryInterface(point.entity, IID_Ownership)
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(msg.to);
+ }
+ }
+
+ // We were created, create any subunits now.
+ // This has to be done here for Ownership ought to be initialised.
+ if (msg.from == INVALID_PLAYER)
+ for (let point of this.turretPoints)
+ if (point.template)
+ this.CreateSubunit(point);
}
OnGarrisonedUnitsChanged(msg)
@@ -217,9 +362,9 @@
return;
for (let entity of msg.removed)
- this.LeaveTurret(entity);
+ this.LeaveTurretPoint(entity);
for (let entity of msg.added)
- this.OccupyTurret(entity);
+ this.OccupyTurretPoint(entity);
}
}
@@ -229,6 +374,16 @@
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
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
@@ -358,6 +358,35 @@
this.SetNextState("INDIVIDUAL.WALKING");
},
+ "Order.Follow": function(msg) {
+ if (!this.AbleToMove())
+ {
+ this.FinishOrder();
+ return;
+ }
+
+ // For packable units:
+ // 1. If packed, we can move.
+ // 2. If unpacked, we first need to pack, then follow case 1.
+ if (this.CanPack())
+ {
+ this.PushOrderFront("Pack", { "force": true });
+ return;
+ }
+
+ // It's not too bad if we don't arrive at exactly the right position.
+ this.order.data.relaxed = true;
+ this.order.data.range = this.GetRange(this.order.data.iid);
+
+ // We need a bit of leeway when following.
+ this.order.data.range.max *= 0.9;
+
+ if (this.IsAnimal())
+ this.SetNextState("ANIMAL.WALKING");
+ else
+ this.SetNextState("INDIVIDUAL.FOLLOWING");
+ },
+
"Order.PickupUnit": function(msg) {
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
@@ -1608,6 +1637,48 @@
},
},
+ "FOLLOWING": {
+ "enter": function() {
+ if (!this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ this.StartTimer(0, 500);
+ return false;
+ },
+
+ "leave": function() {
+ this.StopMoving();
+ this.ResetSpeedMultiplier();
+ this.StopTimer();
+ },
+
+ "Timer": function() {
+ if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max))
+ {
+ this.StopMoving();
+ this.DelegateOrder(this.order.data);
+ }
+ else if (!this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ },
+
+ "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.CheckTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max))
+ {
+ this.TryMatchTargetSpeed(this.order.data.target, true);
+ this.DelegateOrder(this.order.data);
+ }
+ },
+ },
+
"WALKINGANDFIGHTING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
@@ -5472,6 +5543,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("Follow", order, queued);
+ return;
+ }
+
this.RememberTargetPosition(order);
this.AddOrder("Attack", order, queued);
@@ -5592,7 +5675,24 @@
return;
}
- this.AddOrder("Heal", { "target": target, "force": true }, queued);
+ let order = {
+ "target": target,
+ "force": true
+ };
+
+ // This is the case when an entity that has visibly garrisoned entities that can
+ // heal, but the entity itself can't heal.
+ let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal)
+ if (!cmpHeal)
+ {
+ order.type = "Heal";
+ order.iid = IID_Heal;
+
+ this.AddOrder("Follow", order, queued);
+ return;
+ }
+
+ this.AddOrder("Heal", order, queued);
};
/**
@@ -6170,21 +6270,42 @@
UnitAI.prototype.GetRange = function(iid, type)
{
let component = Engine.QueryInterface(this.entity, iid);
- if (!component)
- return undefined;
+ if (component)
+ return component.GetRange(type);
- return component.GetRange(type);
-}
+ let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
+ if (cmpTurretHolder)
+ return cmpTurretHolder.GetSubunitsRange(iid, type);
-UnitAI.prototype.CanAttack = function(target)
+ return undefined;
+};
+
+/**
+ * Checks whether one of this entities turrets can perform the given action on the given target.
+ *
+ * @param {string} action - The desired action to check.
+ * @param {number} target - The entity ID of the target to perform the action upon.
+ * @param {string[]} types - An optional array of the desired types to use (e.g. "Capture" for attack).
+ *
+ * @return {boolean} Whether at least one subunit can perform the given action.
+ */
+UnitAI.prototype.CanAnySubUnitPerform = function(action, target, types)
+{
+ let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
+ return cmpTurretHolder &&
+ cmpTurretHolder.CanAnySubunitPerform(action, target, types);
+};
+
+UnitAI.prototype.CanAttack = function(target, types)
{
// Formation controllers should always respond to commands
- // (then the individual units can make up their own minds)
+ // (then the individual units can make up their own minds).
if (this.IsFormationController())
return true;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
- return cmpAttack && cmpAttack.CanAttack(target);
+ return cmpAttack ? cmpAttack.CanAttack(target, types) :
+ this.CanAnySubUnitPerform("Attack", target, types);
};
UnitAI.prototype.CanGarrison = function(target)
@@ -6245,7 +6366,8 @@
return true;
let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
- return cmpHeal && cmpHeal.CanHeal(target);
+ return cmpHeal ? cmpHeal.CanHeal(target) :
+ this.CanAnySubUnitPerform("Heal", target);
};
/**
@@ -6498,6 +6620,33 @@
});
};
+/**
+ * Add order to UnitAI components of all turrets.
+ * @param {Object} order - The order to delegate.
+ */
+UnitAI.prototype.DelegateOrder = function(order)
+{
+ if (!order || !order.type)
+ return;
+
+ let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
+ if (!cmpTurretHolder)
+ return;
+
+ for (let point of cmpTurretHolder.GetTurretPoints())
+ {
+ let cmpUnitAI = Engine.QueryInterface(point.entity, IID_UnitAI);
+ if (!cmpUnitAI)
+ continue;
+
+ let orders = cmpUnitAI.GetOrders();
+ if (!orders.length ||
+ !orders[0].data || !orders[0].data.target ||
+ orders[0].data.target != order.target || orders[0].type != order.type)
+ 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_TurretHolder.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js
+++ binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js
@@ -1,4 +1,5 @@
Engine.LoadHelperScript("Player.js");
+Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("TurretHolder.js");
@@ -91,51 +92,77 @@
});
// Test visible garrisoning restrictions.
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[0]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[1]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[2]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(archerID, cmpTurretHolder.turretPoints[0]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(archerID, cmpTurretHolder.turretPoints[1]), false);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(archerID, cmpTurretHolder.turretPoints[2]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(cavID, cmpTurretHolder.turretPoints[0]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(cavID, cmpTurretHolder.turretPoints[1]), false);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(cavID, cmpTurretHolder.turretPoints[2]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(infID, cmpTurretHolder.turretPoints[0]), true);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(infID, cmpTurretHolder.turretPoints[1]), false);
-TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(infID, cmpTurretHolder.turretPoints[2]), false);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[0]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[1]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[2]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[0]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[1]), false);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[2]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[0]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[1]), false);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[2]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[0]), true);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[1]), false);
+TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[2]), false);
// Test that one cannot leave a turret that is not occupied.
-TS_ASSERT(!cmpTurretHolder.LeaveTurret(archerID));
+TS_ASSERT(!cmpTurretHolder.LeaveTurretPoint(archerID));
// Test occupying a turret.
-TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID));
-TS_ASSERT(cmpTurretHolder.OccupyTurret(archerID));
-TS_ASSERT(cmpTurretHolder.OccupiesTurret(archerID));
+TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID));
+TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(archerID));
+TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(archerID));
// We're not occupying a turret that we can't occupy.
-TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID, cmpTurretHolder.turretPoints[1]));
-TS_ASSERT(!cmpTurretHolder.OccupyTurret(cavID, cmpTurretHolder.turretPoints[1]));
-TS_ASSERT(!cmpTurretHolder.OccupyTurret(cavID, cmpTurretHolder.turretPoints[0]));
-TS_ASSERT(cmpTurretHolder.OccupyTurret(cavID, cmpTurretHolder.turretPoints[2]));
+TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID, cmpTurretHolder.turretPoints[1]));
+TS_ASSERT(!cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[1]));
+TS_ASSERT(!cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[0]));
+TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[2]));
// Leave turrets.
-TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID));
-TS_ASSERT(!cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[1]));
-TS_ASSERT(cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[2]));
+TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID));
+TS_ASSERT(!cmpTurretHolder.LeaveTurretPoint(cavID, cmpTurretHolder.turretPoints[1]));
+TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(cavID, cmpTurretHolder.turretPoints[2]));
// Test renaming.
AddMock(turretHolderID, IID_GarrisonHolder, {
"IsGarrisoned": () => true
});
-TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[2]));
+TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[2]));
cmpTurretHolder.SwapEntities(siegeEngineID, archerID);
-TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID));
-TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID));
+TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(siegeEngineID));
+TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID));
// Renaming into an entity not allowed on the same turret point hides us.
-TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[1]));
+TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[1]));
cmpTurretHolder.SwapEntities(siegeEngineID, archerID);
-TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID));
-TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID));
+TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(siegeEngineID));
+TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID));
+
+// Test getting the range of subunits.
+TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(archerID));
+AddMock(archerID, IID_Attack, {
+ "GetRange": () => { return { "min": 0, "max": 5, "elevationBonus": 2 }; }
+});
+let expectedResult = {
+ "min": 0,
+ "max": 5 + cmpTurretHolder.GetOccupiedTurretPoint(archerID).offset.z,
+ "elevationBonus": 2
+};
+let result = cmpTurretHolder.GetSubunitsRange(IID_Attack);
+TS_ASSERT(result.min == expectedResult.min &&
+ result.max == expectedResult.max &&
+ result.elevationBonus == expectedResult.elevationBonus);
+TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID));
+
+// Test getting the actions of subunits.
+TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(archerID));
+AddMock(archerID, IID_UnitAI, {
+ "CanAttack": (target) => target == 90
+});
+TS_ASSERT(cmpTurretHolder.CanAnySubUnitPerform("Attack", 90, undefined));
+TS_ASSERT(!cmpTurretHolder.CanAnySubUnitPerform("Attack", 91, undefined));
+DeleteMock(archerID, IID_UnitAI);
+TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID));
// ToDo: Ownership changes are handled by GarrisonHolder.js.
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,18 +1,24 @@
-
- units/heroes/brit_hero_boudicca
-
+
6.0
+
+ 1
+ Unit
+ 0
+ Infantry
+ 0
+ 2
+
brit
female
Boudicca
Boudica
- Chariot
+ Chariot -Hero
units/brit_hero_boudicca.png
@@ -22,6 +28,18 @@
+
+
+
+ units/brit_hero_boudicca_sword
+ false
+ 0
+ 1.4
+ -2.5
+ 0
+
+
+
units/britons/hero_chariot_javelinist_boudicca_m.xml
Index: source/simulation2/components/CCmpPosition.cpp
===================================================================
--- source/simulation2/components/CCmpPosition.cpp
+++ source/simulation2/components/CCmpPosition.cpp
@@ -21,6 +21,7 @@
#include "ICmpPosition.h"
#include "simulation2/MessageTypes.h"
+#include "simulation2/serialization/SerializeTemplates.h"
#include "ICmpTerrain.h"
#include "ICmpTerritoryManager.h"
@@ -226,6 +227,7 @@
serialize.NumberFixed_Unbounded("y", m_TurretPosition.Y);
serialize.NumberFixed_Unbounded("z", m_TurretPosition.Z);
}
+ SerializeSet()(serialize, "turrets", m_Turrets);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
@@ -262,6 +264,7 @@
deserialize.NumberFixed_Unbounded("y", m_TurretPosition.Y);
deserialize.NumberFixed_Unbounded("z", m_TurretPosition.Z);
}
+ SerializeSet()(deserialize, "turrets", m_Turrets);
if (m_InWorld)
UpdateXZRotation();