Allow removing player entities when starting a match.

This commit is contained in:
Stan 2025-05-04 17:52:14 +02:00
parent e4e80a2504
commit b36782388b
No known key found for this signature in database
GPG key ID: 244943DFF8370D60
8 changed files with 180 additions and 28 deletions

View file

@ -18,6 +18,7 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
this.clientItemFactory = new PlayerAssignmentItem.Client();
this.aiItemFactory = new PlayerAssignmentItem.AI();
this.unassignedItem = new PlayerAssignmentItem.Unassigned().createItem();
this.removedItem = new PlayerAssignmentItem.Removed().createItem();
this.aiItems =
g_Settings.AIDescriptions.filter(ai => !ai.data.hidden).map(
@ -31,12 +32,18 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
this.rebuildList();
const savedAI = this.isSavedGame && g_GameSettings.playerAI.get(this.playerIndex);
const savedRemoved = this.isSavedGame && g_GameSettings.playerRemoved.get(this.playerIndex);
if (savedAI)
{
this.setSelectedValue(savedAI.bot);
this.setEnabled(false);
}
else if (savedRemoved)
{
this.setSelectedValue(this.removedItem.Value);
this.setEnabled(false);
}
else
this.rebuildList();
@ -54,7 +61,8 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
{
const isPlayerSlot = Object.values(g_PlayerAssignments).some(x => x.player === this.playerIndex + 1);
if (!isPlayerSlot && !g_GameSettings.playerAI.get(this.playerIndex) &&
this.playerIndex >= oldNb && this.playerIndex < g_GameSettings.playerCount.nbPlayers)
!g_GameSettings.playerRemoved.get(this.playerIndex) &&
this.playerIndex >= oldNb && this.playerIndex < g_GameSettings.playerCount.nbPlayers)
{
// Add AIs to unused slots by default.
// TODO: we could save the settings in case the player lowers, then re-raises the # of players.
@ -104,6 +112,14 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
this.setSelectedValue(ai.bot);
return;
}
const isRemoved = g_GameSettings.playerRemoved.get(this.playerIndex);
if (isRemoved)
{
this.rebuildList();
this.setSelectedValue(this.removedItem.Value)
return;
}
this.setSelectedValue(undefined);
}
@ -118,10 +134,12 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
// If loading a saved game clients and unassigned players can't be replaced by a AI. Don't show
// the AIs in the dropdown.
const disableAI = this.isSavedGame && !g_GameSettings.playerAI.get(this.playerIndex);
const disabledPlayer = this.isSavedGame && !g_GameSettings.playerRemoved.get(this.playerIndex);
this.values = prepareForDropdown([
...this.playerItems,
...disableAI ? [] : this.aiItems,
this.unassignedItem
...disableAI || disabledPlayer ? [] : this.aiItems,
this.unassignedItem,
this.removedItem
]);
const selected = this.dropdown.list_data?.[this.dropdown.selected];
@ -173,6 +191,7 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
guidToAssign, isSavedGame)
{
const sourcePlayer = g_PlayerAssignments[guidToAssign].player - 1;
g_GameSettings.playerRemoved.set(playerIndex, false);
if (sourcePlayer >= 0)
{
const ai = g_GameSettings.playerAI.get(playerIndex);
@ -186,15 +205,9 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
g_GameSettings.playerColor.swap(sourcePlayer, playerIndex);
}
}
playerAssignmentsController.assignPlayer(guidToAssign, playerIndex);
gameSettingsController.setNetworkInitAttributes();
}
isSelected(pData, guid, value)
{
return guid !== undefined && guid == value;
}
};
PlayerAssignmentItem.Client.prototype.PlayerTags =
@ -227,14 +240,9 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
"difficulty": +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"),
"behavior": Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"),
});
g_GameSettings.playerRemoved.set(playerIndex, false);
gameSettingsController.setNetworkInitAttributes();
}
isSelected(pData, guid, value)
{
return !guid && pData.AI && pData.AI == value;
}
};
PlayerAssignmentItem.AI.prototype.Label =
@ -262,14 +270,10 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
playerAssignmentsController.unassignClient(playerIndex + 1);
g_GameSettings.playerAI.setAI(playerIndex, undefined);
g_GameSettings.playerRemoved.set(playerIndex, false);
gameSettingsController.setNetworkInitAttributes();
}
isSelected(pData, guid, value)
{
return !guid && !pData.AI;
}
};
PlayerAssignmentItem.Unassigned.prototype.Label =
@ -278,3 +282,34 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
PlayerAssignmentItem.Unassigned.prototype.Tags =
{ "color": "140 140 140" };
}
{
PlayerAssignmentItem.Removed = class
{
createItem()
{
return {
"Handler": this,
"Value": "removed",
"Autocomplete": this.Label,
"Caption": setStringTags(this.Label, this.Tags)
};
}
onSelectionChange(gameSettingsController, playerAssignmentsController, playerIndex)
{
g_GameSettings.playerRemoved.set(playerIndex, true);
playerAssignmentsController.unassignClient(playerIndex + 1);
g_GameSettings.playerAI.setAI(playerIndex, undefined);
gameSettingsController.setNetworkInitAttributes();
}
};
PlayerAssignmentItem.Removed.prototype.Label =
translate("Removed");
PlayerAssignmentItem.Removed.prototype.Tags =
{ "color": "255 140 140" };
}

