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));
}
/**