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