View file

@ -0,0 +1,68 @@
GameSettings.prototype.Attributes.PlayerRemoved = class PlayerRemoved extends GameSetting
{
init()
{
// NB: watchers aren't auto-triggered when modifying array elements.
this.values = [];
this.settings.playerCount.watch(() => this.maybeUpdate(), ["nbPlayers"]);
}
toInitAttributes(attribs)
{
if (!attribs.settings.PlayerData)
attribs.settings.PlayerData = [];
while (attribs.settings.PlayerData.length < this.values.length)
attribs.settings.PlayerData.push({});
for (let i = 0; i < this.values.length; ++i)
attribs.settings.PlayerData[i].Removed = this.values[i] ?? false;
}
fromInitAttributes(attribs)
{
if (!this.getLegacySetting(attribs, "PlayerData"))
return;
const pData = this.getLegacySetting(attribs, "PlayerData");
for (let i = 0; i < this.values.length; ++i)
{
if (!pData[i])
{
this.set(+i, false);
continue;
}
this.set(+i, pData[i].Removed);
}
}
_resize(nb)
{
while (this.values.length > nb)
this.values.pop();
while (this.values.length < nb)
this.values.push(false);
}
maybeUpdate()
{
if (this.values.length === this.settings.playerCount.nbPlayers)
return;
this._resize(this.settings.playerCount.nbPlayers);
this.trigger("values");
}
swap(sourceIndex, targetIndex)
{
[this.values[sourceIndex], this.values[targetIndex]] = [this.values[targetIndex], this.values[sourceIndex]];
this.trigger("values");
}
set(playerIndex, removed)
{
this.values[playerIndex] = removed;
this.trigger("values");
}
get(playerIndex)
{
return this.values[playerIndex] ?? false;
}
};

View file

@ -65,6 +65,7 @@ Player.prototype.Init = function()
this.startCam = undefined;
this.controlAllUnits = false;
this.isAI = false;
this.isRemoved = false;
this.cheatsEnabled = false;
this.panelEntities = [];
this.resourceNames = {};
@ -576,6 +577,16 @@ Player.prototype.IsAI = function()
return this.isAI;
};
Player.prototype.SetRemoved = function(flag)
{
this.isRemoved = flag;
};
Player.prototype.IsRemoved = function()
{
return this.isRemoved;
};
/**
* Do some map dependant initializations
*/
@ -760,6 +771,10 @@ Player.prototype.OnGlobalPlayerDefeated = function(msg)
if (!cmpSound)
return;
// Don't play defeat/win sounds for removed players.
if (this.playerID === msg.playerId && this.IsRemoved() || QueryPlayerIDInterface(msg.playerId)?.IsRemoved())
return;
const soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : Engine.QueryInterface(this.entity, IID_Diplomacy).IsAlly(msg.playerId) ? "defeated_ally" : this.HasWon() ? "won" : "defeated_enemy");
if (soundGroup)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, this.playerID);

