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);
@@ -238,12 +271,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 +278,42 @@
{
let turretPoint = this.GetOccupiedTurret(from);
if (turretPoint)
- this.LeaveTurret(from, turretPoint);
+ this.LeaveTurret(from, true, turretPoint);
- let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
- if (cmpGarrisonHolder && cmpGarrisonHolder.IsGarrisoned(to))
- this.OccupyTurret(to, turretPoint);
+ this.OccupyTurret(to, turretPoint);
}
- OnGarrisonedUnitsChanged(msg)
+ /**
+ * Update list of turreted entities if one gets renamed (e.g. by promotion).
+ */
+ OnGlobalEntityRenamed(msg)
{
- // Ignore renaming for that is handled seperately
- // (i.e. called directly from GarrisonHolder.js).
- if (msg.renamed)
+ let turretPoint = this.OccupiesTurret(msg.entity);
+ if (turretPoint)
+ this.SwapEntities(msg.entity, msg.newentity);
+
+ if (!this.initTurrets)
return;
- for (let entity of msg.removed)
- this.LeaveTurret(entity);
- for (let entity of msg.added)
- this.OccupyTurret(entity);
+ // 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);
+ }
+ }
}
/**
* 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 +321,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;
}
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