Index: ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js +++ ps/trunk/binaries/data/mods/public/gui/session/unit_actions.js @@ -1105,6 +1105,33 @@ "specificness": 11, }, + // This is a "fake" action to show a failure cursor + // when only uncontrollable entities are selected. + "uncontrollable": + { + "execute": function(target, action, selection, queued) + { + return true; + }, + "actionCheck": function(target, selection) + { + // Only show this action if all entities are marked uncontrollable. + let playerState = g_SimState.players[g_ViewedPlayer]; + if (playerState && playerState.controlsAll || selection.some(ent => { + let entState = GetEntityState(ent); + return entState && entState.identity && entState.identity.controllable; + })) + return false; + + return { + "type": "none", + "cursor": "cursor-no", + "tooltip": translatePlural("This entity cannot be controlled.", "These entities cannot be controlled.", selection.length) + }; + }, + "specificness": 100, + }, + "none": { "execute": function(target, action, selection, queued) @@ -1600,14 +1627,19 @@ function getActionInfo(action, target, selection) { - let simState = GetSimState(); - - // If the selection doesn't exist, no action - if (!GetEntityState(selection[0])) + if (!selection || !selection.length || !GetEntityState(selection[0])) return { "possible": false }; if (!target) // TODO move these non-target actions to an object like unit_actions.js { + // Ensure one entity at least is controllable. + let playerState = g_SimState.players[g_ViewedPlayer]; + if (playerState && !playerState.controlsAll && !selection.some(ent => { + let entState = GetEntityState(ent); + return entState && entState.identity && entState.identity.controllable; + })) + return { "possible": false }; + if (action == "set-rallypoint") { let cursor = ""; @@ -1644,6 +1676,9 @@ if (!targetState) return { "possible": false }; + let simState = GetSimState(); + let playerState = g_SimState.players[g_ViewedPlayer]; + // Check if any entities in the selection can do some of the available actions with target for (let entityID of selection) { @@ -1651,6 +1686,9 @@ if (!entState) continue; + if (playerState && !playerState.controlsAll && !entState.identity.controllable) + continue; + if (g_UnitActions[action] && g_UnitActions[action].getActionInfo) { let r = g_UnitActions[action].getActionInfo(entState, targetState, simState); Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js +++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js @@ -135,7 +135,13 @@ let playerStates = GetSimState().players; let playerState = playerStates[Engine.GetPlayerID()]; - if (g_IsObserver || entStates.every(entState => controlsPlayer(entState.player))) + // Always show selection. + setupUnitPanel("Selection", entStates, playerStates[entStates[0].player]); + + if (g_IsObserver || entStates.every(entState => + controlsPlayer(entState.player) && + (!entState.identity || entState.identity.controllable)) || + playerState.controlsAll) { for (let guiName of g_PanelsOrder) { Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -261,7 +261,8 @@ "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "hasSomeFormation": cmpIdentity.HasSomeFormation(), - "formations": cmpIdentity.GetFormationsList() + "formations": cmpIdentity.GetFormationsList(), + "controllable": cmpIdentity.IsControllable() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); Index: ps/trunk/binaries/data/mods/public/simulation/components/Identity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js @@ -90,6 +90,11 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + ""; @@ -102,6 +107,8 @@ this.phenotype = pickRandom(this.GetPossiblePhenotypes()); else this.phenotype = "default"; + + this.controllable = this.template.Controllable ? this.template.Controllable == "true" : true; }; Identity.prototype.HasSomeFormation = function() @@ -184,4 +191,14 @@ return this.template.Undeletable == "true"; }; +Identity.prototype.IsControllable = function() +{ + return this.controllable; +}; + +Identity.prototype.SetControllable = function(controllability) +{ + this.controllable = controllability; +}; + Engine.RegisterComponentType(IID_Identity, "Identity", Identity); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -214,8 +214,7 @@ // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { - // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) + if (!this.AbleToMove()) { this.FinishOrder(); return; @@ -247,13 +246,6 @@ // (these will switch the unit out of formation mode) "Order.Stop": function(msg) { - // We have no control over non-domestic animals. - if (this.IsAnimal() && !this.IsDomestic()) - { - this.FinishOrder(); - return; - } - this.StopMoving(); this.FinishOrder(); @@ -267,8 +259,7 @@ }, "Order.Walk": function(msg) { - // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) + if (!this.AbleToMove()) { this.FinishOrder(); return; @@ -289,8 +280,7 @@ }, "Order.WalkAndFight": function(msg) { - // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) + if (!this.AbleToMove()) { this.FinishOrder(); return; @@ -312,8 +302,7 @@ "Order.WalkToTarget": function(msg) { - // Let players move captured domestic animals around - if (this.IsAnimal() && !this.IsDomestic() || !this.AbleToMove()) + if (!this.AbleToMove()) { this.FinishOrder(); return; @@ -3341,12 +3330,6 @@ this.template.NaturalBehaviour == "aggressive")); }; -UnitAI.prototype.IsDomestic = function() -{ - var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - return cmpIdentity && cmpIdentity.HasClass("Domestic"); -}; - UnitAI.prototype.IsHealer = function() { return Engine.QueryInterface(this.entity, IID_Heal); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -548,6 +548,7 @@ "GetSelectionGroupName": function() { return "Selection Group Name"; }, "HasClass": function() { return true; }, "IsUndeletable": function() { return false; }, + "IsControllable": function() { return true; }, "HasSomeFormation": function() { return false; }, "GetFormationsList": function() { return []; }, }); @@ -581,6 +582,7 @@ "canDelete": true, "hasSomeFormation": false, "formations": [], + "controllable": true, }, "position": { "x": 1, "y": 2, "z": 3 }, "hitpoints": 50, Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js @@ -340,13 +340,6 @@ "research": function(player, cmd, data) { - if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) - { - if (g_DebugCommands) - warn("Invalid command: research building cannot be controlled by player "+player+": "+uneval(cmd)); - return; - } - var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template)) { @@ -362,13 +355,6 @@ "stop-production": function(player, cmd, data) { - if (!CanControlUnit(cmd.entity, player, data.controlAllUnits)) - { - if (g_DebugCommands) - warn("Invalid command: production building cannot be controlled by player "+player+": "+uneval(cmd)); - return; - } - var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.RemoveBatch(cmd.id); @@ -458,8 +444,7 @@ "garrison": function(player, cmd, data) { - // Verify that the building can be controlled by the player or is mutualAlly - if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) + if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); @@ -473,11 +458,10 @@ "guard": function(player, cmd, data) { - // Verify that the target can be controlled by the player or is mutualAlly - if (!CanControlUnitOrIsAlly(cmd.target, player, data.controlAllUnits)) + if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) - warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd)); + warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd)); return; } @@ -495,8 +479,7 @@ "unload": function(player, cmd, data) { - // Verify that the building can be controlled by the player or is mutualAlly - if (!CanControlUnitOrIsAlly(cmd.garrisonHolder, player, data.controlAllUnits)) + if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); @@ -876,6 +859,27 @@ } /** + * Sends a GUI notification about entities that can't be controlled. + * @param {number} player - The player-ID of the player that needs to receive this message. + */ +function notifyOrderFailure(entity, player) +{ + let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); + if (!cmpIdentity) + return; + + let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGUIInterface.PushNotification({ + "type": "text", + "players": [player], + "message": sprintf(markForTranslation("%(unit)s can't be controlled."), { + "unit": cmpIdentity.GetGenericName() + }), + "translateMessage": true + }); +} + +/** * Get some information about the formations used by entities. * The entities must have a UnitAI component. */ @@ -1661,27 +1665,55 @@ /** * Check if player can control this entity - * returns: true if the entity is valid and owned by the player + * returns: true if the entity is owned by the player and controllable * or control all units is activated, else false */ function CanControlUnit(entity, player, controlAll) { - return IsOwnedByPlayer(player, entity) || controlAll; + let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); + let canBeControlled = IsOwnedByPlayer(player, entity) && + (!cmpIdentity || cmpIdentity.IsControllable()) || + controlAll; + + if (!canBeControlled) + notifyOrderFailure(entity, player); + + return canBeControlled; +} + +/** + * @param {number} entity - The entityID to verify. + * @param {number} player - The playerID to check against. + * @return {boolean}. + */ +function IsOwnedByPlayerOrMutualAlly(entity, player) +{ + return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity); } /** * Check if player can control this entity - * returns: true if the entity is valid and owned by the player - * or the entity is owned by an mutualAlly - * or control all units is activated, else false + * @return {boolean} - True if the entity is valid and controlled by the player + * or the entity is owned by an mutualAlly and can be controlled + * or control all units is activated, else false. + */ +function CanPlayerOrAllyControlUnit(entity, player, controlAll) +{ + return CanControlUnit(player, entity, controlAll) || + IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity); +} + +/** + * @return {boolean} - Whether the owner of this entity can control the entity. */ -function CanControlUnitOrIsAlly(entity, player, controlAll) +function CanOwnerControlEntity(entity) { - return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll; + let cmpOwner = QueryOwnerInterface(entity); + return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID()); } /** - * Filter entities which the player can control + * Filter entities which the player can control. */ function FilterEntityList(entities, player, controlAll) { @@ -1693,7 +1725,7 @@ */ function FilterEntityListWithAllies(entities, player, controlAll) { - return entities.filter(ent => CanControlUnitOrIsAlly(ent, player, controlAll)); + return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll)); } /**