View file

@ -47,10 +47,18 @@ function InitGame(settings)
const cmpPlayer = QueryPlayerIDInterface(i);
cmpPlayer.SetCheatsEnabled(!!settings.CheatsEnabled);
if (settings.PlayerData[i] && !!settings.PlayerData[i].AI)
if (settings.PlayerData[i])
{
cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, +settings.PlayerData[i].AIDiff, settings.PlayerData[i].AIBehavior || "random");
cmpPlayer.SetAI(true);
if(!!settings.PlayerData[i].Removed)
{
cmpPlayer.Defeat(undefined);
continue;
}
else if (!!settings.PlayerData[i].AI)
{
cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, +settings.PlayerData[i].AIDiff, settings.PlayerData[i].AIBehavior || "random");
cmpPlayer.SetAI(true);
}
}
if (settings.AllyView)

View file

@ -66,9 +66,11 @@ function LoadPlayerSettings(settings, newPlayers)
cmpPlayer.SetColor(color.r, color.g, color.b);
// Special case for gaia
if (i == 0)
if (i === 0)
continue;
cmpPlayer.SetRemoved(getPlayerSetting(i, "Removed"));
// StartingResources
if (settings.PlayerData[i].Resources !== undefined)
cmpPlayer.SetResourceCounts(settings.PlayerData[i].Resources);

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -1108,10 +1108,21 @@ int CXMLReader::ReadEntities(XMBElement parent, double end_time)
debug_warn(L"Invalid map XML data");
}
entity_id_t ent = sim.AddEntity(TemplateName, EntityUid);
entity_id_t player = cmpPlayerManager->GetPlayerByID(PlayerID);
CmpPtr<ICmpPlayer> cmpPlayer(sim, player);
// Don't add entities for removed players.
if (cmpPlayer && cmpPlayer->IsRemoved())
{
completed_jobs++;
LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
continue;
}
entity_id_t ent = sim.AddEntity(TemplateName, EntityUid);
if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
{ // Don't add entities with invalid player IDs
{
// Don't add entities with invalid player IDs
LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(TemplateName));
}
else
@ -1497,9 +1508,16 @@ int CMapReader::ParseEntities()
{
// Get current entity struct
currEnt = entities[entity_idx];
entity_id_t player = cmpPlayerManager->GetPlayerByID(currEnt.playerID);
CmpPtr<ICmpPlayer> cmpPlayer(sim, player);
// Don't add entities for removed players.
if (cmpPlayer && cmpPlayer->IsRemoved())
{
entity_idx++;
continue;
}
entity_id_t ent = pSimulation2->AddEntity(currEnt.templateName, currEnt.entityID);
entity_id_t player = cmpPlayerManager->GetPlayerByID(currEnt.playerID);
if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
{ // Don't add entities with invalid player IDs
LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(currEnt.templateName));

View file

@ -92,6 +92,11 @@ public:
return m_Script.Call<std::string>("GetState");
}
bool IsRemoved() override
{
return m_Script.Call<bool>("IsRemoved");
}
bool IsActive() final
{
return m_IsActive;

View file

@ -37,6 +37,7 @@ public:
virtual CFixedVector3D GetStartingCameraRot() = 0;
virtual bool HasStartingCamera() = 0;
virtual bool IsRemoved() = 0;
virtual std::string GetState() = 0;
// See the cpp file for why this is implemented in C++.