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
@@ -700,6 +700,84 @@
"specificness": 0,
},
+ "occupy-turret":
+ {
+ "execute": function(target, action, selection, queued)
+ {
+ Engine.PostNetworkCommand({
+ "type": "occupy-turret",
+ "entities": selection,
+ "target": action.target,
+ "queued": queued
+ });
+
+ Engine.GuiInterfaceCall("PlaySound", {
+ "name": "order_garrison",
+ "entity": selection[0]
+ });
+
+ return true;
+ },
+ "getActionInfo": function(entState, targetState)
+ {
+ if (!entState.canGarrison || !targetState.turretHolder ||
+ !playerCheck(entState, targetState, ["Player", "MutualAlly"]))
+ return false;
+
+ if (!targetState.turretHolder.turretPoints.find(point =>
+ !point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
+ return false;
+
+ let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
+ let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
+ "occupied": occupiedTurrets.length,
+ "capacity": targetState.turretHolder.turretPoints.length
+ });
+
+ if (occupiedTurrets == targetState.turretHolder.turretPoints.length)
+ tooltip = coloredText(tooltip, "orange");
+
+ return {
+ "possible": true,
+ "tooltip": tooltip
+ };
+ },
+ "preSelectedActionCheck": function(target, selection)
+ {
+ if (preSelectedAction != ACTION_GARRISON)
+ return false;
+
+ let actionInfo = getActionInfo("occupy-turret", target, selection);
+ if (!actionInfo.possible)
+ return {
+ "type": "none",
+ "cursor": "action-garrison-disabled",
+ "target": null
+ };
+
+ return {
+ "type": "occupy-turret",
+ "cursor": "action-garrison",
+ "tooltip": actionInfo.tooltip,
+ "target": target
+ };
+ },
+ "hotkeyActionCheck": function(target, selection)
+ {
+ let actionInfo = getActionInfo("occupy-turret", target, selection);
+ if (!Engine.HotkeyIsPressed("session.garrison") || !actionInfo.possible)
+ return false;
+
+ return {
+ "type": "occupy-turret",
+ "cursor": "action-garrison",
+ "tooltip": actionInfo.tooltip,
+ "target": target
+ };
+ },
+ "specificness": 20,
+ },
+
"garrison":
{
"execute": function(target, action, selection, queued)
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
@@ -529,17 +529,6 @@
{
this.Eject(msg.entity, true, true);
this.Garrison(msg.newentity, true);
-
- // TurretHolder is not subscribed to GarrisonChanged, so we must inform it explicitly.
- // Otherwise a renaming entity may re-occupy another turret instead of its previous one,
- // since the message does not know what turret point whas used, which is not wanted.
- // Also ensure the TurretHolder receives the message after we process it.
- // If it processes it before us we garrison a turret and subsequently
- // are hidden by GarrisonHolder again.
- // This could be fixed by not requiring a turret to be 'garrisoned'.
- let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
- if (cmpTurretHolder)
- cmpTurretHolder.SwapEntities(msg.entity, msg.newentity);
}
if (!this.initGarrison)
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
@@ -1,8 +1,6 @@
/**
* This class holds the functions regarding entities being visible on
* another entity, but tied to their parents location.
- * Currently renaming and changing ownership are still managed by GarrisonHolder.js,
- * but in the future these components should be independent.
*/
class TurretHolder
{
@@ -87,6 +85,7 @@
return false;
turretPoint.entity = entity;
+
// Angle of turrets:
// Renamed entities (turretPoint != undefined) should keep their angle.
// Otherwise if an angle is given in the turretPoint, use it.
@@ -138,7 +137,7 @@
*
* @return {boolean} - Whether the entity was occupying a/the turret before.
*/
- LeaveTurret(entity, requestedTurretPoint)
+ LeaveTurret(entity, forced, requestedTurretPoint)
{
let turretPoint;
if (requestedTurretPoint)
@@ -152,6 +151,31 @@
if (!turretPoint)
return false;
+ // Find spawning location (copied from GarrisonHolder).
+ let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
+ let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
+ let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
+
+ // If the TurretHolder is a sinking ship, restrict the location to the intersection of both passabilities
+ // TODO: should use passability classes to be more generic.
+ let pos;
+ if ((cmpHealth && cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
+ pos = cmpFootprint.PickSpawnPointBothPass(entity);
+ else
+ pos = cmpFootprint.PickSpawnPoint(entity);
+
+ if (pos.y < 0)
+ {
+ if (!forced)
+ return false;
+
+ // If ejection is forced, we need to continue, so use center of the TurretHolder.
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ pos = cmpPosition.GetPosition();
+ }
+
+ turretPoint.entity = null;
+
let cmpPositionEntity = Engine.QueryInterface(entity, IID_Position);
cmpPositionEntity.SetTurretParent(INVALID_ENTITY, new Vector3D());
@@ -163,7 +187,16 @@
if (cmpUnitAIEntity)
cmpUnitAIEntity.ResetTurretStance();
- turretPoint.entity = null;
+ let cmpEntPosition = Engine.QueryInterface(entity, IID_Position);
+ if (cmpEntPosition)
+ {
+ cmpEntPosition.JumpTo(pos.x, pos.z);
+ cmpEntPosition.SetHeightOffset(0);
+
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (cmpPosition)
+ cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
+ }
// Reset the obstruction flags to template defaults.
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
@@ -221,6 +254,34 @@
}
/**
+ * @return {boolean} - Whether all the turret points are occupied.
+ */
+ IsFull()
+ {
+ return !!this.turretPoints.find(turretPoint => turretPoint.entity == null);
+ }
+
+ /**
+ * @return {Object} - Max and min ranges at which entities can occupy any turret.
+ */
+ GetLoadingRange()
+ {
+ return { "max": +(this.template.LoadingRange || 2), "min": 0 };
+ }
+
+ /**
+ * @param {number} ent - The entity ID of the turret to be potentially picked up.
+ * @return {boolean} - Whether this entity can pick the specified entity up.
+ */
+ CanPickup(ent)
+ {
+ if (!this.template.Pickup || this.IsFull())
+ return false;
+ let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
+ return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
+ }
+
+ /**
* Sets an init turret, present from game start. (E.g. set in Atlas.)
* @param {String} turretName - The name of the turret point to be used.
* @param {number} entity - The entity-ID to be placed.
@@ -238,12 +299,6 @@
}
/**
- * 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.
- * That means an entity with TurretHolder should not need a GarrisonHolder
- * for e.g. the garrisoning logic.
- *
* @param {number} from - The entity to substitute.
* @param {number} to - The entity to subtitute with.
*/
@@ -251,31 +306,59 @@
{
let turretPoint = this.GetOccupiedTurret(from);
if (turretPoint)
- this.LeaveTurret(from, turretPoint);
-
- let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
- if (cmpGarrisonHolder && cmpGarrisonHolder.IsGarrisoned(to))
+ {
+ this.LeaveTurret(from, true, turretPoint);
this.OccupyTurret(to, turretPoint);
+ }
+ }
+
+ /**
+ * Update list of turreted entities if one gets renamed (e.g. by promotion).
+ */
+ OnGlobalEntityRenamed(msg)
+ {
+ this.SwapEntities(msg.entity, msg.newentity);
+
+ if (!this.initTurrets)
+ return;
+
+ // Update the pre-game turrets because of SkirmishReplacement.
+ if (msg.entity == this.entity)
+ {
+ let cmpTurretHolder = Engine.QueryInterface(msg.newentity, IID_TurretHolder);
+ if (cmpTurretHolder)
+ cmpTurretHolder.initTurrets = this.initTurrets;
+ }
+ else
+ {
+ if (this.initTurrets.has(msg.entity))
+ {
+ this.initTurrets.set(msg.newentity, this.initTurrets.get(msg.entity));
+ this.initTurrets.delete(msg.entity);
+ }
+ }
}
- OnGarrisonedUnitsChanged(msg)
+ /**
+ * Update list of turreted entities if ownership changes.
+ */
+ OnGlobalOwnershipChanged(msg)
{
- // Ignore renaming for that is handled seperately
- // (i.e. called directly from GarrisonHolder.js).
- if (msg.renamed)
+ if (msg.entity == this.entity)
+ {
+ for (let entity of this.GetEntities())
+ if (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, entity))
+ this.LeaveTurret(entity, true);
return;
+ }
- for (let entity of msg.removed)
- this.LeaveTurret(entity);
- for (let entity of msg.added)
- this.OccupyTurret(entity);
+ if (this.OccupiesTurret(msg.entity) &&
+ (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity)))
+ this.LeaveTurret(msg.entity);
}
/**
* Initialise the turreted units.
- * Really ugly, but because GarrisonHolder is processed earlier, and also turrets
- * entities on init, we can find an entity that already is present.
- * In that case we reject and occupy.
*/
OnGlobalInitGame(msg)
{
@@ -283,13 +366,9 @@
return;
for (let [turretPointName, entity] of this.initTurrets)
- {
- if (this.OccupiesTurret(entity))
- this.LeaveTurret(entity);
if (!this.OccupyNamedTurret(entity, turretPointName))
warn("Entity " + entity + " could not occupy the turret point " +
turretPointName + " of turret holder " + this.entity + ".");
- }
delete this.initTurrets;
}
@@ -326,6 +405,16 @@
"" +
"" +
"" +
- "";
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ ""
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
Engine.RegisterComponentType(IID_TurretHolder, "TurretHolder", TurretHolder);
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
@@ -145,7 +145,8 @@
"WalkAndFight",
"WalkToTarget",
"Patrol",
- "Garrison"
+ "Garrison",
+ "OccupyTurret"
]);
// When leaving a foundation, we want to be clear of it by this distance.
@@ -643,6 +644,37 @@
this.isGarrisoned = false;
},
+ "Order.OccupyTurret": function(msg) {
+ if (!this.AbleToMove())
+ {
+ this.SetNextState("IDLE");
+ return;
+ }
+ else if (this.IsTurret())
+ {
+ if (this.IsAnimal())
+ this.SetNextState("ANIMAL.TURRET.OCCUPYING");
+ else
+ this.SetNextState("INDIVIDUAL.TURRET.OCCUPYING");
+ return;
+ }
+
+ if (this.CanPack())
+ {
+ this.PushOrderFront("Pack", { "force": true });
+ return;
+ }
+
+ if (this.IsAnimal())
+ this.SetNextState("ANIMAL.TURRET.APPROACHING");
+ else
+ this.SetNextState("INDIVIDUAL.TURRET.APPROACHING");
+ },
+
+ "Order.LeaveTurret": function() {
+ this.FinishOrder();
+ },
+
"Order.Cheer": function(msg) {
return { "discardOrder": true };
},
@@ -782,6 +814,24 @@
this.SetNextState("GARRISON.GARRISONING");
},
+ "Order.OccupyTurret": function(msg) {
+ if (!Engine.QueryInterface(msg.data.target, IID_TurretHolder))
+ {
+ this.FinishOrder();
+ return;
+ }
+ if (!this.CheckOccupyTurretRange(msg.data.target))
+ {
+ if (!this.CheckTargetVisible(msg.data.target))
+ this.FinishOrder();
+ else
+ this.SetNextState("TURRET.APPROACHING");
+ return;
+ }
+
+ this.SetNextState("TURRET.OCCUPYING");
+ },
+
"Order.Gather": function(msg) {
if (this.MustKillGatherTarget(msg.data.target))
{
@@ -1071,6 +1121,51 @@
},
},
+ "TURRET":{
+ "APPROACHING": {
+ "enter": function() {
+ if (!this.MoveToOccupyTurretRange(this.order.data.target))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
+ cmpFormation.SetRearrange(true);
+ cmpFormation.MoveMembersIntoFormation(true, true);
+
+ let cmpTurretHolder = Engine.QueryInterface(this.order.data.target, IID_TurretHolder);
+ if (cmpTurretHolder && cmpTurretHolder.CanPickup(this.entity))
+ {
+ this.pickup = this.order.data.target; // temporary, deleted in "leave"
+ Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
+ }
+ return false;
+ },
+
+ "leave": function() {
+ this.StopMoving();
+ if (this.pickup)
+ {
+ Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
+ delete this.pickup;
+ }
+ },
+
+ "MovementUpdate": function(msg) {
+ if (msg.likelyFailure || msg.likelySuccess)
+ this.SetNextState("OCCUPYING");
+ },
+ },
+
+ "OCCUPYING": {
+ "enter": function() {
+ this.CallMemberFunction("OccupyTurret", [this.order.data.target, false]);
+ this.SetNextState("MEMBER");
+ return true;
+ },
+ },
+ },
+
"FORMING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
@@ -3019,12 +3114,6 @@
delete this.pickup;
}
- if (this.IsTurret())
- {
- this.SetNextState("IDLE");
- return true;
- }
-
return false;
}
}
@@ -3056,6 +3145,106 @@
},
},
+ "TURRET": {
+ "APPROACHING": {
+ "enter": function() {
+ if (!this.CanOccupyTurret(this.order.data.target))
+ {
+ this.FinishOrder();
+ return true;
+ }
+
+ if (!this.MoveToOccupyTurretRange(this.order.data.target))
+ {
+ this.FinishOrder();
+ return true;
+ }
+
+ if (this.pickup)
+ Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
+
+ let cmpTurretHolder = Engine.QueryInterface(this.order.data.target, IID_TurretHolder);
+ if (cmpTurretHolder && cmpTurretHolder.CanPickup(this.entity))
+ {
+ this.pickup = this.order.data.target; // temporary, deleted in "leave"
+ Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
+ }
+ return false;
+ },
+
+ "leave": function() {
+ if (this.pickup)
+ {
+ Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
+ delete this.pickup;
+ }
+ this.StopMoving();
+ },
+
+ "MovementUpdate": function(msg) {
+ if (!msg.likelyFailure && !msg.likelySuccess)
+ return;
+
+ if (this.CheckOccupyTurretRange(this.order.data.target))
+ this.SetNextState("OCCUPYING");
+ else
+ {
+ // Unable to reach the target, try again (or follow if it is a moving target)
+ // except if the target does not exist anymore or its orders have changed.
+ if (this.pickup)
+ {
+ let cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
+ if (!cmpUnitAI || (!cmpUnitAI.HasPickupOrder(this.entity) && !cmpUnitAI.IsIdle()))
+ this.FinishOrder();
+
+ }
+ }
+ },
+ },
+
+ "OCCUPYING": {
+ "enter": function() {
+ let target = this.order.data.target;
+
+ let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
+ if (!cmpTurretHolder || !cmpTurretHolder.OccupyTurret(this.entity))
+ {
+ this.FinishOrder();
+ return true;
+ }
+ this.SetImmobile(true);
+
+ if (this.formationController)
+ {
+ let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
+ if (cmpFormation)
+ {
+ let rearrange = cmpFormation.rearrange;
+ cmpFormation.SetRearrange(false);
+ cmpFormation.RemoveMembers([this.entity]);
+ cmpFormation.SetRearrange(rearrange);
+ }
+ }
+
+ let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
+ if (this.CanReturnResource(target, true, cmpResourceGatherer))
+ {
+ cmpResourceGatherer.CommitResources(target);
+ this.SetDefaultAnimationVariant();
+ }
+
+ this.SetNextState("TURRETTED");
+ return true;
+ },
+ },
+
+ "TURRETTED": "INDIVIDUAL.IDLE",
+
+ "leave": function() {
+ this.SetImmobile(false);
+ },
+ },
+
"CHEERING": {
"enter": function() {
this.SelectAnimation("promotion");
@@ -4618,6 +4807,20 @@
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
+UnitAI.prototype.MoveToOccupyTurretRange = function(target)
+{
+ if (!this.CheckTargetVisible(target))
+ return false;
+
+ let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
+ if (!cmpTurretHolder)
+ return false;
+ let range = cmpTurretHolder.GetLoadingRange();
+
+ let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
+};
+
/**
* Generic dispatcher for other Check...Range functions.
* @param iid - Interface ID (optional) implementing GetRange
@@ -4743,6 +4946,16 @@
return this.CheckTargetRangeExplicit(target, range.min, range.max);
};
+UnitAI.prototype.CheckOccupyTurretRange = function(target)
+{
+ let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
+ if (!cmpTurretHolder)
+ return false;
+
+ let range = cmpTurretHolder.GetLoadingRange();
+ return this.CheckTargetRangeExplicit(target, range.min, range.max);
+};
+
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
@@ -5398,6 +5611,38 @@
};
/**
+ * Adds garrison order to the queue, forced by the player.
+ */
+UnitAI.prototype.OccupyTurret = function(target, queued)
+{
+ if (target == this.entity)
+ return;
+ if (!this.CanOccupyTurret(target))
+ {
+ this.WalkToTarget(target, queued);
+ return;
+ }
+ this.AddOrder("OccupyTurret", { "target": target, "force": true }, queued);
+};
+
+/**
+ * Adds order to leave turret to the queue.
+ */
+UnitAI.prototype.LeaveTurret = function()
+{
+ if (this.IsTurret())
+ this.AddOrder("LeaveTurret", null, false);
+};
+
+/**
+ * Adds a order to occupy a turret for units that are already turreted.
+ */
+UnitAI.prototype.Autogarrison = function(target)
+{
+ this.PushOrderFront("OccupyTurret", { "target": target });
+};
+
+/**
* Adds gather order to the queue, forced by the player
* until the target is reached
*/
@@ -6197,6 +6442,24 @@
return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
+UnitAI.prototype.CanOccupyTurret = function(target)
+{
+ // Formation controllers should always respond to commands
+ // (then the individual units can make up their own minds).
+ if (this.IsFormationController())
+ return true;
+
+ let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
+ if (!cmpTurretHolder)
+ return false;
+
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
+ return false;
+
+ return true;
+};
+
UnitAI.prototype.CanPack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
Index: binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- binaries/data/mods/public/simulation/helpers/Commands.js
+++ binaries/data/mods/public/simulation/helpers/Commands.js
@@ -442,6 +442,20 @@
data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned."));
},
+ "occupy-turret": function(player, cmd, data)
+ {
+ if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
+ {
+ if (g_DebugCommands)
+ warn("Invalid command: turret target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
+ return;
+ }
+
+ GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
+ cmpUnitAI.OccupyTurret(cmd.target, cmd.queued);
+ });
+ },
+
"garrison": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
Index: binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml
===================================================================
--- binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml
+++ binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml
@@ -20,14 +20,6 @@
13.0
-
- 1
- 0.1
- Unit
- Infantry
- 0
- 2
-
400
decay|rubble/rubble_stone_2x2