Source: gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js

// Declare this first to avoid redundant lint warnings.
class PlayerAssignmentItem
{
}

/**
 * Warning: this class handles more than most other GUI controls.
 * Indeed, the logic of how to handle player assignments is here,
 * as that is not really a GUI-agnostic concern
 * (campaigns and other autostarting scripts should handle that themselves).
 */
PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSettingControlDropdown
{
	constructor(...args)
	{
		super(...args);

		this.clientItemFactory = new PlayerAssignmentItem.Client();
		this.aiItemFactory = new PlayerAssignmentItem.AI();
		this.unassignedItem = new PlayerAssignmentItem.Unassigned().createItem();

		this.aiItems =
			g_Settings.AIDescriptions.filter(ai => !ai.data.hidden).map(
				this.aiItemFactory.createItem.bind(this.aiItemFactory));

		this.values = undefined;
		this.assignedGUID = undefined;
		this.fixedAI = undefined;

		// Build the initial list of values with undefined & AI clients.
		this.rebuildList();

		g_GameSettings.playerAI.watch(() => this.render(), ["values"]);
		g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]);
	}

	setControl()
	{
		this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]");
		this.label = Engine.GetGUIObjectByName("playerAssignmentText[" + this.playerIndex + "]");
	}

	OnPlayerNbChange(oldNb)
	{
		let 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)
		{
			// Add AIs to unused slots by default.
			// TODO: we could save the settings in case the player lowers, then re-raises the # of players.
			g_GameSettings.playerAI.set(this.playerIndex, {
				"bot": g_Settings.PlayerDefaults[this.playerIndex + 1].AI,
				"difficulty": +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"),
				"behavior": Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"),
			});
		}
	}

	onPlayerAssignmentsChange()
	{
		// Rebuild the list to account for new/removed players.
		this.rebuildList();
		let newGUID;
		for (let guid in g_PlayerAssignments)
			if (g_PlayerAssignments[guid].player == this.playerIndex + 1)
			{
				newGUID = guid;
				break;
			}
		if (this.assignedGUID === newGUID)
			return;
		this.assignedGUID = newGUID;
		// Remove the AI from the slot if there was one.
		if (this.assignedGUID && g_GameSettings.playerAI.get(this.playerIndex))
		{
			g_GameSettings.playerAI.setAI(this.playerIndex, undefined);
			this.gameSettingsController.setNetworkInitAttributes();
		}
		this.render();
	}

	render()
	{
		this.setEnabled(true);
		if (this.assignedGUID)
		{
			this.setSelectedValue(this.assignedGUID);
			return;
		}
		let ai = g_GameSettings.playerAI.get(this.playerIndex);
		if (ai)
		{
			this.setSelectedValue(ai.bot);
			return;
		}

		this.setSelectedValue(undefined);
	}

	rebuildList()
	{
		Engine.ProfileStart("updatePlayerAssignmentsList");
		// TODO: this particular bit is done for each row, which is unnecessarily inefficient.
		this.playerItems = sortGUIDsByPlayerID().map(
			this.clientItemFactory.createItem.bind(this.clientItemFactory));
		this.values = prepareForDropdown([
			...this.playerItems,
			...this.aiItems,
			this.unassignedItem
		]);

		let selected = this.dropdown.list_data?.[this.dropdown.selected];
		this.dropdown.list = this.values.Caption;
		this.dropdown.list_data = this.values.Value.map(x => x || "undefined");
		this.setSelectedValue(selected);
		Engine.ProfileStop();
	}

	onSelectionChange(itemIdx)
	{
		this.values.Handler[itemIdx].onSelectionChange(
			this.gameSettingsController,
			this.playerAssignmentsController,
			this.playerIndex,
			this.values.Value[itemIdx]);
	}

	getAutocompleteEntries()
	{
		return this.values.Autocomplete;
	}
};

PlayerSettingControls.PlayerAssignment.prototype.Tooltip =
	translate("Select player.");

PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;

{
	PlayerAssignmentItem.Client = class
	{
		createItem(guid)
		{
			return {
				"Handler": this,
				"Value": guid,
				"Autocomplete": g_PlayerAssignments[guid].name,
				"Caption": setStringTags(
					g_PlayerAssignments[guid].name,
					g_PlayerAssignments[guid].player == -1 ? this.ObserverTags : this.PlayerTags)
			};
		}

		onSelectionChange(gameSettingsController, playerAssignmentsController, playerIndex, guidToAssign)
		{
			let sourcePlayer = g_PlayerAssignments[guidToAssign].player - 1;
			if (sourcePlayer >= 0)
			{
				let ai = g_GameSettings.playerAI.get(playerIndex);
				// If the target was an AI, swap so AI settings are kept.
				if (ai)
					g_GameSettings.playerAI.swap(sourcePlayer, playerIndex);
				// Swap color + civ as well - this allows easy reorganizing of player order.
				if (g_GameSettings.map.type !== "scenario")
				{
					g_GameSettings.playerCiv.swap(sourcePlayer, playerIndex);
					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 =
		{ "color": "white" };

	PlayerAssignmentItem.Client.prototype.ObserverTags =
		{ "color": "170 170 250" };
}

{
	PlayerAssignmentItem.AI = class
	{
		createItem(ai)
		{
			let aiName = translate(ai.data.name);
			return {
				"Handler": this,
				"Value": ai.id,
				"Autocomplete": aiName,
				"Caption": setStringTags(sprintf(this.Label, { "ai": aiName }), this.Tags)
			};
		}

		onSelectionChange(gameSettingsController, playerAssignmentsController, playerIndex, value)
		{
			playerAssignmentsController.unassignClient(playerIndex + 1);

			g_GameSettings.playerAI.set(playerIndex, {
				"bot": value,
				"difficulty": +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"),
				"behavior": Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"),
			});

			gameSettingsController.setNetworkInitAttributes();
		}

		isSelected(pData, guid, value)
		{
			return !guid && pData.AI && pData.AI == value;
		}
	};

	PlayerAssignmentItem.AI.prototype.Label =
		translate("AI: %(ai)s");

	PlayerAssignmentItem.AI.prototype.Tags =
		{ "color": "70 150 70" };
}

{
	PlayerAssignmentItem.Unassigned = class
	{
		createItem()
		{
			return {
				"Handler": this,
				"Value": undefined,
				"Autocomplete": this.Label,
				"Caption": setStringTags(this.Label, this.Tags)
			};
		}

		onSelectionChange(gameSettingsController, playerAssignmentsController, playerIndex)
		{
			playerAssignmentsController.unassignClient(playerIndex + 1);

			g_GameSettings.playerAI.setAI(playerIndex, undefined);

			gameSettingsController.setNetworkInitAttributes();
		}

		isSelected(pData, guid, value)
		{
			return !guid && !pData.AI;
		}
	};

	PlayerAssignmentItem.Unassigned.prototype.Label =
		translate("Unassigned");

	PlayerAssignmentItem.Unassigned.prototype.Tags =
		{ "color": "140 140 140" };
}