1 const g_MatchSettings_SP = "config/matchsettings.json";
2 const g_MatchSettings_MP = "config/matchsettings.mp.json";
4 const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
5 const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly));
6 const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
7 const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
8 const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
9 const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
10 const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
11 const g_WonderDurations = prepareForDropdown(g_Settings && g_Settings.WonderDurations);
14 * All selectable playercolors except gaia.
16 const g_PlayerColors = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
19 * Directory containing all maps of the given type.
22 "random": "maps/random/",
23 "scenario": "maps/scenarios/",
24 "skirmish": "maps/skirmishes/"
28 * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
30 const g_NetMessageTypes = {
31 "netstatus": msg => handleNetStatusMessage(msg),
32 "netwarn": msg => addNetworkWarning(msg),
33 "gamesetup": msg => handleGamesetupMessage(msg),
34 "players": msg => handlePlayerAssignmentMessage(msg),
35 "ready": msg => handleReadyMessage(msg),
36 "start": msg => handleGamestartMessage(msg),
37 "kicked": msg => addChatMessage({
38 "type": msg.banned ? "banned" : "kicked",
39 "username": msg.username
41 "chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text })
44 const g_FormatChatMessage = {
45 "system": (msg, user) => systemMessage(msg.text),
46 "settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
47 "connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
48 "disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
49 "kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
50 "banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })),
51 "chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), {
52 "username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })),
53 "message": escapeText(msg.text || "")
55 "ready": (msg, user) => sprintf(translate("* %(username)s is ready!"), {
58 "not-ready": (msg, user) => sprintf(translate("* %(username)s is not ready."), {
61 "clientlist": (msg, user) => getUsernameList()
65 * The dropdownlist items will appear in the order they are added.
67 const g_MapFilters = [
70 "name": translate("Default"),
71 "filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1)
75 "name": translate("Naval Maps"),
76 "filter": mapKeywords => mapKeywords.indexOf("naval") != -1
80 "name": translate("Demo Maps"),
81 "filter": mapKeywords => mapKeywords.indexOf("demo") != -1
85 "name": translate("New Maps"),
86 "filter": mapKeywords => mapKeywords.indexOf("new") != -1
90 "name": translate("Trigger Maps"),
91 "filter": mapKeywords => mapKeywords.indexOf("trigger") != -1
95 "name": translate("All Maps"),
96 "filter": mapKeywords => true
101 * Used for generating the botnames.
103 const g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
106 * Offer users to select playable civs only.
107 * Load unselectable civs as they could appear in scenario maps.
109 const g_CivData = loadCivData();
112 * Used for highlighting the sender of chat messages.
114 const g_SenderFont = "sans-bold-13";
117 * Highlight the "random" dropdownlist item.
119 const g_ColorRandom = "orange";
122 * Highlight AIs in the player-dropdownlist.
124 const g_AIColor = "70 150 70";
127 * Color for "Unassigned"-placeholder item in the dropdownlist.
129 const g_UnassignedColor = "140 140 140";
132 * Highlight observer players in the dropdownlist.
134 const g_UnassignedPlayerColor = "170 170 250";
137 * Highlight ready players.
139 const g_ReadyColor = "green";
142 * Placeholder item for the map-dropdownlist.
144 const g_RandomMap = '[color="' + g_ColorRandom + '"]' + translateWithContext("map selection", "Random") + "[/color]";
147 * Placeholder item for the civ-dropdownlists.
149 const g_RandomCiv = '[color="' + g_ColorRandom + '"]' + translateWithContext("civilization", "Random") + '[/color]';
152 * Whether this is a single- or multiplayer match.
157 * Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame).
162 * To report the game to the lobby bot.
168 * States whether the GUI is currently updated in response to network messages instead of user input
169 * and therefore shouldn't send further messages to the network.
174 * Whether the current player is ready to start the game.
179 * Ignore duplicate ready commands on init.
181 var g_ReadyInit = true;
184 * If noone has changed the ready status, we have no need to spam the settings changed message.
186 * <=0 - Suppressed settings message
187 * 1 - Will show settings message
188 * 2 - Host's initial ready, suppressed settings message
190 var g_ReadyChanged = 2;
193 * Used to prevent calling resetReadyData when starting a game.
195 var g_GameStarted = false;
197 var g_PlayerAssignments = {};
199 var g_DefaultPlayerData = [];
201 var g_GameAttributes = { "settings": {} };
203 var g_ChatMessages = [];
206 * Cache containing the mapsettings for scenario/skirmish maps. Just-in-time loading.
211 * Wait one tick before initializing the GUI objects and
212 * don't process netmessages prior to that.
214 var g_LoadingState = 0;
217 * Only send a lobby update if something actually changed.
219 var g_LastGameStanza;
222 * Remembers if the current player viewed the AI settings of some playerslot.
224 var g_LastViewedAIPlayer = -1;
227 * Initializes some globals without touching the GUI.
229 * @param {Object} attribs - context data sent by the lobby / mainmenu
231 function init(attribs)
239 if (["offline", "server", "client"].indexOf(attribs.type) == -1)
241 error("Unexpected 'type' in gamesetup init: " + attribs.type);
246 g_IsNetworked = attribs.type != "offline";
247 g_IsController = attribs.type != "client";
248 g_ServerName = attribs.serverName;
249 g_ServerPort = attribs.serverPort;
251 // Replace empty playername when entering a singleplayermatch for the first time
254 Engine.ConfigDB_CreateValue("user", "playername.singleplayer", singleplayerName());
255 Engine.ConfigDB_WriteValueToFile("user", "playername.singleplayer", singleplayerName(), "config/user.cfg");
258 // Get default player data - remove gaia
259 g_DefaultPlayerData = g_Settings.PlayerDefaults;
260 g_DefaultPlayerData.shift();
261 for (let i in g_DefaultPlayerData)
262 g_DefaultPlayerData[i].Civ = "random";
264 setTimeout(displayGamestateNotifications, 1000);
268 * Called after the first tick.
270 function initGUIObjects()
272 Engine.GetGUIObjectByName("cancelGame").tooltip = Engine.HasXmppClient() ? translate("Return to the lobby.") : translate("Return to the main menu.");
280 g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked;
281 g_GameAttributes.settings.RatingEnabled = Engine.IsRankedGame() || undefined;
284 initNumberOfPlayers();
286 initPopulationCaps();
287 initStartingResources();
289 initWonderDurations();
290 initVictoryConditions();
297 initMultiplayerSettings();
298 initPlayerAssignments();
300 resizeMoreOptionsWindow();
302 Engine.GetGUIObjectByName("chatInput").tooltip = colorizeAutocompleteHotkey();
305 Engine.GetGUIObjectByName("chatInput").focus();
309 loadPersistMatchSettings();
311 warn("initGUIObjects() called while in GUI update");
312 updateGameAttributes();
316 function initMapTypes()
318 let mapTypes = Engine.GetGUIObjectByName("mapType");
319 mapTypes.list = g_MapTypes.Title;
320 mapTypes.list_data = g_MapTypes.Name;
321 mapTypes.onSelectionChange = function() {
322 if (this.selected != -1)
323 selectMapType(this.list_data[this.selected]);
326 mapTypes.selected = g_MapTypes.Default;
329 function initMapFilters()
331 let mapFilters = Engine.GetGUIObjectByName("mapFilter");
332 mapFilters.list = g_MapFilters.map(mapFilter => mapFilter.name);
333 mapFilters.list_data = g_MapFilters.map(mapFilter => mapFilter.id);
334 mapFilters.onSelectionChange = function() {
335 if (this.selected != -1)
336 selectMapFilter(this.list_data[this.selected]);
339 mapFilters.selected = 0;
340 g_GameAttributes.mapFilter = "default";
344 * Remove empty space in case of hidden options (like cheats, rating or wonder duration)
346 function resizeMoreOptionsWindow()
348 const elementHeight = 30;
350 let yPos = undefined;
352 for (let guiOption of Engine.GetGUIObjectByName("moreOptions").children)
354 if (guiOption.name == "moreOptionsLabel")
357 let gSize = guiOption.size;
358 yPos = yPos || gSize.top;
360 if (guiOption.hidden)
364 gSize.bottom = yPos + elementHeight - 2;
365 guiOption.size = gSize;
367 yPos += elementHeight;
370 // Resize the vertically centered window containing the options
371 let moreOptions = Engine.GetGUIObjectByName("moreOptions");
372 let mSize = moreOptions.size;
373 mSize.bottom = mSize.top + yPos + 20;
374 moreOptions.size = mSize;
377 function initNumberOfPlayers()
379 let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ..., MaxPlayers
380 let numPlayers = Engine.GetGUIObjectByName("numPlayers");
381 numPlayers.list = playersArray;
382 numPlayers.list_data = playersArray;
383 numPlayers.onSelectionChange = function() {
384 if (this.selected != -1)
385 selectNumPlayers(this.list_data[this.selected]);
387 numPlayers.selected = g_MaxPlayers - 1;
390 function initGameSpeed()
392 let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
393 gameSpeed.hidden = false;
394 Engine.GetGUIObjectByName("gameSpeedText").hidden = true;
395 gameSpeed.list = g_GameSpeeds.Title;
396 gameSpeed.list_data = g_GameSpeeds.Speed;
397 gameSpeed.onSelectionChange = function() {
398 if (this.selected != -1)
399 g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[this.selected];
401 updateGameAttributes();
403 gameSpeed.selected = g_GameSpeeds.Default;
406 function initPopulationCaps()
408 let populationCaps = Engine.GetGUIObjectByName("populationCap");
409 populationCaps.list = g_PopulationCapacities.Title;
410 populationCaps.list_data = g_PopulationCapacities.Population;
411 populationCaps.selected = g_PopulationCapacities.Default;
412 populationCaps.onSelectionChange = function() {
413 if (this.selected != -1)
414 g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[this.selected];
416 updateGameAttributes();
420 function initStartingResources()
422 let startingResourcesL = Engine.GetGUIObjectByName("startingResources");
423 startingResourcesL.list = g_StartingResources.Title;
424 startingResourcesL.list_data = g_StartingResources.Resources;
425 startingResourcesL.selected = g_StartingResources.Default;
426 startingResourcesL.onSelectionChange = function() {
427 if (this.selected != -1)
428 g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[this.selected];
430 updateGameAttributes();
434 function initCeasefire()
436 let ceasefireL = Engine.GetGUIObjectByName("ceasefire");
437 ceasefireL.list = g_Ceasefire.Title;
438 ceasefireL.list_data = g_Ceasefire.Duration;
439 ceasefireL.selected = g_Ceasefire.Default;
440 ceasefireL.onSelectionChange = function() {
441 if (this.selected != -1)
442 g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[this.selected];
444 updateGameAttributes();
448 function initVictoryConditions()
450 let victoryConditions = Engine.GetGUIObjectByName("victoryCondition");
451 victoryConditions.list = g_VictoryConditions.Title;
452 victoryConditions.list_data = g_VictoryConditions.Name;
453 victoryConditions.onSelectionChange = function() {
454 if (this.selected != -1)
456 g_GameAttributes.settings.GameType = g_VictoryConditions.Name[this.selected];
457 g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[this.selected];
460 updateGameAttributes();
462 victoryConditions.selected = g_VictoryConditions.Default;
465 function initWonderDurations()
467 let wonderConditions = Engine.GetGUIObjectByName("wonderDuration");
468 wonderConditions.list = g_WonderDurations.Title;
469 wonderConditions.list_data = g_WonderDurations.Duration;
470 wonderConditions.onSelectionChange = function()
472 if (this.selected != -1)
473 g_GameAttributes.settings.WonderDuration = g_WonderDurations.Duration[this.selected];
475 updateGameAttributes();
477 wonderConditions.selected = g_WonderDurations.Default;
480 function initMapSizes()
482 let mapSize = Engine.GetGUIObjectByName("mapSize");
483 mapSize.list = g_MapSizes.Name;
484 mapSize.list_data = g_MapSizes.Tiles;
485 mapSize.onSelectionChange = function() {
486 if (this.selected != -1)
487 g_GameAttributes.settings.Size = g_MapSizes.Tiles[this.selected];
488 updateGameAttributes();
490 mapSize.selected = 0;
494 * Assign update-functions to all checkboxes.
496 function initRadioButtons()
499 "RevealMap": "revealMap",
500 "ExploreMap": "exploreMap",
501 "DisableTreasures": "disableTreasures",
502 "LockTeams": "lockTeams",
503 "LastManStanding" : "lastManStanding",
504 "CheatsEnabled": "enableCheats"
507 Object.keys(options).forEach(attribute => {
508 Engine.GetGUIObjectByName(options[attribute]).onPress = function() {
509 g_GameAttributes.settings[attribute] = this.checked;
510 updateGameAttributes();
514 Engine.GetGUIObjectByName("enableRating").onPress = function() {
515 g_GameAttributes.settings.RatingEnabled = this.checked;
516 Engine.SetRankedGame(this.checked);
517 Engine.GetGUIObjectByName("enableCheats").enabled = !this.checked;
518 Engine.GetGUIObjectByName("lockTeams").enabled = !this.checked;
519 updateGameAttributes();
522 Engine.GetGUIObjectByName("lockTeams").onPress = function() {
523 g_GameAttributes.settings.LockTeams = this.checked;
524 g_GameAttributes.settings.LastManStanding = false;
525 updateGameAttributes();
529 function hideStartGameButton(hidden)
533 let startGame = Engine.GetGUIObjectByName("startGame");
534 startGame.hidden = hidden;
535 let right = hidden ? startGame.size.right : startGame.size.left - offset;
537 let cancelGame = Engine.GetGUIObjectByName("cancelGame");
538 let cancelGameSize = cancelGame.size;
539 let xButtonSize = cancelGameSize.right - cancelGameSize.left;
540 cancelGameSize.right = right;
541 right -= xButtonSize;
543 for (let element of ["cheatWarningText", "onscreenToolTip"])
545 let elementSize = Engine.GetGUIObjectByName(element).size;
546 elementSize.right = right - (cancelGameSize.left - elementSize.right);
547 Engine.GetGUIObjectByName(element).size = elementSize;
550 cancelGameSize.left = right;
551 cancelGame.size = cancelGameSize;
555 * If we're a network client, hide the controls and show the text instead.
557 function hideControls()
559 for (let ctrl of ["mapType", "mapFilter", "mapSelection", "victoryCondition", "gameSpeed", "numPlayers"])
560 hideControl(ctrl, ctrl + "Text");
562 // TODO: Shouldn't players be able to choose their own assignment?
563 for (let i = 0; i < g_MaxPlayers; ++i)
565 Engine.GetGUIObjectByName("playerAssignment["+i+"]").hidden = true;
566 Engine.GetGUIObjectByName("playerCiv["+i+"]").hidden = true;
567 Engine.GetGUIObjectByName("playerTeam["+i+"]").hidden = true;
570 // The start game button should be hidden until the player assignments are received
571 // and it is known whether the local player is an observer.
572 hideStartGameButton(true);
573 Engine.GetGUIObjectByName("startGame").enabled = true;
577 * Hides the GUI controls for clients and shows the read-only label instead.
579 * @param {string} control - name of the GUI object able to change a setting
580 * @param {string} label - name of the GUI object displaying a setting
581 * @param {boolean} [allowControl] - Whether the current user is allowed to change the control.
583 function hideControl(control, label, allowControl = g_IsController)
585 Engine.GetGUIObjectByName(control).hidden = !allowControl;
586 Engine.GetGUIObjectByName(label).hidden = allowControl;
590 * Checks a boolean checkbox for the host and sets the text of the label for the client.
592 * @param {string} control - name of the GUI object able to change a setting
593 * @param {string} label - name of the GUI object displaying a setting
594 * @param {boolean} checked - Whether the setting is active / enabled.
596 function setGUIBoolean(control, label, checked)
598 Engine.GetGUIObjectByName(control).checked = checked;
599 Engine.GetGUIObjectByName(label).caption = checked ? translate("Yes") : translate("No");
603 * Hide and set some elements depending on whether we play single- or multiplayer.
605 function initMultiplayerSettings()
607 Engine.GetGUIObjectByName("chatPanel").hidden = !g_IsNetworked;
608 Engine.GetGUIObjectByName("optionCheats").hidden = !g_IsNetworked;
609 Engine.GetGUIObjectByName("optionRating").hidden = !Engine.HasXmppClient();
611 Engine.GetGUIObjectByName("enableCheats").enabled = !Engine.IsRankedGame();
612 Engine.GetGUIObjectByName("lockTeams").enabled = !Engine.IsRankedGame();
614 Engine.GetGUIObjectByName("enableCheats").checked = g_GameAttributes.settings.CheatsEnabled;
615 Engine.GetGUIObjectByName("enableRating").checked = !!g_GameAttributes.settings.RatingEnabled;
617 for (let ctrl of ["enableCheats", "enableRating"])
618 hideControl(ctrl, ctrl + "Text");
622 * Populate team-, color- and civ-dropdowns.
624 function initPlayerAssignments()
627 for (let i = 0; i < g_MaxPlayers; ++i)
629 let box = Engine.GetGUIObjectByName("playerBox["+i+"]");
630 let boxSize = box.size;
631 let h = boxSize.bottom - boxSize.top;
632 boxSize.top = i * boxSpacing;
633 boxSize.bottom = i * boxSpacing + h;
636 let team = Engine.GetGUIObjectByName("playerTeam["+i+"]");
637 let teamsArray = Array(g_MaxTeams).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxTeams
638 team.list = [translateWithContext("team", "None")].concat(teamsArray); // "None", 1, 2, ..., maxTeams
639 team.list_data = [-1].concat(teamsArray.map(team => team - 1)); // -1, 0, ..., (maxTeams-1)
642 let playerSlot = i; // declare for inner function use
643 team.onSelectionChange = function() {
644 if (this.selected != -1)
645 g_GameAttributes.settings.PlayerData[playerSlot].Team = this.selected - 1;
647 updateGameAttributes();
650 let colorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
651 colorPicker.list = g_PlayerColors.map(color => ' ' + '[color="' + rgbToGuiColor(color) + '"]â– [/color]');
652 colorPicker.list_data = g_PlayerColors.map((color, index) => index);
653 colorPicker.selected = -1;
654 colorPicker.onSelectionChange = function() { selectPlayerColor(playerSlot, this.selected); };
656 Engine.GetGUIObjectByName("playerCiv["+i+"]").onSelectionChange = function() {
657 if ((this.selected != -1)&&(g_GameAttributes.mapType !== "scenario"))
658 g_GameAttributes.settings.PlayerData[playerSlot].Civ = this.list_data[this.selected];
660 updateGameAttributes();
666 * Called when the client disconnects.
667 * The other cases from NetClient should never occur in the gamesetup.
668 * @param {Object} message
670 function handleNetStatusMessage(message)
672 if (message.status != "disconnected")
674 error("Unrecognised netstatus type " + message.status);
679 reportDisconnect(message.reason, true);
683 * Called whenever a client clicks on ready (or not ready).
684 * @param {Object} message
686 function handleReadyMessage(message)
690 if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
692 "type": message.status == 1 ? "ready" : "not-ready",
699 g_PlayerAssignments[message.guid].status = +message.status == 1;
704 * Called after every player is ready and the host decided to finally start the game.
705 * @param {Object} message
707 function handleGamestartMessage(message)
709 // Immediately inform the lobby server instead of waiting for the load to finish
710 if (g_IsController && Engine.HasXmppClient())
712 let clients = formatClientsForStanza();
713 Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
716 Engine.SwitchGuiPage("page_loading.xml", {
717 "attribs": g_GameAttributes,
718 "isNetworked" : g_IsNetworked,
719 "playerAssignments": g_PlayerAssignments,
720 "isController": g_IsController
725 * Called whenever the host changed any setting.
726 * @param {Object} message
728 function handleGamesetupMessage(message)
733 g_GameAttributes = message.data;
735 if (!!g_GameAttributes.settings.RatingEnabled)
737 g_GameAttributes.settings.CheatsEnabled = false;
738 g_GameAttributes.settings.LockTeams = true;
739 g_GameAttributes.settings.LastManStanding = false;
742 Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
748 * Called whenever a client joins/leaves or any gamesetting is changed.
749 * @param {Object} message
751 function handlePlayerAssignmentMessage(message)
753 for (let guid in message.newAssignments)
754 if (!g_PlayerAssignments[guid])
755 onClientJoin(guid, message.newAssignments);
757 for (let guid in g_PlayerAssignments)
758 if (!message.newAssignments[guid])
761 g_PlayerAssignments = message.newAssignments;
763 hideStartGameButton(!g_IsController && g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1);
767 sendRegisterGameStanza();
770 function onClientJoin(newGUID, newAssignments)
775 "username": newAssignments[newGUID].name
778 // Assign joining observers to unused player numbers
779 if (!g_IsController || newAssignments[newGUID].player != -1)
782 let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v,i) =>
783 Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i+1)
789 Engine.AssignNetworkPlayer(freeSlot + 1, newGUID);
793 function onClientLeave(guid)
796 "type": "disconnect",
800 if (g_PlayerAssignments[guid].player != -1)
805 * Doesn't translate, so that lobby clients can do that locally
806 * (even if they don't have that map).
808 function getMapDisplayName(map)
813 let mapData = loadMapData(map);
814 if (!mapData || !mapData.settings || !mapData.settings.Name)
817 return mapData.settings.Name;
820 function getMapPreview(map)
822 let mapData = loadMapData(map);
823 if (!mapData || !mapData.settings || !mapData.settings.Preview)
824 return "nopreview.png";
826 return mapData.settings.Preview;
830 * Get a playersetting or return the default if it wasn't set.
832 function getSetting(settings, defaults, property)
834 if (settings && (property in settings))
835 return settings[property];
837 if (defaults && (property in defaults))
838 return defaults[property];
844 * Initialize the dropdowns containing all selectable civs (including random).
846 function initCivNameList()
848 let civList = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase);
849 let civListNames = [g_RandomCiv].concat(civList.map(civ => civ.name));
850 let civListCodes = ["random"].concat(civList.map(civ => civ.code));
852 for (let i = 0; i < g_MaxPlayers; ++i)
854 let civ = Engine.GetGUIObjectByName("playerCiv["+i+"]");
855 civ.list = civListNames;
856 civ.list_data = civListCodes;
862 * Initialize the dropdown containing all maps for the selected maptype and mapfilter.
864 function initMapNameList()
866 if (!g_MapPath[g_GameAttributes.mapType])
868 error("Unexpected map type: " + g_GameAttributes.mapType);
872 let mapFiles = g_GameAttributes.mapType == "random" ?
873 getJSONFileList(g_GameAttributes.mapPath) :
874 getXMLFileList(g_GameAttributes.mapPath);
876 // Apply map filter, if any defined
877 // TODO: Should verify these are valid maps before adding to list
879 for (let mapFile of mapFiles)
881 let file = g_GameAttributes.mapPath + mapFile;
882 let mapData = loadMapData(file);
883 let mapFilter = g_MapFilters.find(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "all"));
885 if (!!mapData.settings && mapFilter && mapFilter.filter(mapData.settings.Keywords || []))
886 mapList.push({ "name": getMapDisplayName(file), "file": file });
889 translateObjectKeys(mapList, ["name"]);
890 mapList.sort(sortNameIgnoreCase);
892 let mapListNames = mapList.map(map => map.name);
893 let mapListFiles = mapList.map(map => map.file);
895 if (g_GameAttributes.mapType == "random")
897 mapListNames.unshift(g_RandomMap);
898 mapListFiles.unshift("random");
901 let mapSelectionBox = Engine.GetGUIObjectByName("mapSelection");
902 mapSelectionBox.list = mapListNames;
903 mapSelectionBox.list_data = mapListFiles;
904 mapSelectionBox.onSelectionChange = function() {
905 if (this.list_data[this.selected])
906 selectMap(this.list_data[this.selected]);
908 mapSelectionBox.selected = Math.max(0, mapListFiles.indexOf(g_GameAttributes.map || ""));
911 function loadMapData(name)
913 if (!name || !g_MapPath[g_GameAttributes.mapType])
916 if (name == "random")
917 return { "settings": { "Name": "", "Description": "" } };
919 if (!g_MapData[name])
920 g_MapData[name] = g_GameAttributes.mapType == "random" ?
921 Engine.ReadJSONFile(name + ".json") :
922 Engine.LoadMapSettings(name);
924 return g_MapData[name];
928 * Sets the gameattributes the way they were the last time the user left the gamesetup.
930 function loadPersistMatchSettings()
932 if (Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true")
935 let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
936 if (!Engine.FileExists(settingsFile))
939 let attrs = Engine.ReadJSONFile(settingsFile);
940 if (!attrs || !attrs.settings)
943 g_IsInGuiUpdate = true;
945 let mapName = attrs.map || "";
946 let mapSettings = attrs.settings;
948 g_GameAttributes = attrs;
951 mapSettings.CheatsEnabled = true;
953 // Replace unselectable civs with random civ
954 let playerData = mapSettings.PlayerData;
955 if (playerData && g_GameAttributes.mapType != "scenario")
956 for (let i in playerData)
957 if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup)
958 playerData[i].Civ = "random";
960 // Apply map settings
961 let newMapData = loadMapData(mapName);
962 if (newMapData && newMapData.settings)
964 for (let prop in newMapData.settings)
965 mapSettings[prop] = newMapData.settings[prop];
968 mapSettings.PlayerData = playerData;
971 if (mapSettings.PlayerData)
972 sanitizePlayerData(mapSettings.PlayerData);
974 // Reload, as the maptype or mapfilter might have changed
977 g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
978 Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
983 function savePersistMatchSettings()
985 let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {};
986 Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes);
989 function sanitizePlayerData(playerData)
992 if (playerData.length && !playerData[0])
995 playerData.forEach((pData, index) => {
996 pData.Color = pData.Color || g_PlayerColors[index];
997 pData.Civ = pData.Civ || "random";
999 // Use default AI if the map doesn't specify any explicitly
1000 if (!("AI" in pData))
1001 pData.AI = g_DefaultPlayerData[index].AI;
1003 if (!("AIDiff" in pData))
1004 pData.AIDiff = g_DefaultPlayerData[index].AIDiff;
1007 // Replace colors with the best matching color of PlayerDefaults
1008 if (g_GameAttributes.mapType != "scenario")
1010 playerData.forEach((pData, index) => {
1011 let colorDistances = g_PlayerColors.map(color => colorDistance(color, pData.Color));
1012 let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
1013 pData.Color = g_PlayerColors.find(color => colorDistance(color, pData.Color) == smallestDistance);
1017 ensureUniquePlayerColors(playerData);
1020 function cancelSetup()
1023 savePersistMatchSettings();
1025 Engine.DisconnectNetworkGame();
1027 if (Engine.HasXmppClient())
1029 Engine.LobbySetPlayerPresence("available");
1032 Engine.SendUnregisterGame();
1034 Engine.SwitchGuiPage("page_lobby.xml");
1037 Engine.SwitchGuiPage("page_pregame.xml");
1041 * Can't init the GUI before the first tick.
1042 * Process netmessages afterwards.
1049 // First tick happens before first render, so don't load yet
1050 if (g_LoadingState == 0)
1054 else if (g_LoadingState == 1)
1056 Engine.GetGUIObjectByName("loadingWindow").hidden = true;
1057 Engine.GetGUIObjectByName("setupWindow").hidden = false;
1061 else if (g_LoadingState == 2)
1065 let message = Engine.PollNetworkClient();
1069 log("Net message: " + uneval(message));
1071 if (g_NetMessageTypes[message.type])
1072 g_NetMessageTypes[message.type](message);
1074 error("Unrecognised net message type " + message.type);
1082 * Called when the map or the number of players changes.
1084 function unassignInvalidPlayers(maxPlayers)
1088 // Remove invalid playerIDs from the servers playerassignments copy
1089 for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID)
1090 Engine.AssignNetworkPlayer(playerID, "");
1092 else if (!g_PlayerAssignments.local ||
1093 g_PlayerAssignments.local.player > maxPlayers)
1094 g_PlayerAssignments = {
1096 "name": singleplayerName(),
1103 * Called when the host choses the number of players on a random map.
1104 * @param {Number} num
1106 function selectNumPlayers(num)
1108 if (g_IsInGuiUpdate || !g_IsController || g_GameAttributes.mapType != "random")
1111 let pData = g_GameAttributes.settings.PlayerData;
1112 g_GameAttributes.settings.PlayerData =
1113 num > pData.length ?
1114 pData.concat(g_DefaultPlayerData.slice(pData.length, num)) :
1115 pData.slice(0, num);
1117 unassignInvalidPlayers(num);
1119 sanitizePlayerData(g_GameAttributes.settings.PlayerData);
1121 updateGameAttributes();
1125 * Assigns the given color to that player.
1127 function selectPlayerColor(playerSlot, colorIndex)
1129 if (colorIndex == -1)
1132 let playerData = g_GameAttributes.settings.PlayerData;
1134 // If someone else has that color, give that player the old color
1135 let pData = playerData.find(pData => sameColor(g_PlayerColors[colorIndex], pData.Color));
1137 pData.Color = playerData[playerSlot].Color;
1139 // Assign the new color
1140 playerData[playerSlot].Color = g_PlayerColors[colorIndex];
1142 // Ensure colors are not used twice after increasing the number of players
1143 ensureUniquePlayerColors(playerData);
1145 if (!g_IsInGuiUpdate)
1146 updateGameAttributes();
1149 function ensureUniquePlayerColors(playerData)
1151 for (let i = playerData.length - 1; i >= 0; --i)
1152 // If someone else has that color, assign an unused color
1153 if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
1154 playerData[i].Color = g_PlayerColors.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
1158 * Called when the user selects a map type from the list.
1160 * @param {string} type - scenario, skirmish or random
1162 function selectMapType(type)
1164 if (g_IsInGuiUpdate || !g_IsController)
1167 if (!g_MapPath[type])
1169 error("selectMapType: Unexpected map type " + type);
1174 g_GameAttributes.map = "";
1175 g_GameAttributes.mapType = type;
1176 g_GameAttributes.mapPath = g_MapPath[type];
1178 if (type != "scenario")
1179 g_GameAttributes.settings = {
1180 "PlayerData": g_DefaultPlayerData.slice(0, 4),
1181 "CheatsEnabled": g_GameAttributes.settings.CheatsEnabled
1186 updateGameAttributes();
1189 function selectMapFilter(id)
1191 if (g_IsInGuiUpdate || !g_IsController)
1194 g_GameAttributes.mapFilter = id;
1198 updateGameAttributes();
1201 function selectMap(name)
1203 if (g_IsInGuiUpdate || !g_IsController || !name)
1206 // Reset some map specific properties which are not necessarily redefined on each map
1207 for (let prop of ["TriggerScripts", "CircularMap", "Garrison"])
1208 g_GameAttributes.settings[prop] = undefined;
1210 let mapData = loadMapData(name);
1211 let mapSettings = mapData && mapData.settings ? deepcopy(mapData.settings) : {};
1213 // Reset victory conditions
1214 if (g_GameAttributes.mapType != "random")
1216 let victoryIdx = g_VictoryConditions.Name.indexOf(mapSettings.GameType || "") != -1 ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
1217 g_GameAttributes.settings.GameType = g_VictoryConditions.Name[victoryIdx];
1218 g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[victoryIdx];
1221 if (g_GameAttributes.mapType == "scenario")
1223 delete g_GameAttributes.settings.WonderDuration;
1224 delete g_GameAttributes.settings.LastManStanding;
1227 if (mapSettings.PlayerData)
1228 sanitizePlayerData(mapSettings.PlayerData);
1230 // Copy any new settings
1231 g_GameAttributes.map = name;
1232 g_GameAttributes.script = mapSettings.Script;
1233 if (g_GameAttributes.map !== "random")
1234 for (let prop in mapSettings)
1235 g_GameAttributes.settings[prop] = mapSettings[prop];
1237 unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length);
1239 updateGameAttributes();
1242 function launchGame()
1244 if (!g_IsController)
1246 error("Only host can start game");
1250 if (!g_GameAttributes.map)
1253 savePersistMatchSettings();
1255 // Select random map
1256 if (g_GameAttributes.map == "random")
1258 let victoryScriptsSelected = g_GameAttributes.settings.VictoryScripts;
1259 let gameTypeSelected = g_GameAttributes.settings.GameType;
1260 selectMap(Engine.GetGUIObjectByName("mapSelection").list_data[Math.floor(Math.random() *
1261 (Engine.GetGUIObjectByName("mapSelection").list.length - 1)) + 1]);
1262 g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected;
1263 g_GameAttributes.settings.GameType = gameTypeSelected;
1266 g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []);
1268 // Prevent reseting the readystate
1269 g_GameStarted = true;
1271 g_GameAttributes.settings.mapType = g_GameAttributes.mapType;
1273 // Get a unique array of selectable cultures
1274 let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture);
1275 cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
1277 // Determine random civs and botnames
1278 for (let i in g_GameAttributes.settings.PlayerData)
1280 // Pick a random civ of a random culture
1281 let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random";
1282 if (chosenCiv == "random")
1284 let culture = cultures[Math.floor(Math.random() * cultures.length)];
1285 let civs = Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture);
1286 chosenCiv = civs[Math.floor(Math.random() * civs.length)];
1288 g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv;
1290 // Pick one of the available botnames for the chosen civ
1291 if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI)
1294 let chosenName = g_CivData[chosenCiv].AINames[Math.floor(Math.random() * g_CivData[chosenCiv].AINames.length)];
1297 chosenName = translate(chosenName);
1299 // Count how many players use the chosenName
1300 let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length;
1302 g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName : sprintf(translate("%(playerName)s %(romanNumber)s"), { "playerName": chosenName, "romanNumber": g_RomanNumbers[usedName+1] });
1305 // Copy playernames for the purpose of replays
1306 for (let guid in g_PlayerAssignments)
1308 let player = g_PlayerAssignments[guid];
1309 if (player.player > 0) // not observer or GAIA
1310 g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
1313 // Seed used for both map generation and simulation
1314 g_GameAttributes.settings.Seed = Math.floor(Math.random() * Math.pow(2, 32));
1315 g_GameAttributes.settings.AISeed = Math.floor(Math.random() * Math.pow(2, 32));
1317 // Used for identifying rated game reports for the lobby
1318 g_GameAttributes.matchID = Engine.GetMatchID();
1322 Engine.SetNetworkGameAttributes(g_GameAttributes);
1323 Engine.StartNetworkGame();
1327 // Find the player ID which the user has been assigned to
1329 for (let i in g_GameAttributes.settings.PlayerData)
1331 let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
1332 if (assignBox.list_data[assignBox.selected] == "local")
1336 Engine.StartGame(g_GameAttributes, playerID);
1337 Engine.SwitchGuiPage("page_loading.xml", {
1338 "attribs": g_GameAttributes,
1339 "isNetworked" : g_IsNetworked,
1340 "playerAssignments": g_PlayerAssignments
1346 * Don't set any attributes here, just show the changes in the GUI.
1348 * Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
1350 function updateGUIObjects()
1352 g_IsInGuiUpdate = true;
1354 let mapSettings = g_GameAttributes.settings;
1356 // These dropdowns don't set values while g_IsInGuiUpdate
1357 let mapName = g_GameAttributes.map || "";
1358 let mapFilterIdx = g_MapFilters.findIndex(mapFilter => mapFilter.id == (g_GameAttributes.mapFilter || "default"));
1359 let mapTypeIdx = g_GameAttributes.mapType !== undefined ? g_MapTypes.Name.indexOf(g_GameAttributes.mapType) : g_MapTypes.Default;
1360 let gameSpeedIdx = g_GameAttributes.gameSpeed !== undefined ? g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) : g_GameSpeeds.Default;
1362 // These dropdowns might set the default (as they ignore g_IsInGuiUpdate)
1363 let mapSizeIdx = mapSettings.Size !== undefined ? g_MapSizes.Tiles.indexOf(mapSettings.Size) : g_MapSizes.Default;
1364 let victoryIdx = mapSettings.GameType !== undefined ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default;
1365 let wonderDurationIdx = mapSettings.WonderDuration !== undefined ? g_WonderDurations.Duration.indexOf(mapSettings.WonderDuration) : g_WonderDurations.Default;
1366 let popIdx = mapSettings.PopulationCap !== undefined ? g_PopulationCapacities.Population.indexOf(mapSettings.PopulationCap) : g_PopulationCapacities.Default;
1367 let startingResIdx = mapSettings.StartingResources !== undefined ? g_StartingResources.Resources.indexOf(mapSettings.StartingResources) : g_StartingResources.Default;
1368 let ceasefireIdx = mapSettings.Ceasefire !== undefined ? g_Ceasefire.Duration.indexOf(mapSettings.Ceasefire) : g_Ceasefire.Default;
1369 let numPlayers = mapSettings.PlayerData ? mapSettings.PlayerData.length : g_MaxPlayers;
1373 Engine.GetGUIObjectByName("mapType").selected = mapTypeIdx;
1374 Engine.GetGUIObjectByName("mapFilter").selected = mapFilterIdx;
1375 Engine.GetGUIObjectByName("mapSelection").selected = Engine.GetGUIObjectByName("mapSelection").list_data.indexOf(mapName);
1376 Engine.GetGUIObjectByName("mapSize").selected = mapSizeIdx;
1377 Engine.GetGUIObjectByName("numPlayers").selected = numPlayers - 1;
1378 Engine.GetGUIObjectByName("victoryCondition").selected = victoryIdx;
1379 Engine.GetGUIObjectByName("wonderDuration").selected = wonderDurationIdx;
1380 Engine.GetGUIObjectByName("populationCap").selected = popIdx;
1381 Engine.GetGUIObjectByName("gameSpeed").selected = gameSpeedIdx;
1382 Engine.GetGUIObjectByName("ceasefire").selected = ceasefireIdx;
1383 Engine.GetGUIObjectByName("startingResources").selected = startingResIdx;
1387 Engine.GetGUIObjectByName("mapTypeText").caption = g_MapTypes.Title[mapTypeIdx];
1388 Engine.GetGUIObjectByName("mapFilterText").caption = g_MapFilters[mapFilterIdx].name;
1389 Engine.GetGUIObjectByName("mapSelectionText").caption = mapName == "random" ? g_RandomMap : translate(getMapDisplayName(mapName));
1393 // Can be visible to both host and clients
1394 Engine.GetGUIObjectByName("mapSizeText").caption = g_GameAttributes.mapType == "random" ? g_MapSizes.Name[mapSizeIdx] : translate("Default");
1395 Engine.GetGUIObjectByName("numPlayersText").caption = numPlayers;
1396 Engine.GetGUIObjectByName("victoryConditionText").caption = g_VictoryConditions.Title[victoryIdx];
1397 Engine.GetGUIObjectByName("wonderDurationText").caption = g_WonderDurations.Title[wonderDurationIdx];
1398 Engine.GetGUIObjectByName("populationCapText").caption = g_PopulationCapacities.Title[popIdx];
1399 Engine.GetGUIObjectByName("startingResourcesText").caption = g_StartingResources.Title[startingResIdx];
1400 Engine.GetGUIObjectByName("ceasefireText").caption = g_Ceasefire.Title[ceasefireIdx];
1401 Engine.GetGUIObjectByName("gameSpeedText").caption = g_GameSpeeds.Title[gameSpeedIdx];
1403 setGUIBoolean("enableCheats", "enableCheatsText", !!mapSettings.CheatsEnabled);
1404 setGUIBoolean("disableTreasures", "disableTreasuresText", !!mapSettings.DisableTreasures);
1405 setGUIBoolean("exploreMap", "exploreMapText", !!mapSettings.ExploreMap);
1406 setGUIBoolean("revealMap", "revealMapText", !!mapSettings.RevealMap);
1407 setGUIBoolean("lockTeams", "lockTeamsText", !!mapSettings.LockTeams);
1408 setGUIBoolean("lastManStanding", "lastManStandingText", !!mapSettings.LastManStanding);
1409 setGUIBoolean("enableRating", "enableRatingText", !!mapSettings.RatingEnabled);
1411 Engine.GetGUIObjectByName("optionWonderDuration").hidden =
1412 g_GameAttributes.settings.GameType &&
1413 g_GameAttributes.settings.GameType != "wonder";
1415 Engine.GetGUIObjectByName("cheatWarningText").hidden = !g_IsNetworked || !mapSettings.CheatsEnabled;
1417 Engine.GetGUIObjectByName("lastManStanding").enabled = !mapSettings.LockTeams;
1418 Engine.GetGUIObjectByName("enableCheats").enabled = !mapSettings.RatingEnabled;
1419 Engine.GetGUIObjectByName("lockTeams").enabled = !mapSettings.RatingEnabled;
1421 // Mapsize completely hidden for non-random maps
1422 let isRandom = g_GameAttributes.mapType == "random";
1423 Engine.GetGUIObjectByName("mapSizeDesc").hidden = !isRandom;
1424 Engine.GetGUIObjectByName("mapSize").hidden = !isRandom || !g_IsController;
1425 Engine.GetGUIObjectByName("mapSizeText").hidden = !isRandom || g_IsController;
1426 hideControl("numPlayers", "numPlayersText", isRandom && g_IsController);
1428 let notScenario = g_GameAttributes.mapType != "scenario" && g_IsController ;
1430 for (let ctrl of ["victoryCondition", "wonderDuration", "populationCap",
1431 "startingResources", "ceasefire", "revealMap",
1432 "exploreMap", "disableTreasures", "lockTeams", "lastManStanding"])
1433 hideControl(ctrl, ctrl + "Text", notScenario);
1435 Engine.GetGUIObjectByName("civResetButton").hidden = !notScenario;
1436 Engine.GetGUIObjectByName("teamResetButton").hidden = !notScenario;
1438 for (let i = 0; i < g_MaxPlayers; ++i)
1440 Engine.GetGUIObjectByName("playerBox["+i+"]").hidden = (i >= numPlayers);
1442 if (i >= numPlayers)
1445 let pName = Engine.GetGUIObjectByName("playerName["+i+"]");
1446 let pAssignment = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
1447 let pAssignmentText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
1448 let pCiv = Engine.GetGUIObjectByName("playerCiv["+i+"]");
1449 let pCivText = Engine.GetGUIObjectByName("playerCivText["+i+"]");
1450 let pTeam = Engine.GetGUIObjectByName("playerTeam["+i+"]");
1451 let pTeamText = Engine.GetGUIObjectByName("playerTeamText["+i+"]");
1452 let pColor = Engine.GetGUIObjectByName("playerColor["+i+"]");
1454 let pData = mapSettings.PlayerData ? mapSettings.PlayerData[i] : {};
1455 let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[i] : {};
1457 let color = getSetting(pData, pDefs, "Color");
1458 pColor.sprite = "color:" + rgbToGuiColor(color) + " 100";
1459 pName.caption = translate(getSetting(pData, pDefs, "Name"));
1461 let team = getSetting(pData, pDefs, "Team");
1462 let civ = getSetting(pData, pDefs, "Civ");
1464 pAssignmentText.caption = pAssignment.list[0] ? pAssignment.list[Math.max(0, pAssignment.selected)] : translate("Loading...");
1465 pCivText.caption = civ == "random" ? g_RandomCiv : (g_CivData[civ] ? g_CivData[civ].Name : "Unknown");
1466 pTeamText.caption = (team !== undefined && team >= 0) ? team+1 : "-";
1468 pCiv.selected = civ ? pCiv.list_data.indexOf(civ) : 0;
1469 pTeam.selected = team !== undefined && team >= 0 ? team+1 : 0;
1471 hideControl("playerAssignment["+i+"]", "playerAssignmentText["+i+"]", g_IsController);
1472 hideControl("playerCiv["+i+"]", "playerCivText["+i+"]", notScenario);
1473 hideControl("playerTeam["+i+"]", "playerTeamText["+i+"]", notScenario);
1475 // Allow host to chose player colors on non-scenario maps
1476 let pColorPicker = Engine.GetGUIObjectByName("playerColorPicker["+i+"]");
1477 let pColorPickerHeading = Engine.GetGUIObjectByName("playerColorHeading");
1478 let canChangeColors = g_IsController && g_GameAttributes.mapType != "scenario";
1479 pColorPicker.hidden = !canChangeColors;
1480 pColorPickerHeading.hidden = !canChangeColors;
1481 if (canChangeColors)
1482 pColorPicker.selected = g_PlayerColors.findIndex(col => sameColor(col, color));
1485 updateGameDescription();
1486 resizeMoreOptionsWindow();
1488 g_IsInGuiUpdate = false;
1490 // Game attributes include AI settings, so update the player list
1495 // Refresh AI config page
1496 if (g_LastViewedAIPlayer != -1)
1498 Engine.PopGuiPage();
1499 openAIConfig(g_LastViewedAIPlayer);
1503 function updateGameDescription()
1505 setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map));
1507 Engine.GetGUIObjectByName("mapInfoName").caption =
1508 translateMapTitle(getMapDisplayName(g_GameAttributes.map));
1510 Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription();
1514 * Broadcast the changed settings to all clients and the lobbybot.
1516 function updateGameAttributes()
1518 if (g_IsInGuiUpdate || !g_IsController)
1523 Engine.SetNetworkGameAttributes(g_GameAttributes);
1524 if (g_LoadingState >= 2)
1525 sendRegisterGameStanza();
1531 function openAIConfig(playerSlot)
1533 g_LastViewedAIPlayer = playerSlot;
1535 Engine.PushGuiPage("page_aiconfig.xml", {
1536 "callback": "AIConfigCallback",
1537 "isController": g_IsController,
1538 "playerSlot": playerSlot,
1539 "id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
1540 "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff
1545 * Called after closing the dialog.
1547 function AIConfigCallback(ai)
1549 g_LastViewedAIPlayer = -1;
1551 if (!ai.save || !g_IsController)
1554 g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
1555 g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
1557 updateGameAttributes();
1560 function updatePlayerList()
1562 g_IsInGuiUpdate = true;
1564 let hostNameList = [];
1565 let hostGuidList = [];
1566 let assignments = [];
1567 let aiAssignments = {};
1569 let assignedCount = 0;
1570 for (let guid of sortGUIDsByPlayerID())
1572 let player = g_PlayerAssignments[guid].player;
1575 hostNameList.push(g_PlayerAssignments[guid].name);
1577 hostNameList.push("[color=\""+ g_UnassignedPlayerColor + "\"]" + g_PlayerAssignments[guid].name + "[/color]");
1579 hostGuidList.push(guid);
1580 assignments[player] = hostNameList.length-1;
1586 // Only enable start button if we have enough assigned players
1588 Engine.GetGUIObjectByName("startGame").enabled = assignedCount > 0;
1590 for (let ai of g_Settings.AIDescriptions)
1592 // If the map uses a hidden AI then don't hide it
1593 if (ai.data.hidden && g_GameAttributes.settings.PlayerData.every(pData => pData.AI != ai.id))
1596 aiAssignments[ai.id] = hostNameList.length;
1597 hostNameList.push("[color=\""+ g_AIColor + "\"]" + sprintf(translate("AI: %(ai)s"), { "ai": translate(ai.data.name) }));
1598 hostGuidList.push("ai:" + ai.id);
1601 noAssignment = hostNameList.length;
1602 hostNameList.push("[color=\""+ g_UnassignedColor + "\"]" + translate("Unassigned"));
1603 hostGuidList.push("");
1605 for (let i = 0; i < g_MaxPlayers; ++i)
1608 let playerID = i+1; // we don't show Gaia, so first slot is ID 1
1610 let selection = assignments[playerID];
1612 let configButton = Engine.GetGUIObjectByName("playerConfig["+i+"]");
1613 configButton.hidden = true;
1615 // Look for valid player slots
1616 if (playerSlot >= g_GameAttributes.settings.PlayerData.length)
1619 // If no human is assigned, look for an AI instead
1620 if (selection === undefined)
1622 let aiId = g_GameAttributes.settings.PlayerData[playerSlot].AI;
1625 // Check for a valid AI
1626 if (aiId in aiAssignments)
1628 selection = aiAssignments[aiId];
1629 configButton.hidden = false;
1630 configButton.onpress = function()
1632 openAIConfig(playerSlot);
1637 g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
1638 warn("AI \"" + aiId + "\" not present. Defaulting to unassigned.");
1643 selection = noAssignment;
1645 // There was a human, so make sure we don't have any AI left
1646 // over in their slot, if we're in charge of the attributes
1647 else if (g_IsController && g_GameAttributes.settings.PlayerData[playerSlot].AI)
1649 g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
1651 Engine.SetNetworkGameAttributes(g_GameAttributes);
1654 let assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]");
1655 let assignBoxText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]");
1656 assignBox.list = hostNameList;
1657 assignBox.list_data = hostGuidList;
1658 if (assignBox.selected != selection)
1659 assignBox.selected = selection;
1660 assignBoxText.caption = hostNameList[selection];
1663 assignBox.onselectionchange = function() {
1664 if (g_IsInGuiUpdate)
1667 let guid = hostGuidList[this.selected];
1671 // Unassign any host from this player slot
1672 Engine.AssignNetworkPlayer(playerID, "");
1673 // Remove AI from this player slot
1674 g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
1676 else if (guid.substr(0, 3) == "ai:")
1679 // Unassign any host from this player slot
1680 Engine.AssignNetworkPlayer(playerID, "");
1681 // Set the AI for this player slot
1682 g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3);
1685 swapPlayers(guid, playerSlot);
1688 Engine.SetNetworkGameAttributes(g_GameAttributes);
1695 g_IsInGuiUpdate = false;
1698 function swapPlayers(guid, newSlot)
1700 // Player slots are indexed from 0 as Gaia is omitted.
1701 let newPlayerID = newSlot + 1;
1702 let playerID = g_PlayerAssignments[guid].player;
1704 // Attempt to swap the player or AI occupying the target slot,
1705 // if any, into the slot this player is currently in.
1708 for (let guid in g_PlayerAssignments)
1710 // Move the player in the destination slot into the current slot.
1711 if (g_PlayerAssignments[guid].player != newPlayerID)
1715 Engine.AssignNetworkPlayer(playerID, guid);
1717 g_PlayerAssignments[guid].player = playerID;
1721 // Transfer the AI from the target slot to the current slot.
1722 g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI;
1724 // Swap civilizations if they aren't fixed
1725 if (g_GameAttributes.mapType != "scenario")
1727 [g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] =
1728 [g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ];
1733 Engine.AssignNetworkPlayer(newPlayerID, guid);
1735 g_PlayerAssignments[guid].player = newPlayerID;
1737 g_GameAttributes.settings.PlayerData[newSlot].AI = "";
1740 function submitChatInput()
1742 let input = Engine.GetGUIObjectByName("chatInput");
1743 let text = input.caption;
1749 if (executeNetworkCommand(text))
1752 Engine.SendNetworkChat(text);
1755 function senderFont(text)
1757 return '[font="' + g_SenderFont + '"]' + text + '[/font]';
1760 function systemMessage(message)
1762 return senderFont(sprintf(translate("== %(message)s"), { "message": message }));
1765 function colorizePlayernameByGUID(guid, username = "")
1767 // TODO: Maybe the host should have the moderator-prefix?
1769 username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
1770 let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
1772 let color = "white";
1775 color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
1777 // Enlighten playercolor to improve readability
1778 let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
1779 let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
1781 color = rgbToGuiColor({ "r": r, "g": g, "b": b });
1784 return '[color="'+ color +'"]' + username + '[/color]';
1787 function addChatMessage(msg)
1789 if (msg.type != "system" && msg.text)
1791 let userName = g_PlayerAssignments[Engine.GetPlayerGUID() || "local"].name;
1793 if (userName != g_PlayerAssignments[msg.guid].name)
1794 notifyUser(userName, msg.text);
1797 if (!g_FormatChatMessage[msg.type])
1800 let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
1802 let text = g_FormatChatMessage[msg.type](msg, user);
1804 if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true")
1805 text = sprintf(translate("%(time)s %(message)s"), {
1806 "time": sprintf(translate("\\[%(time)s]"), {
1807 "time": Engine.FormatMillisecondsIntoDateString(new Date().getTime(), translate("HH:mm"))
1812 g_ChatMessages.push(text);
1814 Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
1817 function showMoreOptions(show)
1819 Engine.GetGUIObjectByName("moreOptionsFade").hidden = !show;
1820 Engine.GetGUIObjectByName("moreOptions").hidden = !show;
1823 function resetCivilizations()
1825 for (let i in g_GameAttributes.settings.PlayerData)
1826 g_GameAttributes.settings.PlayerData[i].Civ = "random";
1828 updateGameAttributes();
1831 function resetTeams()
1833 for (let i in g_GameAttributes.settings.PlayerData)
1834 g_GameAttributes.settings.PlayerData[i].Team = -1;
1836 updateGameAttributes();
1839 function toggleReady()
1841 setReady(!g_IsReady);
1844 function setReady(ready, sendMessage = true)
1849 Engine.SendNetworkReady(+g_IsReady);
1854 let button = Engine.GetGUIObjectByName("startGame");
1856 button.caption = g_IsReady ?
1857 translate("I'm not ready!") :
1858 translate("I'm ready");
1860 button.tooltip = g_IsReady ?
1861 translate("State that you are not ready to play.") :
1862 translate("State that you are ready to play!");
1865 function updateReadyUI()
1870 let isAI = new Array(g_MaxPlayers + 1).fill(true);
1871 let allReady = true;
1872 for (let guid in g_PlayerAssignments)
1874 // We don't really care whether observers are ready.
1875 if (g_PlayerAssignments[guid].player == -1 || !g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
1877 let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1] : {};
1878 let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[g_PlayerAssignments[guid].player - 1] : {};
1879 isAI[g_PlayerAssignments[guid].player] = false;
1880 if (g_PlayerAssignments[guid].status || !g_IsNetworked)
1881 Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = '[color="' + g_ReadyColor + '"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]';
1884 Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = translate(getSetting(pData, pDefs, "Name"));
1889 // AIs are always ready.
1890 for (let playerid = 0; playerid < g_MaxPlayers; ++playerid)
1892 if (!g_GameAttributes.settings.PlayerData[playerid])
1894 let pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[playerid] : {};
1895 let pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[playerid] : {};
1896 if (isAI[playerid + 1])
1897 Engine.GetGUIObjectByName("playerName[" + playerid + "]").caption = '[color="' + g_ReadyColor + '"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]';
1900 // The host is not allowed to start until everyone is ready.
1901 if (g_IsNetworked && g_IsController)
1903 let startGameButton = Engine.GetGUIObjectByName("startGame");
1904 startGameButton.enabled = allReady;
1905 // Add a explanation on to the tooltip if disabled.
1906 let disabledIndex = startGameButton.tooltip.indexOf('Disabled');
1907 if (disabledIndex != -1 && allReady)
1908 startGameButton.tooltip = startGameButton.tooltip.substring(0, disabledIndex - 2);
1909 else if (disabledIndex == -1 && !allReady)
1910 startGameButton.tooltip = startGameButton.tooltip + " (Disabled until all players are ready)";
1914 function resetReadyData()
1919 if (g_ReadyChanged < 1)
1920 addChatMessage({ "type": "settings" });
1921 else if (g_ReadyChanged == 2 && !g_ReadyInit)
1922 return; // duplicate calls on init
1924 g_ReadyInit = false;
1929 else if (g_IsController)
1931 Engine.ClearAllPlayerReady();
1935 setReady(false, false);
1939 * Send a list of playernames and distinct between players and observers.
1940 * Don't send teams, AIs or anything else until the game was started.
1941 * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
1943 function formatClientsForStanza()
1945 let connectedPlayers = 0;
1946 let playerData = [];
1948 for (let guid in g_PlayerAssignments)
1950 let pData = { "Name": g_PlayerAssignments[guid].name };
1952 if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
1955 pData.Team = "observer";
1957 playerData.push(pData);
1961 "list": playerDataToStringifiedTeamList(playerData),
1962 "connectedPlayers": connectedPlayers
1967 * Send the relevant gamesettings to the lobbybot.
1969 function sendRegisterGameStanza()
1971 if (!g_IsController || !Engine.HasXmppClient())
1974 let selectedMapSize = Engine.GetGUIObjectByName("mapSize").selected;
1975 let selectedVictoryCondition = Engine.GetGUIObjectByName("victoryCondition").selected;
1977 let mapSize = g_GameAttributes.mapType == "random" ? Engine.GetGUIObjectByName("mapSize").list_data[selectedMapSize] : "Default";
1978 let victoryCondition = Engine.GetGUIObjectByName("victoryCondition").list[selectedVictoryCondition];
1979 let clients = formatClientsForStanza();
1982 "name": g_ServerName,
1983 "port": g_ServerPort,
1984 "mapName": g_GameAttributes.map,
1985 "niceMapName": getMapDisplayName(g_GameAttributes.map),
1987 "mapType": g_GameAttributes.mapType,
1988 "victoryCondition": victoryCondition,
1989 "nbp": clients.connectedPlayers,
1990 "maxnbp": g_GameAttributes.settings.PlayerData.length,
1991 "players": clients.list,
1994 // Only send the stanza if the relevant settings actually changed
1995 if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop]))
1998 g_LastGameStanza = stanza;
1999 Engine.SendRegisterGame(stanza);