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
@@ -1862,10 +1862,24 @@
GuiInterface.prototype.CanAttack = function(player, data)
{
+ let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI);
+ if (cmpUnitAI)
+ return cmpUnitAI.CanAttack(data.target, data.types || undefined);
+
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
+GuiInterface.prototype.CanHeal = function(player, data)
+{
+ let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI);
+ if (cmpUnitAI)
+ return cmpUnitAI.CanHeal(data.target);
+
+ let cmpHeal = Engine.QueryInterface(data.entity, IID_Heal);
+ return cmpHeal && cmpHeal.CanHeal(data.target);
+};
+
/*
* Returns batch build time.
*/
@@ -2014,6 +2028,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,63 @@
},
"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"
});
}
/**
+ * Autogarrison the turrets as specified in the template.
+ * This function has to be called from ownership change for all components ought to be initialised.
+ */
+ AutogarrisonTurrets()
+ {
+ if (!this.turretPoints)
+ return;
+
+ for (let point of this.turretPoints)
+ if (point.template)
+ this.CreateTurret(point);
+ }
+
+ /**
+ * Add a turret 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 turret for.
+ *
+ * @return {boolean} - Whether the turret creation has succeeded.
+ */
+ CreateTurret(turretPoint)
+ {
+ // This position is already occupied.
+ if (turretPoint.entity)
+ return false;
+
+ let owner = INVALID_PLAYER;
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (cmpOwnership)
+ owner = cmpOwnership.GetOwner();
+
+ let ent = Engine.AddEntity(turretPoint.template);
+ let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(owner);
+
+ if (turretPoint.fixed)
+ return this.OccupyTurret(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)
+ return false;
+
+ return cmpGarrisonHolder.Garrison(ent, false, true) || Engine.DestroyEntity(ent);
+ }
+
+ /**
* @return {Object[]} - An array of the turret points this entity has.
*/
GetTurretPoints()
@@ -56,7 +108,7 @@
/**
* Occupy a turret point with the given entity.
* @param {number} entity - The entity to use.
- * @param {Object} turretPoint - Optionally the specific turret point to occupy.
+ * @param {Object} requestedTurretPoint - Optionally the specific turret point to occupy.
*
* @return {boolean} - Whether the occupation was successful.
*/
@@ -80,7 +132,7 @@
turretPoint = requestedTurretPoint;
}
else
- turretPoint = this.turretPoints.find(turret => !turret.entity && this.AllowedToOccupyTurret(entity, turret));
+ turretPoint = this.turretPoints.find(turret => this.AllowedToOccupyTurret(entity, turret));
if (!turretPoint)
return false;
@@ -199,6 +251,36 @@
this.OccupyTurret(to, turretPoint);
}
+ OnOwnershipChanged(msg)
+ {
+ // If we died, take fixed turrets with us.
+ 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);
+ }
+ }
+
+ if (msg.from == INVALID_PLAYER)
+ this.AutogarrisonTurrets();
+ }
+
OnGarrisonedUnitsChanged(msg)
{
// Ignore renaming for that is handled seperately
@@ -219,6 +301,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
@@ -354,6 +354,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())
@@ -1600,6 +1629,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))
@@ -3404,9 +3475,7 @@
UnitAI.prototype.IsTurret = function()
{
- if (!this.IsGarrisoned())
- return false;
- var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY;
};
@@ -5425,6 +5494,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);
@@ -5545,7 +5626,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);
};
/**
@@ -6104,21 +6202,79 @@
UnitAI.prototype.GetRange = function(iid, type)
{
let component = Engine.QueryInterface(this.entity, iid);
- if (!component)
+ if (component)
+ return component.GetRange(type);
+
+ let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
+ if (!cmpTurretHolder)
return undefined;
- return component.GetRange(type);
-}
+ let turretPoints = cmpTurretHolder.GetTurretPoints();
+ if (!turretPoints)
+ return undefined;
+
+ let result = {
+ "min": Infinity,
+ "max": -1,
+ "elevationBonus": 0
+ };
+
+ for (let point of turretPoints)
+ {
+ if (!point.entity)
+ continue;
+
+ 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 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);
+ if (!cmpTurretHolder)
+ return false;
+
+ let turretPoints = cmpTurretHolder.GetTurretPoints();
+ if (!turretPoints)
+ return false;
+
+ return turretPoints.some(point => {
+ if (!point.entity)
+ return false;
+
+ let cmpUnitAI = Engine.QueryInterface(point.entity, IID_UnitAI);
+ return cmpUnitAI && cmpUnitAI["Can" + action](target, types);
+ });
+};
-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)
+ // (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)
@@ -6179,7 +6335,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);
};
/**
@@ -6432,6 +6589,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/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,38 @@
-
- units/heroes/brit_hero_boudicca
-
+
5.0
brit
- Chariot
+ Chariot -Hero
Boudicca
Boudica
female
units/brit_hero_boudicca.png
+
+ 1
+ Unit
+ 0
+ Infantry
+ 0
+ 2
+
+
+
+
+ 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();