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/selection_panels.js
===================================================================
--- binaries/data/mods/public/gui/session/selection_panels.js
+++ binaries/data/mods/public/gui/session/selection_panels.js
@@ -348,7 +348,7 @@
for (let state of unitEntStates)
if (state.garrisonHolder)
- groups.add(state.garrisonHolder.entities);
+ groups.add(state.garrisonHolder.hiddenEntities);
return groups.getEntsGrouped();
},
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
@@ -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)
{
@@ -1113,7 +1109,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 && state.canGarrison.unloadable)
+ ++count;
+ }
if (!count)
return false;
@@ -1221,7 +1222,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
@@ -331,12 +331,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,12 +90,58 @@
"z": +points[point].Z
},
"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.
+ */
+GarrisonHolder.prototype.AutogarrisonTurrets = function()
+{
+ if (!this.visibleGarrisonPoints)
+ return;
+
+ for (let point of this.visibleGarrisonPoints)
+ if (point.template)
+ this.CreateTurret(point);
+};
+
+/**
+ * Add a turret as specified in the template.
+ * This function can be called from AutogarrisonTurrets
+ * or when retraining a turret.
+ *
+ * @param {Object} vgp - A visible garrison point to (re)create the predefined turret for.
+ *
+ * @return {boolean} - Whether the turret creation has succeeded.
+ */
+GarrisonHolder.prototype.CreateTurret = function(vgp)
+{
+ // This position is already occupied.
+ if (vgp.entity)
+ return false;
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership)
+ return false;
+ let owner = cmpOwnership.GetOwner();
+
+ let filter = vgp.fixed ? "fixedgarrison|" : "";
+ let ent = Engine.AddEntity(filter + vgp.template);
+ let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(owner);
+
+ return this.Garrison(ent, vgp, true) || Engine.DestroyEntity(ent);
+};
+
+/**
* @return {Object} max and min range at which entities can garrison the holder.
*/
GarrisonHolder.prototype.GetLoadingRange = function()
@@ -106,6 +163,51 @@
};
/**
+ * Get the entity IDs of the entities which are not visibly garrisoned.
+ * Used in the GUI to display in the garrison panel.
+ *
+ * @return {number[]} - An array containing entity IDs.
+ */
+GarrisonHolder.prototype.GetHiddenGarrisonedEntities = function()
+{
+ return this.entities.filter(entity => !this.IsVisiblyGarrisoned(entity));
+};
+
+/**
+ * Test whether the entity is visibly garrisoned.
+ *
+ * @param {number} entity - The entity ID of the entity to check.
+ *
+ * @return {boolean} - Whether the entity is visibly garrisoned.
+ */
+GarrisonHolder.prototype.IsVisiblyGarrisoned = function(entity)
+{
+ return this.visibleGarrisonPoints.some(vgp => vgp.entity == entity);
+};
+
+/**
+ * Get the visible garrison points of this entity.
+ *
+ * @return {Object[]} - An array containing visibly garrison points.
+ */
+GarrisonHolder.prototype.GetVisibleGarrisonPoints = function()
+{
+ return this.visibleGarrisonPoints;
+};
+
+/**
+ * Get the visible garrison point this entity occupies.
+ *
+ * @param {number} entity - The entity ID of the entity to check.
+ *
+ * @return {Object[]} - An array containing visibly garrison points.
+ */
+GarrisonHolder.prototype.GetVisibleGarrisonPoint = function(entity)
+{
+ return this.visibleGarrisonPoints.find(vgp => vgp.entity == entity);
+};
+
+/**
* @return {Array} unit classes which can be garrisoned inside this
* particular entity. Obtained from the entity's template.
*/
@@ -179,18 +281,20 @@
};
/**
- * Garrison a unit inside. The timer for AutoHeal is started here.
- * @param {number} vgpEntity - The visual garrison point that will be used.
+ * Garrison a unit inside. The timer for AutoHeal is started in PerformGarrison.
+ * @param {Object} vgpEntity - The visual garrison point that will be used.
* If vgpEntity is given, this visualGarrisonPoint will be used for the entity.
- * @return {boolean} Whether the entity was garrisonned.
+ * @param {boolean} forced - Whether the garrison is forced (e.g. a turret created on init).
+ *
+ * @return {boolean} Whether the entity was garrisoned.
*/
-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;
@@ -213,7 +317,7 @@
// 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.
let cmpTurretPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!vgpEntity && visibleGarrisonPoint.angle != null)
+ if (!vgpEntity && visibleGarrisonPoint.angle != null || forced)
cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle);
else if (!vgpEntity && !cmpPosition.IsInWorld())
cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI);
@@ -223,7 +327,10 @@
cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset);
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
+ {
cmpUnitAI.SetTurretStance();
+ cmpUnitAI.SetGarrisoned();
+ }
}
else
cmpPosition.MoveOutOfWorld();
@@ -232,18 +339,22 @@
};
/**
- * @return {boolean} Whether the entity was garrisonned.
+ * @param {number} entity - The entity ID to garrison.
+ * @param {boolean} forced - Whether the garrison has to be forced (e.g. turret creation on init).
+ *
+ * @return {boolean} Whether the entity was garrisoned.
*/
-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))
+ // Check if the unit is allowed to be garrisoned inside this entity.
+ // If it is forced we don't care whether it is allowed or not.
+ if (!forced && !this.IsAllowedToGarrison(entity))
return false;
- // Check the capacity
+ // Check the capacity.
let extraCount = 0;
let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
@@ -431,7 +542,7 @@
*/
GarrisonHolder.prototype.Unload = function(entity, forced)
{
- return this.PerformEject([entity], forced);
+ return this.IsUnloadable(entity) && this.PerformEject([entity], forced);
};
/**
@@ -477,7 +588,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 +600,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);
};
/**
@@ -561,18 +672,39 @@
*/
GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg)
{
- // The ownership change may be on the garrisonholder
+ // The ownership change may be on the garrisonholder...
if (this.entity == msg.entity)
{
+
let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent));
+ // If the visible garrison point is forced and the garrisonholder did not die, transfer the turrets ownership as well.
+ if (msg.to != INVALID_PLAYER)
+ for (let point of this.visibleGarrisonPoints)
+ if (point.entity != null && point.forced)
+ {
+ let entIndex = this.entities.indexOf(point.entity);
+ if (entIndex == -1)
+ continue;
+ let cmpEntOwnership = Engine.QueryInterface(point.entity, IID_Ownership)
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(msg.to);
+ entities.splice(entIndex, 1);
+ }
+
if (entities.length)
this.EjectOrKill(entities);
+ // If the garrison holder has just been created, autogarrison turrets.
+ // If there is a initGarrison, assume the turrets will already be created
+ // (e.g. by loading a map) and don't create new ones.
+ if (msg.from == INVALID_PLAYER && !this.initGarrison)
+ this.AutogarrisonTurrets();
+
return;
}
- // or on some of its garrisoned units
+ // ...or on some of its garrisoned units.
let entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
@@ -666,6 +798,13 @@
if (cmpHealth)
cmpHealth.Kill();
this.entities.splice(entityIndex, 1);
+ for (let vgp of this.visibleGarrisonPoints)
+ {
+ if (vgp.entity != entity)
+ continue;
+ vgp.entity = null;
+ break;
+ }
killedEntities.push(entity);
}
@@ -681,7 +820,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;
@@ -701,6 +840,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.some(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,40 @@
-function Garrisonable() {}
+class Garrisonable
+{
+ get Schema()
+ {
+ return "Controls the garrisonability of an entity." +
+ "" +
+ "true" +
+ "" +
+ "" +
+ "" +
+ "";
+ }
-Garrisonable.prototype.Schema = "";
+ Init()
+ {
+ this.SetUnloadable(this.template.Unloadable == "true");
+ }
-Garrisonable.prototype.Init = function()
-{
-};
+ /**
+ * Gets the ability to ungarrison.
+ *
+ * @return {boolean} Whether ungarrisoning is allowed.
+ */
+ IsUnloadable()
+ {
+ return this.unloadable;
+ }
-Garrisonable.prototype.Serialize = null;
+ /**
+ * Sets the ability to ungarrison.
+ *
+ * @param {boolean} unloadable - Whether ungarrisoning is allowed.
+ */
+ SetUnloadable(unloadable)
+ {
+ this.unloadable = unloadable;
+ }
+}
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
@@ -360,13 +360,18 @@
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
+ "hiddenEntities": cmpGarrisonHolder.GetHiddenGarrisonedEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"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)
@@ -1841,8 +1846,14 @@
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);
+};
+
+GuiInterface.prototype.CanHeal = function(player, data)
+{
+ let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI);
+ return cmpUnitAI && cmpUnitAI.CanHeal(data.target);
};
/*
@@ -1991,6 +2002,7 @@
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
+ "CanHeal": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
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
@@ -347,6 +347,32 @@
this.SetNextState("INDIVIDUAL.WALKING");
},
+ "Order.Follow": function(msg) {
+ // Let players move captured domestic animals around.
+ if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
+ {
+ 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;
+
+ 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())
@@ -1585,6 +1611,49 @@
},
},
+ "FOLLOWING": {
+ "enter": function() {
+ if (!this.MoveTo(this.order.data, this.order.data.iid))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ this.StartTimer(0, 500);
+ return false;
+ },
+
+ "leave": function() {
+ this.StopMoving();
+ this.StopTimer();
+ },
+
+ "Timer": function() {
+ if (this.CheckRange(this.order.data, this.order.data.iid || undefined))
+ {
+ this.StopMoving();
+ this.DelegateOrder(this.order.data);
+ }
+ else if (!this.MoveTo(this.order.data, this.order.data.iid || undefined))
+ {
+ this.FinishOrder();
+ return true;
+ }
+
+ // Run after a fleeing target.
+ let cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
+ if (cmpUnitAI && cmpUnitAI.IsFleeing())
+ this.SetSpeedMultiplier(this.GetRunMultiplier());
+ },
+
+ "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.order.data.iid || undefined))
+ this.DelegateOrder(this.order.data);
+ },
+ },
+
"WALKINGANDFIGHTING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
@@ -5266,6 +5335,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);
@@ -5383,7 +5464,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);
};
/**
@@ -5925,21 +6023,79 @@
UnitAI.prototype.GetRange = function(iid, type)
{
let component = Engine.QueryInterface(this.entity, iid);
- if (!component)
- return undefined;
+ if (component)
+ return component.GetRange(type);
+
+ let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder)
+ return false;
+
+ let visibleGarrisonPoints = cmpGarrisonHolder.GetVisibleGarrisonPoints();
+ if (!visibleGarrisonPoints)
+ return false;
+
+ let result = {
+ "min": Infinity,
+ "max": -1,
+ "elevationBonus": 0
+ };
+
+ for (let point of visibleGarrisonPoints)
+ {
+ 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);
+ }
+
+ 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.CheckActionSubunits = function(action, target, types)
+{
+ let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder)
+ return false;
- return component.GetRange(type);
+ let visibleGarrisonPoints = cmpGarrisonHolder.GetVisibleGarrisonPoints();
+ if (!visibleGarrisonPoints)
+ return false;
+
+ return visibleGarrisonPoints.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.CheckActionSubunits("Attack", target, types);
};
UnitAI.prototype.CanGarrison = function(target)
@@ -5995,14 +6151,15 @@
UnitAI.prototype.CanHeal = function(target)
{
// 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;
- // Verify that we're able to respond to Heal commands
+ // Verify that we're able to respond to Heal commands,
+ // or any of our visible garrison points can.
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (!cmpHeal)
- return false;
+ return this.CheckActionSubunits("Heal", target);
// Verify that the target is alive
if (!this.TargetIsAlive(target))
@@ -6261,6 +6418,31 @@
});
};
+/**
+ * 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;
+
+ for (let point of cmpGarrisonHolder.GetVisibleGarrisonPoints())
+ {
+ let cmpUnitAI = Engine.QueryInterface(point.entity, IID_UnitAI);
+ if (!cmpUnitAI)
+ continue;
+
+ let currentOrder = cmpUnitAI.GetOrders()[0];
+ if (!currentOrder || !currentOrder.data || !currentOrder.data.target ||
+ currentOrder.data.target != order.target || currentOrder.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_Attack.js
===================================================================
--- binaries/data/mods/public/simulation/components/tests/test_Attack.js
+++ binaries/data/mods/public/simulation/components/tests/test_Attack.js
@@ -150,7 +150,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
@@ -13,6 +13,7 @@
const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33];
const garrisonHolderId = 15;
const unitToGarrisonId = 24;
+const turretToGarrisonId = 35;
const enemyUnitId = 34;
const player = 1;
const friendlyPlayer = 2;
@@ -52,7 +53,7 @@
"GetPlayerByID": id => id
});
-for (let i = 24; i <= 34; ++i)
+for (let i = 24; i <= 35; ++i)
{
AddMock(i, IID_Identity, {
"GetClassesList": () => "Infantry+Cavalry",
@@ -72,7 +73,9 @@
"GetOwner": () => friendlyPlayer
});
- AddMock(i, IID_Garrisonable, {});
+ AddMock(i, IID_Garrisonable, {
+ "IsUnloadable": () => i != turretToGarrisonId
+ });
AddMock(i, IID_Position, {
"GetHeightOffset": () => 0,
@@ -168,3 +171,7 @@
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
+
+// Check that a non-unloadable entity is not unloadable.
+TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(turretToGarrisonId), true);
+TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(turretToGarrisonId), false);
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
@@ -586,7 +586,6 @@
"needsRepair": false,
"needsHeal": true,
"builder": true,
- "canGarrison": false,
"visibility": "visible",
"isBarterMarket":true,
"resourceTrickle": {
Index: binaries/data/mods/public/simulation/templates/special/filter/fixedgarrison.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/simulation/templates/special/filter/fixedgarrison.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
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/graphics/MapReader.cpp
===================================================================
--- source/graphics/MapReader.cpp
+++ source/graphics/MapReader.cpp
@@ -1040,10 +1040,8 @@
// TODO: other parts of the position
}
- CmpPtr cmpOwnership(sim, ent);
- if (cmpOwnership)
- cmpOwnership->SetOwner(PlayerID);
-
+ // Should be before the ownership change due to autocreation of
+ // turrets in GarrisonHolder.js.
if (!Garrison.empty())
{
CmpPtr cmpGarrisonHolder(sim, ent);
@@ -1054,6 +1052,10 @@
Garrison.clear();
}
+ CmpPtr cmpOwnership(sim, ent);
+ if (cmpOwnership)
+ cmpOwnership->SetOwner(PlayerID);
+
CmpPtr cmpObstruction(sim, ent);
if (cmpObstruction)
{
Index: source/simulation2/components/CCmpPosition.cpp
===================================================================
--- source/simulation2/components/CCmpPosition.cpp
+++ source/simulation2/components/CCmpPosition.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -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();