Move the controllability of an entity to cmpIdentity.

Where controllability is defined as the ability to receive _any_ order
from the player.
Fixes the concern not raised at a1dc9cadd8#42637.

Differential Revision: D1960
Reviewed By: @wraitii
Comments by: @bb, @Stan.
This was SVN commit r24148.
This commit is contained in:
Freagarach 2020-11-09 08:38:09 +00:00
parent 41570a2dc1
commit b97d251322
7 changed files with 136 additions and 57 deletions

View File

@ -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);

View File

@ -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)
{

View File

@ -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);

View File

@ -90,6 +90,11 @@ Identity.prototype.Schema =
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Controllable' a:help='Whether players can control this entity. Defaults to true.'>" +
"<data type='boolean'/>" +
"</element>" +
"</optional>" +
"<element name='Undeletable' a:help='Prevent players from deleting this entity.'>" +
"<data type='boolean'/>" +
"</element>";
@ -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);

View File

@ -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);

View File

@ -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,

View File

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