diff --git a/binaries/data/mods/public/gui/session/unit_actions.js b/binaries/data/mods/public/gui/session/unit_actions.js
index 6c839cb1ab..66e6f980ef 100644
--- a/binaries/data/mods/public/gui/session/unit_actions.js
+++ b/binaries/data/mods/public/gui/session/unit_actions.js
@@ -1105,6 +1105,33 @@ var g_UnitActions =
"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 findGatherType(gatherer, supply)
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 @@ function getActionInfo(action, target, selection)
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 @@ function getActionInfo(action, target, selection)
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);
diff --git a/binaries/data/mods/public/gui/session/unit_commands.js b/binaries/data/mods/public/gui/session/unit_commands.js
index e2ea1fbfc5..bfdba1a0ef 100644
--- a/binaries/data/mods/public/gui/session/unit_commands.js
+++ b/binaries/data/mods/public/gui/session/unit_commands.js
@@ -135,7 +135,13 @@ function updateUnitCommands(entStates, supplementalDetailsPanel, commandsPanel)
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)
{
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index a96e6e6325..aa1e5219f0 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -261,7 +261,8 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"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);
diff --git a/binaries/data/mods/public/simulation/components/Identity.js b/binaries/data/mods/public/simulation/components/Identity.js
index 485b4c269f..72d9a36368 100644
--- a/binaries/data/mods/public/simulation/components/Identity.js
+++ b/binaries/data/mods/public/simulation/components/Identity.js
@@ -90,6 +90,11 @@ Identity.prototype.Schema =
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"";
@@ -102,6 +107,8 @@ Identity.prototype.Init = function()
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 @@ Identity.prototype.IsUndeletable = function()
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);
diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index 36530b5cb6..9288a7bf7c 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -214,8 +214,7 @@ UnitAI.prototype.UnitFsmSpec = {
// 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 @@ UnitAI.prototype.UnitFsmSpec = {
// (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 @@ UnitAI.prototype.UnitFsmSpec = {
},
"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 @@ UnitAI.prototype.UnitFsmSpec = {
},
"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 @@ UnitAI.prototype.UnitFsmSpec = {
"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 @@ UnitAI.prototype.IsDangerousAnimal = function()
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);
diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
index 5726b7670e..b2b7680b3d 100644
--- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
@@ -548,6 +548,7 @@ AddMock(10, IID_Identity, {
"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 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
"canDelete": true,
"hasSomeFormation": false,
"formations": [],
+ "controllable": true,
},
"position": { "x": 1, "y": 2, "z": 3 },
"hitpoints": 50,
diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js
index d810df58c9..274d956331 100644
--- a/binaries/data/mods/public/simulation/helpers/Commands.js
+++ b/binaries/data/mods/public/simulation/helpers/Commands.js
@@ -340,13 +340,6 @@ var g_Commands = {
"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 @@ var g_Commands = {
"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 @@ var g_Commands = {
"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 @@ var g_Commands = {
"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 @@ var g_Commands = {
"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));
@@ -875,6 +858,27 @@ function notifyBackToWorkFailure(player)
});
}
+/**
+ * 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 @@ function CanMoveEntsIntoFormation(ents, formationTemplate)
/**
* 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 CanControlUnitOrIsAlly(entity, player, controlAll)
+function CanPlayerOrAllyControlUnit(entity, player, controlAll)
{
- return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity) || controlAll;
+ return CanControlUnit(player, entity, controlAll) ||
+ IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity);
}
/**
- * Filter entities which the player can control
+ * @return {boolean} - Whether the owner of this entity can control the entity.
+ */
+function CanOwnerControlEntity(entity)
+{
+ let cmpOwner = QueryOwnerInterface(entity);
+ return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID());
+}
+
+/**
+ * Filter entities which the player can control.
*/
function FilterEntityList(entities, player, controlAll)
{
@@ -1693,7 +1725,7 @@ function FilterEntityList(entities, player, controlAll)
*/
function FilterEntityListWithAllies(entities, player, controlAll)
{
- return entities.filter(ent => CanControlUnitOrIsAlly(ent, player, controlAll));
+ return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll));
}
/**