Display the map difficulty in the gamedescription (gamesetup and objectives window...
[0ad.git] / binaries / data / mods / public / gui / gamesetup / gamesetup.js
blobf0d0cf7d67d7441484bbc85cbd377b505f0f0ab2
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_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
6 const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
7 const g_TriggerDifficulties = prepareForDropdown(g_Settings && g_Settings.TriggerDifficulties);
8 const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
9 const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
10 const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
11 const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
13 var g_GameSpeeds = getGameSpeedChoices(false);
15 /**
16  * Offer users to select playable civs only.
17  * Load unselectable civs as they could appear in scenario maps.
18  */
19 const g_CivData = loadCivData(false, false);
21 /**
22  * Store civilization code and page (structree or history) opened in civilization info.
23  */
24 var g_CivInfo = {
25         "code": "",
26         "page": "page_civinfo.xml"
29 /**
30  * Highlight the "random" dropdownlist item.
31  */
32 var g_ColorRandom = "orange";
34 /**
35  * Color for regular dropdownlist items.
36  */
37 var g_ColorRegular = "white";
39 /**
40  * Color for "Unassigned"-placeholder item in the dropdownlist.
41  */
42 var g_PlayerAssignmentColors = {
43         "player": g_ColorRegular,
44         "observer": "170 170 250",
45         "unassigned": "140 140 140",
46         "AI": "70 150 70"
49 /**
50  * Used for highlighting the sender of chat messages.
51  */
52 var g_SenderFont = "sans-bold-13";
54 /**
55  * This yields [1, 2, ..., MaxPlayers].
56  */
57 var g_NumPlayersList = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1);
59 /**
60  * Used for generating the botnames.
61  */
62 var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
64 var g_PlayerTeamList = prepareForDropdown([{
65                 "label": translateWithContext("team", "None"),
66                 "id": -1
67         }].concat(
68                 Array(g_MaxTeams).fill(0).map((v, i) => ({
69                         "label": i + 1,
70                         "id": i
71                 }))
72         )
75 /**
76  * Number of relics: [1, ..., NumCivs]
77  */
78 var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1);
80 var g_PlayerCivList = g_CivData && prepareForDropdown([{
81                 "name": translateWithContext("civilization", "Random"),
82                 "tooltip": translate("Picks one civilization at random when the game starts."),
83                 "color": g_ColorRandom,
84                 "code": "random"
85         }].concat(
86                 Object.keys(g_CivData).filter(
87                         civ => g_CivData[civ].SelectableInGameSetup
88                 ).map(civ => ({
89                         "name": g_CivData[civ].Name,
90                         "tooltip": g_CivData[civ].History,
91                         "color": g_ColorRegular,
92                         "code": civ
93                 })).sort(sortNameIgnoreCase)
94         )
97 /**
98  * All selectable playercolors except gaia.
99  */
100 var g_PlayerColorPickerList = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
103  * Directory containing all maps of the given type.
104  */
105 var g_MapPath = {
106         "random": "maps/random/",
107         "scenario": "maps/scenarios/",
108         "skirmish": "maps/skirmishes/"
112  * Containing the colors to highlight the ready status of players,
113  * the chat ready messages and
114  * the tooltips and captions for the ready button
115  */
117 var g_ReadyData = [
118         {
119                 "color": g_ColorRegular,
120                 "chat": translate("* %(username)s is not ready."),
121                 "caption": translate("I'm ready"),
122                 "tooltip": translate("State that you are ready to play.")
123         },
124         {
125                 "color": "green",
126                 "chat": translate("* %(username)s is ready!"),
127                 "caption": translate("Stay ready"),
128                 "tooltip": translate("Stay ready even when the game settings change.")
129         },
130         {
131                 "color": "150 150 250",
132                 "chat": "",
133                 "caption": translate("I'm not ready!"),
134                 "tooltip": translate("State that you are not ready to play.")
135         }
139  * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
140  */
141 var g_NetMessageTypes = {
142         "netstatus": msg => handleNetStatusMessage(msg),
143         "netwarn": msg => addNetworkWarning(msg),
144         "gamesetup": msg => handleGamesetupMessage(msg),
145         "players": msg => handlePlayerAssignmentMessage(msg),
146         "ready": msg => handleReadyMessage(msg),
147         "start": msg => handleGamestartMessage(msg),
148         "kicked": msg => addChatMessage({
149                 "type": msg.banned ? "banned" : "kicked",
150                 "username": msg.username
151         }),
152         "chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text }),
155 var g_FormatChatMessage = {
156         "system": (msg, user) => systemMessage(msg.text),
157         "settings": (msg, user) => systemMessage(translate('Game settings have been changed')),
158         "connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })),
159         "disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })),
160         "kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })),
161         "banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })),
162         "chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), {
163                 "username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })),
164                 "message": escapeText(msg.text || "")
165         }),
166         "ready": (msg, user) => sprintf(g_ReadyData[msg.status].chat, { "username": user }),
167         "clientlist": (msg, user) => getUsernameList(),
170 var g_MapFilters = [
171         {
172                 "id": "default",
173                 "name": translateWithContext("map filter", "Default"),
174                 "tooltip": translateWithContext("map filter", "All maps except naval and demo maps."),
175                 "filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1),
176                 "Default": true
177         },
178         {
179                 "id": "naval",
180                 "name": translate("Naval Maps"),
181                 "tooltip": translateWithContext("map filter", "Maps where ships are needed to reach the enemy."),
182                 "filter": mapKeywords => mapKeywords.indexOf("naval") != -1
183         },
184         {
185                 "id": "demo",
186                 "name": translate("Demo Maps"),
187                 "tooltip": translateWithContext("map filter", "These maps are not playable but for demonstration purposes only."),
188                 "filter": mapKeywords => mapKeywords.indexOf("demo") != -1
189         },
190         {
191                 "id": "new",
192                 "name": translate("New Maps"),
193                 "tooltip": translateWithContext("map filter", "Maps that are brand new in this release of the game."),
194                 "filter": mapKeywords => mapKeywords.indexOf("new") != -1
195         },
196         {
197                 "id": "trigger",
198                 "name": translate("Trigger Maps"),
199                 "tooltip": translateWithContext("map filter", "Maps that come with scripted events and potentially spawn enemy units."),
200                 "filter": mapKeywords => mapKeywords.indexOf("trigger") != -1
201         },
202         {
203                 "id": "all",
204                 "name": translate("All Maps"),
205                 "tooltip": translateWithContext("map filter", "Every map of the chosen maptype."),
206                 "filter": mapKeywords => true
207         },
211  * This contains only filters that have at least one map matching them.
212  */
213 var g_MapFilterList;
216  * Array of biome identifiers supported by the currently selected map.
217  */
218 var g_BiomeList;
221  * Array of trigger difficulties identifiers supported by the currently selected map.
222  */
223 var g_TriggerDifficultyList;
226  * Whether this is a single- or multiplayer match.
227  */
228 var g_IsNetworked;
231  * Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame).
232  */
233 var g_IsController;
236  * Whether this is a tutorial.
237  */
238 var g_IsTutorial;
241  * To report the game to the lobby bot.
242  */
243 var g_ServerName;
244 var g_ServerPort;
247  * IP address and port of the STUN endpoint.
248  */
249 var g_StunEndpoint;
252  * States whether the GUI is currently updated in response to network messages instead of user input
253  * and therefore shouldn't send further messages to the network.
254  */
255 var g_IsInGuiUpdate = false;
258  * Whether the current player is ready to start the game.
259  * 0 - not ready
260  * 1 - ready
261  * 2 - stay ready
262  */
263 var g_IsReady = 0;
266  * Ignore duplicate ready commands on init.
267  */
268 var g_ReadyInit = true;
271  * If noone has changed the ready status, we have no need to spam the settings changed message.
273  * <=0 - Suppressed settings message
274  * 1 - Will show settings message
275  * 2 - Host's initial ready, suppressed settings message
276  */
277 var g_ReadyChanged = 2;
280  * Used to prevent calling resetReadyData when starting a game.
281  */
282 var g_GameStarted = false;
285  * Selectable options (player, AI, unassigned) in the player assignment dropdowns and
286  * their colorized, textual representation.
287  */
288 var g_PlayerAssignmentList = {};
291  * Remembers which clients are assigned to which player slots and whether they are ready.
292  * The keys are guids or "local" in Singleplayer.
293  */
294 var g_PlayerAssignments = {};
296 var g_DefaultPlayerData = [];
298 var g_GameAttributes = { "settings": {} };
301  * List of translated words that can be used to autocomplete titles of settings
302  * and their values (for example playernames).
303  */
304 var g_Autocomplete = [];
307  * Array of strings formatted as displayed, including playername.
308  */
309 var g_ChatMessages = [];
312  * Minimum amount of pixels required for the chat panel to be visible.
313  */
314 var g_MinChatWidth = 96;
317  * Horizontal space between chat window and settings.
318  */
319 var g_ChatSettingsMargin = 10;
322  * Filename and translated title of all maps, given the currently selected
323  * maptype and filter. Sorted by title, shown in the dropdown.
324  */
325 var g_MapSelectionList = [];
328  * Cache containing the mapsettings. Just-in-time loading.
329  */
330 var g_MapData = {};
333  * Wait one tick before initializing the GUI objects and
334  * don't process netmessages prior to that.
335  */
336 var g_LoadingState = 0;
339  * Send the current gamesettings to the lobby bot if the settings didn't change for this number of seconds.
340  */
341 var g_GameStanzaTimeout = 2;
344  * Index of the GUI timer.
345  */
346 var g_GameStanzaTimer;
349  * Only send a lobby update if something actually changed.
350  */
351 var g_LastGameStanza;
354  * Remembers if the current player viewed the AI settings of some playerslot.
355  */
356 var g_LastViewedAIPlayer = -1;
359  * Total number of units that the engine can run with smoothly.
360  * It means a 4v4 with 150 population can still run nicely, but more than that might "lag".
361  */
362 var g_PopulationCapacityRecommendation = 1200;
365  * Horizontal space between tab buttons and lobby button.
366  */
367 var g_LobbyButtonSpacing = 8;
370  * Vertical size of a tab button.
371  */
372 var g_TabButtonHeight = 30;
375  * Vertical space between two tab buttons.
376  */
377 var g_TabButtonDist = 4;
380  * Vertical size of a setting object.
381  */
382 var g_SettingHeight = 32;
385  * Vertical space between two setting objects.
386  */
387 var g_SettingDist = 2;
390  * Maximum width of a column in the settings panel.
391  */
392 var g_MaxColumnWidth = 470;
395  * Pixels per millisecond the settings panel slides when opening/closing.
396  */
397 var g_SlideSpeed = 1.2;
400  * Store last tick time.
401  */
402 var g_LastTickTime = Date.now();
405  * Order in which the GUI elements will be shown.
406  * All valid settings are required to appear here.
407  */
408 var g_SettingsTabsGUI = [
409         {
410                 "label": translateWithContext("Match settings tab name", "Map"),
411                 "settings": [
412                         "mapType",
413                         "mapFilter",
414                         "mapSelection",
415                         "mapSize",
416                         "biome",
417                         "triggerDifficulty",
418                         "nomad",
419                         "disableTreasures",
420                         "exploreMap",
421                         "revealMap"
422                 ]
423         },
424         {
425                 "label": translateWithContext("Match settings tab name", "Player"),
426                 "settings": [
427                         "numPlayers",
428                         "populationCap",
429                         "startingResources",
430                         "disableSpies",
431                         "enableCheats"
432                 ]
433         },
434         {
435                 "label": translateWithContext("Match settings tab name", "Game Type"),
436                 "settings": [
437                         ...g_VictoryConditions.map(victoryCondition => victoryCondition.Name),
438                         "relicCount",
439                         "relicDuration",
440                         "wonderDuration",
441                         "regicideGarrison",
442                         "gameSpeed",
443                         "ceasefire",
444                         "lockTeams",
445                         "lastManStanding",
446                         "enableRating"
447                 ]
448         }
452  * Contains the logic of all multiple-choice gamesettings.
454  * Logic
455  * ids     - Array of identifier strings that indicate the selected value.
456  * default - Returns the index of the default value (not the value itself).
457  * defined - Whether a value for the setting is actually specified.
458  * get     - The identifier of the currently selected value.
459  * select  - Saves and processes the value of the selected index of the ids array.
461  * GUI
462  * title        - The caption shown in the label.
463  * tooltip      - A description shown when hovering the dropdown or a specific item.
464  * labels       - Array of translated strings selectable for this dropdown.
465  * colors       - Optional array of colors to tint the according dropdown items with.
466  * hidden       - If hidden, both the label and dropdown won't be visible. (default: false)
467  * enabled      - Only the label will be shown if the setting is disabled. (default: true)
468  * autocomplete - Marks whether to autocomplete translated values of the string. (default: undefined)
469  *                If not undefined, must be a number that denotes the priority (higher numbers come first).
470  *                If undefined, still autocompletes the translated title of the setting.
471  * initOrder    - Settings with lower values will be initialized first.
472  */
473 var g_Dropdowns = {
474         "mapType": {
475                 "title": () => translate("Map Type"),
476                 "tooltip": (hoverIdx) => g_MapTypes.Description[hoverIdx] || translate("Select a map type."),
477                 "labels": () => g_MapTypes.Title,
478                 "ids": () => g_MapTypes.Name,
479                 "default": () => g_MapTypes.Default,
480                 "defined": () => g_GameAttributes.mapType !== undefined,
481                 "get": () => g_GameAttributes.mapType,
482                 "select": (itemIdx) => {
483                         g_MapData = {};
484                         g_GameAttributes.mapType = g_MapTypes.Name[itemIdx];
485                         g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType];
486                         delete g_GameAttributes.map;
488                         if (g_GameAttributes.mapType != "scenario")
489                                 g_GameAttributes.settings = {
490                                         "PlayerData": clone(g_DefaultPlayerData.slice(0, 4))
491                                 };
493                         reloadMapFilterList();
494                 },
495                 "autocomplete": 0,
496                 "initOrder": 1
497         },
498         "mapFilter": {
499                 "title": () => translate("Map Filter"),
500                 "tooltip": (hoverIdx) => g_MapFilterList.tooltip[hoverIdx] || translate("Select a map filter."),
501                 "labels": () => g_MapFilterList.name,
502                 "ids": () => g_MapFilterList.id,
503                 "default": () => g_MapFilterList.Default,
504                 "defined": () => g_MapFilterList.id.indexOf(g_GameAttributes.mapFilter || "") != -1,
505                 "get": () => g_GameAttributes.mapFilter,
506                 "select": (itemIdx) => {
507                         g_GameAttributes.mapFilter = g_MapFilterList.id[itemIdx];
508                         delete g_GameAttributes.map;
509                         reloadMapList();
510                 },
511                 "autocomplete": 0,
512                 "initOrder": 2
513         },
514         "mapSelection": {
515                 "title": () => translate("Select Map"),
516                 "tooltip": (hoverIdx) => g_MapSelectionList.description[hoverIdx] || translate("Select a map to play on."),
517                 "labels": () => g_MapSelectionList.name,
518                 "colors": () => g_MapSelectionList.color,
519                 "ids": () => g_MapSelectionList.file,
520                 "default": () => 0,
521                 "defined": () => g_GameAttributes.map !== undefined,
522                 "get": () => g_GameAttributes.map,
523                 "select": (itemIdx) => {
524                         selectMap(g_MapSelectionList.file[itemIdx]);
525                 },
526                 "autocomplete": 0,
527                 "initOrder": 3
528         },
529         "mapSize": {
530                 "title": () => translate("Map Size"),
531                 "tooltip": (hoverIdx) => g_MapSizes.Tooltip[hoverIdx] || translate("Select map size. (Larger sizes may reduce performance.)"),
532                 "labels": () => g_MapSizes.Name,
533                 "ids": () => g_MapSizes.Tiles,
534                 "default": () => g_MapSizes.Default,
535                 "defined": () => g_GameAttributes.settings.Size !== undefined,
536                 "get": () => g_GameAttributes.settings.Size,
537                 "select": (itemIdx) => {
538                         g_GameAttributes.settings.Size = g_MapSizes.Tiles[itemIdx];
539                 },
540                 "hidden": () => g_GameAttributes.mapType != "random",
541                 "autocomplete": 0,
542                 "initOrder": 1000
543         },
544         "biome": {
545                 "title": () => translate("Biome"),
546                 "tooltip": (hoverIdx) => g_BiomeList && g_BiomeList.Description && g_BiomeList.Description[hoverIdx] || translate("Select the flora and fauna."),
547                 "labels": () => g_BiomeList ? g_BiomeList.Title : [],
548                 "colors": (itemIdx) => g_BiomeList ? g_BiomeList.Color : [],
549                 "ids": () => g_BiomeList ? g_BiomeList.Id : [],
550                 "default": () => 0,
551                 "defined": () => g_GameAttributes.settings.Biome !== undefined,
552                 "get": () => g_GameAttributes.settings.Biome,
553                 "select": (itemIdx) => {
554                         g_GameAttributes.settings.Biome = g_BiomeList && g_BiomeList.Id[itemIdx];
555                 },
556                 "hidden": () => !g_BiomeList,
557                 "autocomplete": 0,
558                 "initOrder": 1000
559         },
560         "numPlayers": {
561                 "title": () => translate("Number of Players"),
562                 "tooltip": (hoverIdx) => translate("Select number of players."),
563                 "labels": () => g_NumPlayersList,
564                 "ids": () => g_NumPlayersList,
565                 "default": () => g_MaxPlayers - 1,
566                 "defined": () => g_GameAttributes.settings.PlayerData !== undefined,
567                 "get": () => g_GameAttributes.settings.PlayerData.length,
568                 "enabled": () => g_GameAttributes.mapType == "random",
569                 "select": (itemIdx) => {
570                         let num = itemIdx + 1;
571                         let pData = g_GameAttributes.settings.PlayerData;
572                         g_GameAttributes.settings.PlayerData =
573                                 num > pData.length ?
574                                         pData.concat(clone(g_DefaultPlayerData.slice(pData.length, num))) :
575                                         pData.slice(0, num);
576                         unassignInvalidPlayers(num);
577                         sanitizePlayerData(g_GameAttributes.settings.PlayerData);
578                 },
579                 "initOrder": 1000
580         },
581         "populationCap": {
582                 "title": () => translate("Population Cap"),
583                 "tooltip": (hoverIdx) => {
585                         let popCap = g_PopulationCapacities.Population[hoverIdx];
586                         let players = g_GameAttributes.settings.PlayerData.length;
588                         if (hoverIdx == -1 || popCap * players <= g_PopulationCapacityRecommendation)
589                                 return translate("Select population limit.");
591                         return coloredText(
592                                 sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), {
593                                         "players": players,
594                                         "popCap": popCap
595                                 }),
596                                 "orange");
597                 },
598                 "labels": () => g_PopulationCapacities.Title,
599                 "ids": () => g_PopulationCapacities.Population,
600                 "default": () => g_PopulationCapacities.Default,
601                 "defined": () => g_GameAttributes.settings.PopulationCap !== undefined,
602                 "get": () => g_GameAttributes.settings.PopulationCap,
603                 "select": (itemIdx) => {
604                         g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx];
605                 },
606                 "enabled": () => g_GameAttributes.mapType != "scenario",
607                 "initOrder": 1000
608         },
609         "startingResources": {
610                 "title": () => translate("Starting Resources"),
611                 "tooltip": (hoverIdx) => {
612                         return hoverIdx >= 0 ?
613                                 sprintf(translate("Initial amount of each resource: %(resources)s."), {
614                                         "resources": g_StartingResources.Resources[hoverIdx]
615                                 }) :
616                                 translate("Select the game's starting resources.");
617                 },
618                 "labels": () => g_StartingResources.Title,
619                 "ids": () => g_StartingResources.Resources,
620                 "default": () => g_StartingResources.Default,
621                 "defined": () => g_GameAttributes.settings.StartingResources !== undefined,
622                 "get": () => g_GameAttributes.settings.StartingResources,
623                 "select": (itemIdx) => {
624                         g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx];
625                 },
626                 "hidden": () => g_GameAttributes.mapType == "scenario",
627                 "autocomplete": 0,
628                 "initOrder": 1000
629         },
630         "ceasefire": {
631                 "title": () => translate("Ceasefire"),
632                 "tooltip": (hoverIdx) => translate("Set time where no attacks are possible."),
633                 "labels": () => g_Ceasefire.Title,
634                 "ids": () => g_Ceasefire.Duration,
635                 "default": () => g_Ceasefire.Default,
636                 "defined": () => g_GameAttributes.settings.Ceasefire !== undefined,
637                 "get": () => g_GameAttributes.settings.Ceasefire,
638                 "select": (itemIdx) => {
639                         g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[itemIdx];
640                 },
641                 "enabled": () => g_GameAttributes.mapType != "scenario",
642                 "initOrder": 1000
643         },
644         "relicCount": {
645                 "title": () => translate("Relic Count"),
646                 "tooltip": (hoverIdx) => translate("Total number of relics spawned on the map. Relic victory is most realistic with only one or two relics. With greater numbers, the relics are important to capture to receive aura bonuses."),
647                 "labels": () => g_RelicCountList,
648                 "ids": () => g_RelicCountList,
649                 "default": () => g_RelicCountList.indexOf(2),
650                 "defined": () => g_GameAttributes.settings.RelicCount !== undefined,
651                 "get": () => g_GameAttributes.settings.RelicCount,
652                 "select": (itemIdx) => {
653                         g_GameAttributes.settings.RelicCount = g_RelicCountList[itemIdx];
654                 },
655                 "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
656                 "enabled": () => g_GameAttributes.mapType != "scenario",
657                 "initOrder": 1000
658         },
659         "relicDuration": {
660                 "title": () => translate("Relic Duration"),
661                 "tooltip": (hoverIdx) => translate("Minutes until the player has achieved Relic Victory."),
662                 "labels": () => g_VictoryDurations.Title,
663                 "ids": () => g_VictoryDurations.Duration,
664                 "default": () => g_VictoryDurations.Default,
665                 "defined": () => g_GameAttributes.settings.RelicDuration !== undefined,
666                 "get": () => g_GameAttributes.settings.RelicDuration,
667                 "select": (itemIdx) => {
668                         g_GameAttributes.settings.RelicDuration = g_VictoryDurations.Duration[itemIdx];
669                 },
670                 "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
671                 "enabled": () => g_GameAttributes.mapType != "scenario",
672                 "initOrder": 1000
673         },
674         "wonderDuration": {
675                 "title": () => translate("Wonder Duration"),
676                 "tooltip": (hoverIdx) => translate("Minutes until the player has achieved Wonder Victory."),
677                 "labels": () => g_VictoryDurations.Title,
678                 "ids": () => g_VictoryDurations.Duration,
679                 "default": () => g_VictoryDurations.Default,
680                 "defined": () => g_GameAttributes.settings.WonderDuration !== undefined,
681                 "get": () => g_GameAttributes.settings.WonderDuration,
682                 "select": (itemIdx) => {
683                         g_GameAttributes.settings.WonderDuration = g_VictoryDurations.Duration[itemIdx];
684                 },
685                 "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("wonder") == -1,
686                 "enabled": () => g_GameAttributes.mapType != "scenario",
687                 "initOrder": 1000
688         },
689         "gameSpeed": {
690                 "title": () => translate("Game Speed"),
691                 "tooltip": (hoverIdx) => translate("Select game speed."),
692                 "labels": () => g_GameSpeeds.Title,
693                 "ids": () => g_GameSpeeds.Speed,
694                 "default": () => g_GameSpeeds.Default,
695                 "defined": () =>
696                         g_GameAttributes.gameSpeed !== undefined &&
697                         g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) != -1,
698                 "get": () => g_GameAttributes.gameSpeed,
699                 "select": (itemIdx) => {
700                         g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[itemIdx];
701                 },
702                 "initOrder": 1000
703         },
704         "triggerDifficulty": {
705                 "title": () => translate("Difficulty"),
706                 "tooltip": (hoverIdx) => g_TriggerDifficultyList && g_TriggerDifficultyList.Description[hoverIdx] ||
707                         translate("Select the difficulty of this scenario."),
708                 "labels": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Title : [],
709                 "ids": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Id : [],
710                 "default": () => g_TriggerDifficultyList ? g_TriggerDifficultyList.Default : 0,
711                 "defined": () => g_GameAttributes.settings.TriggerDifficulty !== undefined,
712                 "get": () => g_GameAttributes.settings.TriggerDifficulty,
713                 "select": (itemIdx) => {
714                         g_GameAttributes.settings.TriggerDifficulty = g_TriggerDifficultyList && g_TriggerDifficultyList.Id[itemIdx];
715                 },
716                 "hidden": () => !g_TriggerDifficultyList,
717                 "initOrder": 1000
718         },
722  * These dropdowns provide a setting that is repeated once for each player
723  * (where playerIdx is starting from 0 for player 1).
724  */
725 var g_PlayerDropdowns = {
726         "playerAssignment": {
727                 "labels": (playerIdx) => g_PlayerAssignmentList.Name || [],
728                 "colors": (playerIdx) => g_PlayerAssignmentList.Color || [],
729                 "ids": (playerIdx) => g_PlayerAssignmentList.Choice || [],
730                 "default": (playerIdx) => "ai:petra",
731                 "defined": (playerIdx) => playerIdx < g_GameAttributes.settings.PlayerData.length,
732                 "get": (playerIdx) => {
733                         for (let guid in g_PlayerAssignments)
734                                 if (g_PlayerAssignments[guid].player == playerIdx + 1)
735                                         return "guid:" + guid;
737                         for (let ai of g_Settings.AIDescriptions)
738                                 if (g_GameAttributes.settings.PlayerData[playerIdx].AI == ai.id)
739                                         return "ai:" + ai.id;
741                         return "unassigned";
742                 },
743                 "select": (selectedIdx, playerIdx) => {
745                         let choice = g_PlayerAssignmentList.Choice[selectedIdx];
746                         if (choice == "unassigned" || choice.startsWith("ai:"))
747                         {
748                                 if (g_IsNetworked)
749                                         Engine.AssignNetworkPlayer(playerIdx+1, "");
750                                 else if (g_PlayerAssignments.local.player == playerIdx+1)
751                                         g_PlayerAssignments.local.player = -1;
753                                 g_GameAttributes.settings.PlayerData[playerIdx].AI = choice.startsWith("ai:") ? choice.substr(3) : "";
754                         }
755                         else
756                                 swapPlayers(choice.substr("guid:".length), playerIdx);
757                 },
758                 "autocomplete": 100,
759         },
760         "playerTeam": {
761                 "labels": (playerIdx) => g_PlayerTeamList.label,
762                 "ids": (playerIdx) => g_PlayerTeamList.id,
763                 "default": (playerIdx) => 0,
764                 "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team !== undefined,
765                 "get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team,
766                 "select": (selectedIdx, playerIdx) => {
767                         g_GameAttributes.settings.PlayerData[playerIdx].Team = selectedIdx - 1;
768                 },
769                 "enabled": () => g_GameAttributes.mapType != "scenario",
770         },
771         "playerCiv": {
772                 "tooltip": (hoverIdx, playerIdx) => g_PlayerCivList.tooltip[hoverIdx] || translate("Chose the civilization for this player"),
773                 "labels": (playerIdx) => g_PlayerCivList.name,
774                 "colors": (playerIdx) => g_PlayerCivList.color,
775                 "ids": (playerIdx) => g_PlayerCivList.code,
776                 "default": (playerIdx) => 0,
777                 "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ !== undefined,
778                 "get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ,
779                 "select": (selectedIdx, playerIdx) => {
780                         g_GameAttributes.settings.PlayerData[playerIdx].Civ = g_PlayerCivList.code[selectedIdx];
781                 },
782                 "enabled": () => g_GameAttributes.mapType != "scenario",
783                 "autocomplete": 0,
784         },
785         "playerColorPicker": {
786                 "labels": (playerIdx) => g_PlayerColorPickerList.map(color => "â– "),
787                 "colors": (playerIdx) => g_PlayerColorPickerList.map(color => rgbToGuiColor(color)),
788                 "ids": (playerIdx) => g_PlayerColorPickerList.map((color, index) => index),
789                 "default": (playerIdx) => playerIdx,
790                 "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Color !== undefined,
791                 "get": (playerIdx) => g_PlayerColorPickerList.indexOf(g_GameAttributes.settings.PlayerData[playerIdx].Color),
792                 "select": (selectedIdx, playerIdx) => {
793                         let playerData = g_GameAttributes.settings.PlayerData;
795                         // If someone else has that color, give that player the old color
796                         let sameColorPData = playerData.find(pData => sameColor(g_PlayerColorPickerList[selectedIdx], pData.Color));
797                         if (sameColorPData)
798                                 sameColorPData.Color = playerData[playerIdx].Color;
800                         playerData[playerIdx].Color = g_PlayerColorPickerList[selectedIdx];
801                         ensureUniquePlayerColors(playerData);
802                 },
803                 "enabled": () => g_GameAttributes.mapType != "scenario",
804         },
808  * Contains the logic of all boolean gamesettings.
809  */
810 var g_Checkboxes = Object.assign(
811         {},
812         g_VictoryConditions.reduce((obj, victoryCondition) => {
813                 obj[victoryCondition.Name] = {
814                         "title": () => victoryCondition.Title,
815                         "tooltip": () => victoryCondition.Description,
816                         // Defaults are set in supplementDefault directly from g_VictoryConditions since we use an array
817                         "defined": () => true,
818                         "get": () => g_GameAttributes.settings.VictoryConditions.indexOf(victoryCondition.Name) != -1,
819                         "set": checked => {
820                                 if (checked)
821                                 {
822                                         g_GameAttributes.settings.VictoryConditions.push(victoryCondition.Name);
823                                         if (victoryCondition.ChangeOnChecked)
824                                                 for (let setting in victoryCondition.ChangeOnChecked)
825                                                         g_Checkboxes[setting].set(victoryCondition.ChangeOnChecked[setting]);
826                                 }
827                                 else
828                                         g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions.filter(victoryConditionName => victoryConditionName != victoryCondition.Name);
829                         },
830                         "enabled": () =>
831                                 g_GameAttributes.mapType != "scenario" &&
832                                 (!victoryCondition.DisabledWhenChecked ||
833                                 victoryCondition.DisabledWhenChecked.every(victoryConditionName => g_GameAttributes.settings.VictoryConditions.indexOf(victoryConditionName) == -1))
834                 };
835                 return obj;
836         }, {}),
837         {
838                 "regicideGarrison": {
839                         "title": () => translate("Hero Garrison"),
840                         "tooltip": () => translate("Toggle whether heroes can be garrisoned."),
841                         "default": () => false,
842                         "defined": () => g_GameAttributes.settings.RegicideGarrison !== undefined,
843                         "get": () => g_GameAttributes.settings.RegicideGarrison,
844                         "set": checked => {
845                                 g_GameAttributes.settings.RegicideGarrison = checked;
846                         },
847                         "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("regicide") == -1,
848                         "enabled": () => g_GameAttributes.mapType != "scenario",
849                         "initOrder": 1000
850                 },
851                 "nomad": {
852                         "title": () => translate("Nomad"),
853                         "tooltip": () => translate("In Nomad mode, players start with only few units and have to find a suitable place to build their city. Ceasefire is recommended."),
854                         "default": () => false,
855                         "defined": () => g_GameAttributes.settings.Nomad !== undefined,
856                         "get": () => g_GameAttributes.settings.Nomad,
857                         "set": checked => {
858                                 g_GameAttributes.settings.Nomad = checked;
859                         },
860                         "hidden": () => g_GameAttributes.mapType != "random",
861                         "initOrder": 1000
862                 },
863                 "revealMap": {
864                         "title": () =>
865                                 // Translation: Make sure to differentiate between the revealed map and explored map settings!
866                                 translate("Revealed Map"),
867                         "tooltip":
868                                 // Translation: Make sure to differentiate between the revealed map and explored map settings!
869                                 () => translate("Toggle revealed map (see everything)."),
870                         "default": () => false,
871                         "defined": () => g_GameAttributes.settings.RevealMap !== undefined,
872                         "get": () => g_GameAttributes.settings.RevealMap,
873                         "set": checked => {
874                                 g_GameAttributes.settings.RevealMap = checked;
876                                 if (checked)
877                                         g_Checkboxes.exploreMap.set(true);
878                         },
879                         "enabled": () => g_GameAttributes.mapType != "scenario",
880                         "initOrder": 1000
881                 },
882                 "exploreMap": {
883                         "title":
884                                 // Translation: Make sure to differentiate between the revealed map and explored map settings!
885                                 () => translate("Explored Map"),
886                         "tooltip":
887                                 // Translation: Make sure to differentiate between the revealed map and explored map settings!
888                                 () => translate("Toggle explored map (see initial map)."),
889                         "default": () => false,
890                         "defined": () => g_GameAttributes.settings.ExploreMap !== undefined,
891                         "get": () => g_GameAttributes.settings.ExploreMap,
892                         "set": checked => {
893                                 g_GameAttributes.settings.ExploreMap = checked;
894                         },
895                         "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap,
896                         "initOrder": 1000
897                 },
898                 "disableTreasures": {
899                         "title": () => translate("Disable Treasures"),
900                         "tooltip": () => translate("Disable all treasures on the map."),
901                         "default": () => false,
902                         "defined": () => g_GameAttributes.settings.DisableTreasures !== undefined,
903                         "get": () => g_GameAttributes.settings.DisableTreasures,
904                         "set": checked => {
905                                 g_GameAttributes.settings.DisableTreasures = checked;
906                         },
907                         "enabled": () => g_GameAttributes.mapType != "scenario",
908                         "initOrder": 1000
909                 },
910                 "disableSpies": {
911                         "title": () => translate("Disable Spies"),
912                         "tooltip": () => translate("Disable spies during the game."),
913                         "default": () => false,
914                         "defined": () => g_GameAttributes.settings.DisableSpies !== undefined,
915                         "get": () => g_GameAttributes.settings.DisableSpies,
916                         "set": checked => {
917                                 g_GameAttributes.settings.DisableSpies = checked;
918                         },
919                         "enabled": () => g_GameAttributes.mapType != "scenario",
920                         "initOrder": 1000
921                 },
922                 "lockTeams": {
923                         "title": () => translate("Teams Locked"),
924                         "tooltip": () => translate("Toggle locked teams."),
925                         "default": () => Engine.HasXmppClient(),
926                         "defined": () => g_GameAttributes.settings.LockTeams !== undefined,
927                         "get": () => g_GameAttributes.settings.LockTeams,
928                         "set": checked => {
929                                 g_GameAttributes.settings.LockTeams = checked;
930                                 g_GameAttributes.settings.LastManStanding = false;
931                         },
932                         "enabled": () =>
933                                 g_GameAttributes.mapType != "scenario" &&
934                                 !g_GameAttributes.settings.RatingEnabled,
935                         "initOrder": 1000
936                 },
937                 "lastManStanding": {
938                         "title": () => translate("Last Man Standing"),
939                         "tooltip": () => translate("Toggle whether the last remaining player or the last remaining set of allies wins."),
940                         "default": () => false,
941                         "defined": () => g_GameAttributes.settings.LastManStanding !== undefined,
942                         "get": () => g_GameAttributes.settings.LastManStanding,
943                         "set": checked => {
944                                 g_GameAttributes.settings.LastManStanding = checked;
945                         },
946                         "enabled": () =>
947                                 g_GameAttributes.mapType != "scenario" &&
948                                 !g_GameAttributes.settings.LockTeams,
949                         "initOrder": 1000
950                 },
951                 "enableCheats": {
952                         "title": () => translate("Cheats"),
953                         "tooltip": () => translate("Toggle the usability of cheats."),
954                         "default": () => !g_IsNetworked,
955                         "hidden": () => !g_IsNetworked,
956                         "defined": () => g_GameAttributes.settings.CheatsEnabled !== undefined,
957                         "get": () => g_GameAttributes.settings.CheatsEnabled,
958                         "set": checked => {
959                                 g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked ||
960                                         checked && !g_GameAttributes.settings.RatingEnabled;
961                         },
962                         "enabled": () => !g_GameAttributes.settings.RatingEnabled,
963                         "initOrder": 1000
964                 },
965                 "enableRating": {
966                         "title": () => translate("Rated Game"),
967                         "tooltip": () => translate("Toggle if this game will be rated for the leaderboard."),
968                         "default": () => Engine.HasXmppClient(),
969                         "hidden": () => !Engine.HasXmppClient(),
970                         "defined": () => g_GameAttributes.settings.RatingEnabled !== undefined,
971                         "get": () => !!g_GameAttributes.settings.RatingEnabled,
972                         "set": checked => {
973                                 g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined;
974                                 Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
975                                 if (checked)
976                                 {
977                                         g_Checkboxes.lockTeams.set(true);
978                                         g_Checkboxes.enableCheats.set(false);
979                                 }
980                         },
981                         "initOrder": 1000
982                 },
983         }
987  * For setting up arbitrary GUI objects.
988  */
989 var g_MiscControls = {
990         "chatPanel": {
991                 "hidden": () => {
992                         if (!g_IsNetworked)
993                                 return true;
995                         let size = Engine.GetGUIObjectByName("chatPanel").getComputedSize();
996                         return size.right - size.left < g_MinChatWidth;
997                 },
998         },
999         "chatInput": {
1000                 "tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete playernames or settings.")),
1001         },
1002         "cheatWarningText": {
1003                 "hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled,
1004         },
1005         "cancelGame": {
1006                 "tooltip": () =>
1007                         Engine.HasXmppClient() ?
1008                                 translate("Return to the lobby.") :
1009                                 translate("Return to the main menu."),
1010         },
1011         "startGame": {
1012                 "caption": () =>
1013                         g_IsController ? translate("Start Game!") : g_ReadyData[g_IsReady].caption,
1014                 "tooltip": (hoverIdx) =>
1015                         !g_IsController ?
1016                                 g_ReadyData[g_IsReady].tooltip :
1017                                 !g_IsNetworked || Object.keys(g_PlayerAssignments).every(guid =>
1018                                         g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1) ?
1019                                         translate("Start a new game with the current settings.") :
1020                                         translate("Start a new game with the current settings (disabled until all players are ready)"),
1021                 "enabled": () => !g_IsController ||
1022                                  Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status ||
1023                                                                                 g_PlayerAssignments[guid].player == -1 ||
1024                                                                                 guid == Engine.GetPlayerGUID() && g_IsController),
1025                 "hidden": () =>
1026                         !g_PlayerAssignments[Engine.GetPlayerGUID()] ||
1027                         g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1 && !g_IsController,
1028         },
1029         "civResetButton": {
1030                 "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
1031         },
1032         "teamResetButton": {
1033                 "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
1034         },
1035         "lobbyButton": {
1036                 "onPress": () => function() {
1037                         if (Engine.HasXmppClient())
1038                                 Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
1039                 },
1040                 "hidden": () => !Engine.HasXmppClient()
1041         },
1042         "spTips": {
1043                 "hidden": () => {
1044                         let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
1045                         let spTips = Engine.GetGUIObjectByName("spTips");
1046                         return g_IsNetworked ||
1047                                 Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true" ||
1048                                 spTips.size.right > settingsPanel.getComputedSize().left;
1049                 }
1050         }
1054  * Contains gui elements that are repeated for every player.
1055  */
1056 var g_PlayerMiscElements = {
1057         "playerBox": {
1058                 "size": (playerIdx) => ["0", 32 * playerIdx, "100%", 32 * (playerIdx + 1)].join(" "),
1059         },
1060         "playerName": {
1061                 "caption": (playerIdx) => {
1062                         let pData = g_GameAttributes.settings.PlayerData[playerIdx];
1064                         let assignedGUID = Object.keys(g_PlayerAssignments).find(
1065                                 guid => g_PlayerAssignments[guid].player == playerIdx + 1);
1067                         let name = translate(pData.Name || g_DefaultPlayerData[playerIdx].Name);
1069                         if (g_IsNetworked)
1070                                 name = coloredText(name, g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color);
1072                         return name;
1073                 },
1074         },
1075         "playerColor": {
1076                 "sprite": (playerIdx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[playerIdx].Color, 100),
1077         },
1078         "playerConfig": {
1079                 "hidden": (playerIdx) => !g_GameAttributes.settings.PlayerData[playerIdx].AI,
1080                 "onPress": (playerIdx) => function() {
1081                         openAIConfig(playerIdx);
1082                 },
1083                 "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(description)s."), {
1084                         "description": translateAISettings(g_GameAttributes.settings.PlayerData[playerIdx])
1085                 }),
1086         },
1090  * Initializes some globals without touching the GUI.
1092  * @param {Object} attribs - context data sent by the lobby / mainmenu
1093  */
1094 function init(attribs)
1096         if (!g_Settings)
1097         {
1098                 cancelSetup();
1099                 return;
1100         }
1102         if (["offline", "server", "client"].indexOf(attribs.type) == -1)
1103         {
1104                 error("Unexpected 'type' in gamesetup init: " + attribs.type);
1105                 cancelSetup();
1106                 return;
1107         }
1109         g_IsNetworked = attribs.type != "offline";
1110         g_IsController = attribs.type != "client";
1111         g_IsTutorial = !!attribs.tutorial;
1112         g_ServerName = attribs.serverName;
1113         g_ServerPort = attribs.serverPort;
1114         g_StunEndpoint = attribs.stunEndpoint;
1116         if (!g_IsNetworked)
1117                 g_PlayerAssignments = {
1118                         "local": {
1119                                 "name": singleplayerName(),
1120                                 "player": 1
1121                         }
1122                 };
1124         // Replace empty playername when entering a singleplayermatch for the first time
1125         if (!g_IsNetworked)
1126                 saveSettingAndWriteToUserConfig("playername.singleplayer", singleplayerName());
1128         initDefaults();
1129         supplementDefaults();
1131         setTimeout(displayGamestateNotifications, 1000);
1133         Engine.GetGUIObjectByName("civInfoButton").tooltip = sprintf(
1134                 translate("%(hotkey_civinfo)s / %(hotkey_structree)s: View History / Structure Tree\nLast opened will be reopened on click."), {
1135                         "hotkey_civinfo": colorizeHotkey("%(hotkey)s", "civinfo"),
1136                         "hotkey_structree": colorizeHotkey("%(hotkey)s", "structree")
1137         });
1140 function initDefaults()
1142         // Remove gaia from both arrays
1143         g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1));
1145         let aiDifficulty = +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty");
1146         let aiBehavior = Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior");
1148         // Don't change the underlying defaults file, as Atlas uses that file too
1149         for (let i in g_DefaultPlayerData)
1150         {
1151                 g_DefaultPlayerData[i].Civ = "random";
1152                 g_DefaultPlayerData[i].Team = -1;
1153                 g_DefaultPlayerData[i].AIDiff = aiDifficulty;
1154                 g_DefaultPlayerData[i].AIBehavior = aiBehavior;
1155         }
1157         deepfreeze(g_DefaultPlayerData);
1161  * Sets default values for all g_GameAttribute settings which don't have a value set.
1162  */
1163 function supplementDefaults()
1165         g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions ||
1166                 g_VictoryConditions.filter(victoryCondition => !!victoryCondition.Default).map(victoryCondition => victoryCondition.Name);
1168         for (let dropdown in g_Dropdowns)
1169                 if (!g_Dropdowns[dropdown].defined())
1170                         g_Dropdowns[dropdown].select(g_Dropdowns[dropdown].default());
1172         for (let checkbox in g_Checkboxes)
1173                 if (!g_Checkboxes[checkbox].defined())
1174                         g_Checkboxes[checkbox].set(g_Checkboxes[checkbox].default());
1176         for (let dropdown in g_PlayerDropdowns)
1177                 for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
1178                         if (!isControlArrayElementHidden(i) && !g_PlayerDropdowns[dropdown].defined(i))
1179                                 g_PlayerDropdowns[dropdown].select(g_PlayerDropdowns[dropdown].default(i), i);
1183  * Called after the first tick.
1184  */
1185 function initGUIObjects()
1187         for (let tab in g_SettingsTabsGUI)
1188                 g_SettingsTabsGUI[tab].tooltip =
1189                         sprintf(translate("Toggle the %(name)s settings tab."), { "name": g_SettingsTabsGUI[tab].label }) +
1190                         colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab down."), "tab.next") +
1191                         colorizeHotkey("\n" + translate("Use %(hotkey)s to move a settings tab up."), "tab.prev");
1193         // Copy all initOrder values into one object
1194         let initOrder = {};
1195         for (let dropdown in g_Dropdowns)
1196                 initOrder[dropdown] = g_Dropdowns[dropdown].initOrder;
1197         for (let checkbox in g_Checkboxes)
1198                 initOrder[checkbox] = g_Checkboxes[checkbox].initOrder;
1200         // Sort the object on initOrder so we can init the settings in an arbitrary order
1201         for (let setting of Object.keys(initOrder).sort((a, b) => initOrder[a] - initOrder[b]))
1202                 if (g_Dropdowns[setting])
1203                         initDropdown(setting);
1204                 else if (g_Checkboxes[setting])
1205                         initCheckbox(setting);
1206                 else
1207                         warn('The setting "' + setting + '" is not defined.');
1209         for (let dropdown in g_PlayerDropdowns)
1210                 initPlayerDropdowns(dropdown);
1212         let settingTabButtons = Engine.GetGUIObjectByName("settingTabButtons");
1213         let settingTabButtonsSize = settingTabButtons.size;
1214         settingTabButtonsSize.bottom = settingTabButtonsSize.top + g_SettingsTabsGUI.length * (g_TabButtonHeight + g_TabButtonDist);
1215         settingTabButtonsSize.right = g_MiscControls.lobbyButton.hidden() ?
1216                 settingTabButtonsSize.right :
1217                 Engine.GetGUIObjectByName("lobbyButton").size.left - g_LobbyButtonSpacing;
1218         settingTabButtons.size = settingTabButtonsSize;
1220         let settingTabButtonsBackground = Engine.GetGUIObjectByName("settingTabButtonsBackground");
1221         settingTabButtonsBackground.size = settingTabButtonsSize;
1223         let gameDescription = Engine.GetGUIObjectByName("mapInfoDescriptionFrame");
1224         let gameDescriptionSize = gameDescription.size;
1225         gameDescriptionSize.top = settingTabButtonsSize.bottom + 3;
1226         gameDescription.size = gameDescriptionSize;
1228         placeTabButtons(
1229                 g_SettingsTabsGUI,
1230                 g_TabButtonHeight,
1231                 g_TabButtonDist,
1232                 category => {
1233                         selectPanel(category == g_TabCategorySelected ? undefined : category);
1234                 },
1235                 () => {
1236                         updateGUIObjects();
1237                         Engine.GetGUIObjectByName("settingsPanel").hidden = false;
1238                 });
1240         initSPTips();
1242         loadPersistMatchSettings();
1243         updateGameAttributes();
1244         sendRegisterGameStanzaImmediate();
1246         if (g_IsTutorial)
1247         {
1248                 launchTutorial();
1249                 return;
1250         }
1252         // Don't lift the curtain until the controls are updated the first time
1253         if (!g_IsNetworked)
1254                 hideLoadingWindow();
1258  * Slide settings panel.
1259  * @param {number} dt - Time in milliseconds since last call.
1260  */
1261 function updateSettingsPanelPosition(dt)
1263         let slideSpeed = Engine.ConfigDB_GetValue("user", "gui.gamesetup.settingsslide") == "true" ? g_SlideSpeed : Infinity;
1265         let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
1266         let rightBorder = Engine.GetGUIObjectByName("settingTabButtons").size.left;
1267         let offset = 0;
1268         if (g_TabCategorySelected === undefined)
1269         {
1270                 let maxOffset = rightBorder - settingsPanel.size.left;
1271                 if (maxOffset > 0)
1272                         offset = Math.min(slideSpeed * dt, maxOffset);
1273         }
1274         else if (rightBorder > settingsPanel.size.right)
1275                 offset = Math.min(slideSpeed * dt, rightBorder - settingsPanel.size.right);
1276         else
1277         {
1278                 let maxOffset = settingsPanel.size.right - rightBorder;
1279                 if (maxOffset > 0)
1280                         offset = -Math.min(slideSpeed * dt, maxOffset);
1281         }
1283         let size = settingsPanel.size;
1284         size.left += offset;
1285         size.right += offset;
1286         settingsPanel.size = size;
1288         let settingsBackground = Engine.GetGUIObjectByName("settingsBackground");
1289         let backgroundSize = settingsBackground.size;
1290         backgroundSize.left = size.left;
1291         settingsBackground.size = backgroundSize;
1293         let chatPanel = Engine.GetGUIObjectByName("chatPanel");
1294         let chatSize = chatPanel.size;
1296         chatSize.right = size.left - g_ChatSettingsMargin;
1297         chatPanel.size = chatSize;
1298         chatPanel.hidden = g_MiscControls.chatPanel.hidden();
1300         let spTips = Engine.GetGUIObjectByName("spTips");
1301         spTips.hidden = g_MiscControls.spTips.hidden();
1304 function hideLoadingWindow()
1306         let loadingWindow = Engine.GetGUIObjectByName("loadingWindow");
1307         if (loadingWindow.hidden)
1308                 return;
1310         loadingWindow.hidden = true;
1311         Engine.GetGUIObjectByName("setupWindow").hidden = false;
1313         if (!Engine.GetGUIObjectByName("chatPanel").hidden)
1314                 Engine.GetGUIObjectByName("chatInput").focus();
1318  * Settings under the settings tabs use a generic name.
1319  * Player settings use custom names.
1320  */
1321 function getGUIObjectNameFromSetting(setting)
1323         let idxOffset = 0;
1324         for (let category of g_SettingsTabsGUI)
1325         {
1326                 let idx = category.settings.indexOf(setting);
1327                 if (idx != -1)
1328                         return [
1329                                 "setting",
1330                                 g_Dropdowns[setting] ? "Dropdown" : "Checkbox",
1331                                 "[" + (idx + idxOffset) + "]"
1332                         ];
1333                 idxOffset += category.settings.length;
1334         }
1336         // Assume there is a GUI object with exactly that setting name
1337         return [setting, "", ""];
1340 function initDropdown(name, playerIdx)
1342         let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
1343         let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
1344         let data = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
1346         let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
1348         dropdown.list = data.labels(playerIdx).map((label, id) =>
1349                 data.colors && data.colors(playerIdx) ?
1350                         coloredText(label, data.colors(playerIdx)[id]) :
1351                         label);
1353         dropdown.list_data = data.ids(playerIdx);
1355         dropdown.onSelectionChange = function() {
1357                 if (!g_IsController ||
1358                     g_IsInGuiUpdate ||
1359                     !this.list_data[this.selected] ||
1360                     data.hidden && data.hidden(playerIdx) ||
1361                     data.enabled && !data.enabled(playerIdx))
1362                         return;
1364                 data.select(this.selected, playerIdx);
1366                 supplementDefaults();
1367                 updateGameAttributes();
1368         };
1370         if (data.tooltip)
1371                 dropdown.onHoverChange = function() {
1372                         this.tooltip = data.tooltip(this.hovered, playerIdx);
1373                 };
1376 function initPlayerDropdowns(name)
1378         for (let i = 0; i < g_MaxPlayers; ++i)
1379                 initDropdown(name, i);
1382 function initCheckbox(name)
1384         let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
1385         Engine.GetGUIObjectByName(guiName + guiType + guiIdx).onPress = function() {
1387                 let obj = g_Checkboxes[name];
1389                 if (!g_IsController ||
1390                     g_IsInGuiUpdate ||
1391                     obj.enabled && !obj.enabled() ||
1392                     obj.hidden && obj.hidden())
1393                         return;
1395                 obj.set(this.checked);
1397                 supplementDefaults();
1398                 updateGameAttributes();
1399         };
1402 function initSPTips()
1404         if (g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true")
1405                 return;
1407         Engine.GetGUIObjectByName("spTips").hidden = false;
1408         Engine.GetGUIObjectByName("displaySPTips").checked = true;
1409         Engine.GetGUIObjectByName("aiTips").caption = Engine.TranslateLines(Engine.ReadFile("gui/gamesetup/ai.txt"));
1413  * Distribute the currently visible settings over the settings panel.
1414  * First calculate the number of columns required, then place the objects.
1415  */
1416 function distributeSettings()
1418         let setupWindowSize = Engine.GetGUIObjectByName("setupWindow").getComputedSize();
1419         let columnWidth = Math.min(
1420                 g_MaxColumnWidth,
1421                 (setupWindowSize.right - setupWindowSize.left + Engine.GetGUIObjectByName("settingTabButtons").size.left) / 2);
1423         let settingsPanel = Engine.GetGUIObjectByName("settingsPanel");
1424         let actualSettingsPanelSize = settingsPanel.getComputedSize();
1426         let maxPerColumn = Math.floor((actualSettingsPanelSize.bottom - actualSettingsPanelSize.top) / g_SettingHeight);
1427         let childCount = settingsPanel.children.filter(child => !child.hidden).length;
1428         let perColumn = childCount / Math.ceil(childCount / maxPerColumn);
1430         let yPos = g_SettingDist;
1431         let column = 0;
1432         let thisColumn = 0;
1433         let settingsPanelSize = settingsPanel.size;
1434         for (let child of settingsPanel.children)
1435         {
1436                 if (child.hidden)
1437                         continue;
1439                 if (thisColumn >= perColumn)
1440                 {
1441                         yPos = g_SettingDist;
1442                         ++column;
1443                         thisColumn = 0;
1444                 }
1446                 let childSize = child.size;
1447                 child.size = new GUISize(
1448                         column * columnWidth,
1449                         yPos,
1450                         column * columnWidth + columnWidth - 10,
1451                         yPos + g_SettingHeight - g_SettingDist);
1453                 yPos += g_SettingHeight;
1454                 ++thisColumn;
1455         }
1457         settingsPanelSize.right = settingsPanelSize.left + (column + 1) * columnWidth;
1458         settingsPanel.size = settingsPanelSize;
1462  * Called when the client disconnects.
1463  * The other cases from NetClient should never occur in the gamesetup.
1464  */
1465 function handleNetStatusMessage(message)
1467         if (message.status != "disconnected")
1468         {
1469                 error("Unrecognised netstatus type " + message.status);
1470                 return;
1471         }
1473         cancelSetup();
1474         reportDisconnect(message.reason, true);
1478  * Called whenever a client clicks on ready (or not ready).
1479  */
1480 function handleReadyMessage(message)
1482         --g_ReadyChanged;
1484         if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
1485                 addChatMessage({
1486                         "type": "ready",
1487                         "status": message.status,
1488                         "guid": message.guid
1489                 });
1491         g_PlayerAssignments[message.guid].status = message.status;
1492         updateGUIObjects();
1496  * Called after every player is ready and the host decided to finally start the game.
1497  */
1498 function handleGamestartMessage(message)
1500         // Immediately inform the lobby server instead of waiting for the load to finish
1501         if (g_IsController && Engine.HasXmppClient())
1502         {
1503                 sendRegisterGameStanzaImmediate();
1504                 let clients = formatClientsForStanza();
1505                 Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
1506         }
1508         Engine.SwitchGuiPage("page_loading.xml", {
1509                 "attribs": g_GameAttributes,
1510                 "isNetworked": g_IsNetworked,
1511                 "playerAssignments": g_PlayerAssignments,
1512                 "isController": g_IsController
1513         });
1517  * Called whenever the host changed any setting.
1518  */
1519 function handleGamesetupMessage(message)
1521         if (!message.data)
1522                 return;
1524         g_GameAttributes = message.data;
1526         if (!!g_GameAttributes.settings.RatingEnabled)
1527         {
1528                 g_GameAttributes.settings.CheatsEnabled = false;
1529                 g_GameAttributes.settings.LockTeams = true;
1530                 g_GameAttributes.settings.LastManStanding = false;
1531         }
1533         Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
1535         resetReadyData();
1537         updateGUIObjects();
1539         hideLoadingWindow();
1543  * Called whenever a client joins/leaves or any gamesetting is changed.
1544  */
1545 function handlePlayerAssignmentMessage(message)
1547         let playerChange = false;
1549         for (let guid in message.newAssignments)
1550                 if (!g_PlayerAssignments[guid])
1551                 {
1552                         onClientJoin(guid, message.newAssignments);
1553                         playerChange = true;
1554                 }
1556         for (let guid in g_PlayerAssignments)
1557                 if (!message.newAssignments[guid])
1558                 {
1559                         onClientLeave(guid);
1560                         playerChange = true;
1561                 }
1563         g_PlayerAssignments = message.newAssignments;
1565         sanitizePlayerData(g_GameAttributes.settings.PlayerData);
1566         updateGUIObjects();
1568         if (playerChange)
1569                 sendRegisterGameStanzaImmediate();
1570         else
1571                 sendRegisterGameStanza();
1574 function onClientJoin(newGUID, newAssignments)
1576         let playername = newAssignments[newGUID].name;
1578         addChatMessage({
1579                 "type": "connect",
1580                 "guid": newGUID,
1581                 "username": playername
1582         });
1584         let isRejoiningPlayer = newAssignments[newGUID].player != -1;
1586         // Assign the client (or only buddies if prefered) to an unused playerslot and rejoining players to their old slot
1587         if (!isRejoiningPlayer && playername != newAssignments[Engine.GetPlayerGUID()].name)
1588         {
1589                 let assignOption = Engine.ConfigDB_GetValue("user", "gui.gamesetup.assignplayers");
1590                 if (assignOption == "disabled" ||
1591                     assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(playername).nick) == -1)
1592                         return;
1593         }
1595         let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v, i) =>
1596                 Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i + 1)
1597         );
1599         // Client is not and cannot become assigned as player
1600         if (!isRejoiningPlayer && freeSlot == -1)
1601                 return;
1603         // Assign the joining client to the free slot
1604         if (g_IsController && !isRejoiningPlayer)
1605                 Engine.AssignNetworkPlayer(freeSlot + 1, newGUID);
1607         resetReadyData();
1610 function onClientLeave(guid)
1612         addChatMessage({
1613                 "type": "disconnect",
1614                 "guid": guid
1615         });
1617         if (g_PlayerAssignments[guid].player != -1)
1618                 resetReadyData();
1622  * Doesn't translate, so that lobby clients can do that locally
1623  * (even if they don't have that map).
1624  */
1625 function getMapDisplayName(map)
1627         if (map == "random")
1628                 return map;
1630         let mapData = loadMapData(map);
1631         if (!mapData || !mapData.settings || !mapData.settings.Name)
1632                 return map;
1634         return mapData.settings.Name;
1637 function getMapPreview(map)
1639         let mapBiome = g_Settings.Biomes.find(biome => biome.Id == g_GameAttributes.settings.Biome);
1640         if (mapBiome && mapBiome.Preview)
1641                 return mapBiome.Preview;
1643         let mapData = loadMapData(map);
1644         if (!mapData || !mapData.settings || !mapData.settings.Preview)
1645                 return "nopreview.png";
1647         return mapData.settings.Preview;
1651  * Filter maps with filterFunc and by chosen map type.
1653  * @param {function} filterFunc - Filter function that should be applied.
1654  * @return {Array} the maps that match the filterFunc and the chosen map type.
1655  */
1656 function getFilteredMaps(filterFunc)
1658         if (!g_MapPath[g_GameAttributes.mapType])
1659         {
1660                 error("Unexpected map type: " + g_GameAttributes.mapType);
1661                 return [];
1662         }
1664         let maps = [];
1665         // TODO: Should verify these are valid maps before adding to list
1666         for (let mapFile of listFiles(g_GameAttributes.mapPath, g_GameAttributes.mapType == "random" ? ".json" : ".xml", false))
1667         {
1668                 if (mapFile.startsWith("_"))
1669                         continue;
1671                 let file = g_GameAttributes.mapPath + mapFile;
1672                 let mapData = loadMapData(file);
1674                 if (!mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || []))
1675                         continue;
1677                 maps.push({
1678                         "file": file,
1679                         "name": translate(getMapDisplayName(file)),
1680                         "color": g_ColorRegular,
1681                         "description": translate(mapData.settings.Description)
1682                 });
1683         }
1684         return maps;
1688  * Initialize the dropdown containing all map filters for the selected maptype.
1689  */
1690 function reloadMapFilterList()
1692         g_MapFilterList = prepareForDropdown(g_MapFilters.filter(
1693                 mapFilter => getFilteredMaps(mapFilter.filter).length
1694         ));
1696         initDropdown("mapFilter");
1697         reloadMapList();
1701  * Initialize the dropdown containing all maps for the selected maptype and mapfilter.
1702  */
1703 function reloadMapList()
1705         let filterID = g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter);
1706         let filterFunc = g_MapFilterList.filter[filterID];
1707         let mapList = getFilteredMaps(filterFunc).sort(sortNameIgnoreCase);
1709         if (g_GameAttributes.mapType == "random")
1710                 mapList.unshift({
1711                         "file": "random",
1712                         "name": translateWithContext("map selection", "Random"),
1713                         "color": g_ColorRandom,
1714                         "description": translate("Pick any of the given maps at random.")
1715                 });
1717         g_MapSelectionList = prepareForDropdown(mapList);
1718         initDropdown("mapSelection");
1722  * Initialize the dropdowns specific to each map.
1723  */
1724 function reloadMapSpecific()
1726         reloadBiomeList();
1727         reloadTriggerDifficulties();
1730 function reloadBiomeList()
1732         let biomeList;
1734         if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes)
1735         {
1736                 if (typeof g_GameAttributes.settings.SupportedBiomes == "string")
1737                         biomeList = g_Settings.Biomes.filter(biome => biome.Id.startsWith(g_GameAttributes.settings.SupportedBiomes));
1738                 else
1739                         biomeList = g_Settings.Biomes.filter(
1740                                 biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1);
1741         }
1743         g_BiomeList = biomeList && prepareForDropdown(
1744                 [{
1745                         "Id": "random",
1746                         "Title": translateWithContext("biome", "Random"),
1747                         "Description": translate("Pick a biome at random."),
1748                         "Color": g_ColorRandom
1749                 }].concat(biomeList.map(biome => ({
1750                         "Id": biome.Id,
1751                         "Title": biome.Title,
1752                         "Description": biome.Description,
1753                         "Color": g_ColorRegular
1754                 }))));
1756         initDropdown("biome");
1757         updateGUIDropdown("biome");
1760 function reloadTriggerDifficulties()
1762         g_TriggerDifficultyList = undefined;
1764         if (!g_GameAttributes.settings.SupportedTriggerDifficulties)
1765                 return;
1767         let triggerDifficultyList;
1768         if (g_GameAttributes.settings.SupportedTriggerDifficulties.Values === true)
1769                 triggerDifficultyList = g_Settings.TriggerDifficulties;
1770         else
1771         {
1772                 triggerDifficultyList = g_Settings.TriggerDifficulties.filter(
1773                         diff => g_GameAttributes.settings.SupportedTriggerDifficulties.Values.indexOf(diff.Name) != -1);
1774                 if (!triggerDifficultyList.length)
1775                         return;
1776         }
1778         g_TriggerDifficultyList = prepareForDropdown(
1779                 triggerDifficultyList.map(diff => ({
1780                         "Id": diff.Difficulty,
1781                         "Title": diff.Title,
1782                         "Description": diff.Tooltip,
1783                         "Default": diff.Name == g_GameAttributes.settings.SupportedTriggerDifficulties.Default
1784                 })));
1786         initDropdown("triggerDifficulty");
1787         updateGUIDropdown("triggerDifficulty");
1790 function reloadGameSpeedChoices()
1792         g_GameSpeeds = getGameSpeedChoices(Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player == -1));
1793         initDropdown("gameSpeed");
1794         supplementDefaults();
1797 function loadMapData(name)
1799         if (!name || !g_MapPath[g_GameAttributes.mapType])
1800                 return undefined;
1802         if (name == "random")
1803                 return { "settings": { "Name": "", "Description": "" } };
1805         if (!g_MapData[name])
1806                 g_MapData[name] = g_GameAttributes.mapType == "random" ?
1807                         Engine.ReadJSONFile(name + ".json") :
1808                         Engine.LoadMapSettings(name);
1810         return g_MapData[name];
1814  * Sets the gameattributes the way they were the last time the user left the gamesetup.
1815  */
1816 function loadPersistMatchSettings()
1818         if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial)
1819                 return;
1821         let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
1822         if (!Engine.FileExists(settingsFile))
1823                 return;
1825         let attrs = Engine.ReadJSONFile(settingsFile);
1826         if (!attrs || !attrs.settings)
1827                 return;
1829         g_IsInGuiUpdate = true;
1831         let mapName = attrs.map || "";
1832         let mapSettings = attrs.settings;
1834         g_GameAttributes = attrs;
1836         if (!g_IsNetworked)
1837                 mapSettings.CheatsEnabled = true;
1839         // Replace unselectable civs with random civ
1840         let playerData = mapSettings.PlayerData;
1841         if (playerData && g_GameAttributes.mapType != "scenario")
1842                 for (let i in playerData)
1843                         if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup)
1844                                 playerData[i].Civ = "random";
1846         // Apply map settings
1847         let newMapData = loadMapData(mapName);
1848         if (newMapData && newMapData.settings)
1849         {
1850                 for (let prop in newMapData.settings)
1851                         mapSettings[prop] = newMapData.settings[prop];
1853                 if (playerData)
1854                         mapSettings.PlayerData = playerData;
1855         }
1857         if (mapSettings.PlayerData)
1858                 sanitizePlayerData(mapSettings.PlayerData);
1860         // Reload, as the maptype or mapfilter might have changed
1861         reloadMapFilterList();
1862         reloadMapSpecific();
1864         g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient();
1865         Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled);
1867         supplementDefaults();
1869         g_IsInGuiUpdate = false;
1872 function savePersistMatchSettings()
1874         if (g_IsTutorial)
1875                 return;
1876         let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {};
1877         Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes);
1880 function sanitizePlayerData(playerData)
1882         // Remove gaia
1883         if (playerData.length && !playerData[0])
1884                 playerData.shift();
1886         playerData.forEach((pData, index) => {
1888                 // Use defaults if the map doesn't specify a value
1889                 for (let prop in g_DefaultPlayerData[index])
1890                         if (!(prop in pData))
1891                                 pData[prop] = clone(g_DefaultPlayerData[index][prop]);
1893                 // Replace colors with the best matching color of PlayerDefaults
1894                 if (g_GameAttributes.mapType != "scenario")
1895                 {
1896                         let colorDistances = g_PlayerColorPickerList.map(color => colorDistance(color, pData.Color));
1897                         let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance)));
1898                         pData.Color = g_PlayerColorPickerList.find(color => colorDistance(color, pData.Color) == smallestDistance);
1899                 }
1901                 // If there is a player in that slot, then there can't be an AI
1902                 if (Object.keys(g_PlayerAssignments).some(guid => g_PlayerAssignments[guid].player == index + 1))
1903                         pData.AI = "";
1904         });
1906         ensureUniquePlayerColors(playerData);
1909 function cancelSetup()
1911         if (g_IsController)
1912                 savePersistMatchSettings();
1914         Engine.DisconnectNetworkGame();
1916         if (Engine.HasXmppClient())
1917         {
1918                 Engine.LobbySetPlayerPresence("available");
1920                 if (g_IsController)
1921                         Engine.SendUnregisterGame();
1923                 Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
1924         }
1925         else
1926                 Engine.SwitchGuiPage("page_pregame.xml");
1930  * Can't init the GUI before the first tick.
1931  * Process netmessages afterwards.
1932  */
1933 function onTick()
1935         if (!g_Settings)
1936                 return;
1938         // First tick happens before first render, so don't load yet
1939         if (g_LoadingState == 0)
1940                 ++g_LoadingState;
1941         else if (g_LoadingState == 1)
1942         {
1943                 initGUIObjects();
1944                 ++g_LoadingState;
1945         }
1946         else if (g_LoadingState == 2)
1947                 handleNetMessages();
1949         updateTimers();
1951         let now = Date.now();
1952         let tickLength = now - g_LastTickTime;
1953         g_LastTickTime = now;
1955         updateSettingsPanelPosition(tickLength);
1959  * Handles all pending messages sent by the net client.
1960  */
1961 function handleNetMessages()
1963         while (g_IsNetworked)
1964         {
1965                 let message = Engine.PollNetworkClient();
1966                 if (!message)
1967                         break;
1969                 log("Net message: " + uneval(message));
1971                 if (g_NetMessageTypes[message.type])
1972                         g_NetMessageTypes[message.type](message);
1973                 else
1974                         error("Unrecognised net message type " + message.type);
1975         }
1979  * Called when the map or the number of players changes.
1980  */
1981 function unassignInvalidPlayers(maxPlayers)
1983         if (g_IsNetworked)
1984                 // Remove invalid playerIDs from the servers playerassignments copy
1985                 for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID)
1986                         Engine.AssignNetworkPlayer(playerID, "");
1988         else if (g_PlayerAssignments.local.player > maxPlayers)
1989                 g_PlayerAssignments.local.player = -1;
1992 function ensureUniquePlayerColors(playerData)
1994         for (let i = playerData.length - 1; i >= 0; --i)
1995                 // If someone else has that color, assign an unused color
1996                 if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color)))
1997                         playerData[i].Color = g_PlayerColorPickerList.find(color => playerData.every(pData => !sameColor(color, pData.Color)));
2000 function selectMap(name)
2002         // Reset some map specific properties which are not necessarily redefined on each map
2003         for (let prop of ["TriggerScripts", "CircularMap", "Garrison", "DisabledTemplates", "Biome", "SupportedBiomes", "SupportedTriggerDifficulties", "TriggerDifficulty"])
2004                 g_GameAttributes.settings[prop] = undefined;
2006         let mapData = loadMapData(name);
2007         let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {};
2009         if (g_GameAttributes.mapType != "random")
2010                 delete g_GameAttributes.settings.Nomad;
2012         if (g_GameAttributes.mapType == "scenario")
2013         {
2014                 delete g_GameAttributes.settings.RelicDuration;
2015                 delete g_GameAttributes.settings.WonderDuration;
2016                 delete g_GameAttributes.settings.LastManStanding;
2017                 delete g_GameAttributes.settings.RegicideGarrison;
2018         }
2020         if (mapSettings.PlayerData)
2021                 sanitizePlayerData(mapSettings.PlayerData);
2023         // Copy any new settings
2024         g_GameAttributes.map = name;
2025         g_GameAttributes.script = mapSettings.Script;
2026         if (g_GameAttributes.map !== "random")
2027                 for (let prop in mapSettings)
2028                         g_GameAttributes.settings[prop] = mapSettings[prop];
2030         reloadMapSpecific();
2031         unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length);
2032         supplementDefaults();
2035 function isControlArrayElementHidden(playerIdx)
2037         return playerIdx !== undefined && playerIdx >= g_GameAttributes.settings.PlayerData.length;
2041  * @param playerIdx - Only specified for dropdown arrays.
2042  */
2043 function updateGUIDropdown(name, playerIdx = undefined)
2045         let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
2046         let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
2048         let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName);
2049         let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx + idxName);
2050         let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx + idxName);
2051         let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx + idxName);
2053         if (guiType == "Dropdown")
2054                 Engine.GetGUIObjectByName(guiName + "Checkbox" + guiIdx).hidden = true;
2056         let indexHidden = isControlArrayElementHidden(playerIdx);
2057         let obj = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name];
2059         let hidden = indexHidden || obj.hidden && obj.hidden(playerIdx);
2060         let selected = hidden ? -1 : dropdown.list_data.indexOf(String(obj.get(playerIdx)));
2061         let enabled = !indexHidden && (!obj.enabled || obj.enabled(playerIdx));
2063         dropdown.enabled = g_IsController && enabled;
2064         dropdown.hidden = !g_IsController || !enabled || hidden;
2065         dropdown.selected = selected;
2066         dropdown.tooltip = !indexHidden && obj.tooltip ? obj.tooltip(-1, playerIdx) : "";
2068         if (frame)
2069                 frame.hidden = hidden;
2071         if (title && obj.title && !indexHidden)
2072                 title.caption = sprintf(translateWithContext("Title for specific setting", "%(setting)s:"), { "setting": obj.title(playerIdx) });
2074         if (label && !indexHidden)
2075         {
2076                 label.hidden = g_IsController && enabled || hidden;
2077                 label.caption = selected == -1 ? translateWithContext("settings value", "Unknown") : dropdown.list[selected];
2078         }
2082  * Not used for the player assignments, so playerCheckboxes are not implemented,
2083  * hence no index.
2084  */
2085 function updateGUICheckbox(name)
2087         let obj = g_Checkboxes[name];
2089         let checked = obj.get();
2090         let hidden = obj.hidden && obj.hidden();
2091         let enabled = !obj.enabled || obj.enabled();
2093         let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name);
2094         let checkbox = Engine.GetGUIObjectByName(guiName + guiType + guiIdx);
2095         let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx);
2096         let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx);
2097         let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx);
2099         if (guiType == "Checkbox")
2100                 Engine.GetGUIObjectByName(guiName + "Dropdown" + guiIdx).hidden = true;
2102         checkbox.checked = checked;
2103         checkbox.enabled = g_IsController && enabled;
2104         checkbox.hidden = hidden || !g_IsController;
2105         checkbox.tooltip = obj.tooltip ? obj.tooltip() : "";
2107         label.caption = checked ? translate("Yes") : translate("No");
2108         label.hidden = hidden || g_IsController;
2110         if (frame)
2111                 frame.hidden = hidden;
2113         if (title && obj.title)
2114                 title.caption = sprintf(translate("%(setting)s:"), { "setting": obj.title() });
2117 function updateGUIMiscControl(name, playerIdx)
2119         let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]";
2120         let obj = (playerIdx === undefined ? g_MiscControls : g_PlayerMiscElements)[name];
2122         let control = Engine.GetGUIObjectByName(name + idxName);
2123         if (!control)
2124                 warn("No GUI object with name '" + name + "'");
2126         let hide = isControlArrayElementHidden(playerIdx);
2127         control.hidden = hide;
2129         if (hide)
2130                 return;
2132         for (let property in obj)
2133                 control[property] = obj[property](playerIdx);
2136 function launchGame()
2138         if (!g_IsController)
2139         {
2140                 error("Only host can start game");
2141                 return;
2142         }
2144         if (!g_GameAttributes.map)
2145                 return;
2147         savePersistMatchSettings();
2149         // Select random map
2150         if (g_GameAttributes.map == "random")
2151                 selectMap(pickRandom(g_Dropdowns.mapSelection.ids().slice(1)));
2153         if (g_GameAttributes.settings.Biome == "random")
2154                 g_GameAttributes.settings.Biome = pickRandom(
2155                         typeof g_GameAttributes.settings.SupportedBiomes == "string" ?
2156                                 g_BiomeList.Id.slice(1).filter(biomeID => biomeID.startsWith(g_GameAttributes.settings.SupportedBiomes)) :
2157                                 g_GameAttributes.settings.SupportedBiomes);
2159         g_GameAttributes.settings.VictoryScripts = g_GameAttributes.settings.VictoryConditions.reduce(
2160                 (scripts, victoryConditionName) => scripts.concat(g_VictoryConditions[g_VictoryConditions.map(data =>
2161                         data.Name).indexOf(victoryConditionName)].Scripts.filter(script => scripts.indexOf(script) == -1)),
2162                 []);
2164         g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []);
2166         // Prevent reseting the readystate
2167         g_GameStarted = true;
2169         g_GameAttributes.settings.mapType = g_GameAttributes.mapType;
2171         // Get a unique array of selectable cultures
2172         let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture);
2173         cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index);
2175         // Determine random civs and botnames
2176         for (let i in g_GameAttributes.settings.PlayerData)
2177         {
2178                 // Pick a random civ of a random culture
2179                 let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random";
2180                 if (chosenCiv == "random")
2181                 {
2182                         let culture = pickRandom(cultures);
2183                         chosenCiv = pickRandom(Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture));
2184                 }
2185                 g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv;
2187                 // Pick one of the available botnames for the chosen civ
2188                 if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI)
2189                         continue;
2191                 let chosenName = pickRandom(g_CivData[chosenCiv].AINames);
2193                 if (!g_IsNetworked)
2194                         chosenName = translate(chosenName);
2196                 // Count how many players use the chosenName
2197                 let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length;
2199                 g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName :
2200                         sprintf(translate("%(playerName)s %(romanNumber)s"), {
2201                                 "playerName": chosenName,
2202                                 "romanNumber": g_RomanNumbers[usedName+1]
2203                         });
2204         }
2206         // Copy playernames for the purpose of replays
2207         for (let guid in g_PlayerAssignments)
2208         {
2209                 let player = g_PlayerAssignments[guid];
2210                 if (player.player > 0)  // not observer or GAIA
2211                         g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name;
2212         }
2214         // Seed used for both map generation and simulation
2215         g_GameAttributes.settings.Seed = randIntExclusive(0, Math.pow(2, 32));
2216         g_GameAttributes.settings.AISeed = randIntExclusive(0, Math.pow(2, 32));
2218         // Used for identifying rated game reports for the lobby
2219         g_GameAttributes.matchID = Engine.GetMatchID();
2221         if (g_IsNetworked)
2222         {
2223                 Engine.SetNetworkGameAttributes(g_GameAttributes);
2224                 Engine.StartNetworkGame();
2225         }
2226         else
2227         {
2228                 // Find the player ID which the user has been assigned to
2229                 let playerID = -1;
2230                 for (let i in g_GameAttributes.settings.PlayerData)
2231                 {
2232                         let assignBox = Engine.GetGUIObjectByName("playerAssignment[" + i + "]");
2233                         if (assignBox.list_data[assignBox.selected] == "guid:local")
2234                                 playerID = +i + 1;
2235                 }
2237                 Engine.StartGame(g_GameAttributes, playerID);
2238                 Engine.SwitchGuiPage("page_loading.xml", {
2239                         "attribs": g_GameAttributes,
2240                         "isNetworked": g_IsNetworked,
2241                         "playerAssignments": g_PlayerAssignments
2242                 });
2243         }
2246 function launchTutorial()
2248         g_GameAttributes.mapType = "scenario";
2249         selectMap("maps/tutorials/starting_economy_walkthrough");
2250         launchGame();
2254  * Don't set any attributes here, just show the changes in the GUI.
2256  * Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously.
2257  */
2258 function updateGUIObjects()
2260         g_IsInGuiUpdate = true;
2262         reloadMapFilterList();
2263         reloadMapSpecific();
2264         reloadGameSpeedChoices();
2265         reloadPlayerAssignmentChoices();
2267         // Hide exceeding dropdowns and checkboxes
2268         for (let setting of Engine.GetGUIObjectByName("settingsPanel").children)
2269                 setting.hidden = true;
2271         // Show the relevant ones
2272         if (g_TabCategorySelected !== undefined)
2273         {
2274                 for (let name in g_Dropdowns)
2275                         if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1)
2276                                 updateGUIDropdown(name);
2278                 for (let name in g_Checkboxes)
2279                         if (g_SettingsTabsGUI[g_TabCategorySelected].settings.indexOf(name) != -1)
2280                                 updateGUICheckbox(name);
2281         }
2283         for (let i = 0; i < g_MaxPlayers; ++i)
2284         {
2285                 for (let name in g_PlayerDropdowns)
2286                         updateGUIDropdown(name, i);
2288                 for (let name in g_PlayerMiscElements)
2289                         updateGUIMiscControl(name, i);
2290         }
2292         for (let name in g_MiscControls)
2293                 updateGUIMiscControl(name);
2295         updateGameDescription();
2296         distributeSettings();
2297         rightAlignCancelButton();
2298         updateAutocompleteEntries();
2300         g_IsInGuiUpdate = false;
2302         // Refresh AI config page
2303         if (g_LastViewedAIPlayer != -1)
2304         {
2305                 Engine.PopGuiPage();
2306                 openAIConfig(g_LastViewedAIPlayer);
2307         }
2310 function rightAlignCancelButton()
2312         let offset = 10;
2314         let startGame = Engine.GetGUIObjectByName("startGame");
2315         let right = startGame.hidden ? startGame.size.right : startGame.size.left - offset;
2317         let cancelGame = Engine.GetGUIObjectByName("cancelGame");
2318         let cancelGameSize = cancelGame.size;
2319         let buttonWidth = cancelGameSize.right - cancelGameSize.left;
2320         cancelGameSize.right = right;
2321         right -= buttonWidth;
2323         for (let element of ["cheatWarningText", "onscreenToolTip"])
2324         {
2325                 let elementSize = Engine.GetGUIObjectByName(element).size;
2326                 elementSize.right = right - (cancelGameSize.left - elementSize.right);
2327                 Engine.GetGUIObjectByName(element).size = elementSize;
2328         }
2330         cancelGameSize.left = right;
2331         cancelGame.size = cancelGameSize;
2334 function updateGameDescription()
2336         setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map));
2338         Engine.GetGUIObjectByName("mapInfoName").caption =
2339                 translateMapTitle(getMapDisplayName(g_GameAttributes.map));
2341         Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription();
2345  * Broadcast the changed settings to all clients and the lobbybot.
2346  */
2347 function updateGameAttributes()
2349         if (g_IsInGuiUpdate || !g_IsController)
2350                 return;
2352         if (g_IsNetworked)
2353         {
2354                 Engine.SetNetworkGameAttributes(g_GameAttributes);
2355                 if (g_LoadingState >= 2)
2356                         sendRegisterGameStanza();
2357                 resetReadyData();
2358         }
2359         else
2360                 updateGUIObjects();
2363 function openAIConfig(playerSlot)
2365         g_LastViewedAIPlayer = playerSlot;
2367         Engine.PushGuiPage("page_aiconfig.xml", {
2368                 "callback": "AIConfigCallback",
2369                 "isController": g_IsController,
2370                 "playerSlot": playerSlot,
2371                 "id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
2372                 "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
2373                 "behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior
2374         });
2378  * Called after closing the dialog.
2379  */
2380 function AIConfigCallback(ai)
2382         g_LastViewedAIPlayer = -1;
2384         if (!ai.save || !g_IsController)
2385                 return;
2387         g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
2388         g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
2389         g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior;
2391         updateGameAttributes();
2394 function reloadPlayerAssignmentChoices()
2396         let playerChoices = sortGUIDsByPlayerID().map(guid => ({
2397                 "Choice": "guid:" + guid,
2398                 "Color": g_PlayerAssignments[guid].player == -1 ? g_PlayerAssignmentColors.observer : g_PlayerAssignmentColors.player,
2399                 "Name": g_PlayerAssignments[guid].name
2400         }));
2402         // Only display hidden AIs if the map preselects them
2403         let aiChoices = g_Settings.AIDescriptions
2404                 .filter(ai => !ai.data.hidden || g_GameAttributes.settings.PlayerData.some(pData => pData.AI == ai.id))
2405                 .map(ai => ({
2406                         "Choice": "ai:" + ai.id,
2407                         "Name": sprintf(translate("AI: %(ai)s"), {
2408                                 "ai": translate(ai.data.name)
2409                         }),
2410                         "Color": g_PlayerAssignmentColors.AI
2411                 }));
2413         let unassignedSlot = [{
2414                 "Choice": "unassigned",
2415                 "Name": translate("Unassigned"),
2416                 "Color": g_PlayerAssignmentColors.unassigned
2417         }];
2418         g_PlayerAssignmentList = prepareForDropdown(playerChoices.concat(aiChoices).concat(unassignedSlot));
2420         initPlayerDropdowns("playerAssignment");
2423 function swapPlayers(guidToSwap, newSlot)
2425         // Player slots are indexed from 0 as Gaia is omitted.
2426         let newPlayerID = newSlot + 1;
2427         let playerID = g_PlayerAssignments[guidToSwap].player;
2429         // Attempt to swap the player or AI occupying the target slot,
2430         // if any, into the slot this player is currently in.
2431         if (playerID != -1)
2432         {
2433                 for (let guid in g_PlayerAssignments)
2434                 {
2435                         // Move the player in the destination slot into the current slot.
2436                         if (g_PlayerAssignments[guid].player != newPlayerID)
2437                                 continue;
2439                         if (g_IsNetworked)
2440                                 Engine.AssignNetworkPlayer(playerID, guid);
2441                         else
2442                                 g_PlayerAssignments[guid].player = playerID;
2443                         break;
2444                 }
2446                 // Transfer the AI from the target slot to the current slot.
2447                 g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI;
2448                 g_GameAttributes.settings.PlayerData[playerID - 1].AIDiff = g_GameAttributes.settings.PlayerData[newSlot].AIDiff;
2449                 g_GameAttributes.settings.PlayerData[playerID - 1].AIBehavior = g_GameAttributes.settings.PlayerData[newSlot].AIBehavior;
2451                 // Swap civilizations and colors if they aren't fixed
2452                 if (g_GameAttributes.mapType != "scenario")
2453                 {
2454                         [g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] =
2455                                 [g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ];
2456                         [g_GameAttributes.settings.PlayerData[playerID - 1].Color, g_GameAttributes.settings.PlayerData[newSlot].Color] =
2457                                 [g_GameAttributes.settings.PlayerData[newSlot].Color, g_GameAttributes.settings.PlayerData[playerID - 1].Color];
2458                 }
2459         }
2461         if (g_IsNetworked)
2462                 Engine.AssignNetworkPlayer(newPlayerID, guidToSwap);
2463         else
2464                 g_PlayerAssignments[guidToSwap].player = newPlayerID;
2466         g_GameAttributes.settings.PlayerData[newSlot].AI = "";
2469 function submitChatInput()
2471         let chatInput = Engine.GetGUIObjectByName("chatInput");
2472         let text = chatInput.caption;
2473         if (!text.length)
2474                 return;
2476         chatInput.caption = "";
2478         if (!executeNetworkCommand(text))
2479                 Engine.SendNetworkChat(text);
2481         chatInput.focus();
2484 function senderFont(text)
2486         return '[font="' + g_SenderFont + '"]' + text + '[/font]';
2489 function systemMessage(message)
2491         return senderFont(sprintf(translate("== %(message)s"), { "message": message }));
2494 function colorizePlayernameByGUID(guid, username = "")
2496         // TODO: Maybe the host should have the moderator-prefix?
2497         if (!username)
2498                 username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player");
2499         let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
2501         let color = g_ColorRegular;
2502         if (playerID > 0)
2503         {
2504                 color = g_GameAttributes.settings.PlayerData[playerID - 1].Color;
2506                 // Enlighten playercolor to improve readability
2507                 let [h, s, l] = rgbToHsl(color.r, color.g, color.b);
2508                 let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l));
2510                 color = rgbToGuiColor({ "r": r, "g": g, "b": b });
2511         }
2513         return coloredText(username, color);
2516 function addChatMessage(msg)
2518         if (!g_FormatChatMessage[msg.type])
2519                 return;
2521         if (msg.type == "chat")
2522         {
2523                 let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name;
2524                 if (userName != g_PlayerAssignments[msg.guid].name &&
2525                     msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1)
2526                         soundNotification("nick");
2527         }
2529         let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
2531         let text = g_FormatChatMessage[msg.type](msg, user);
2533         if (!text)
2534                 return;
2536         if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true")
2537                 text = sprintf(translate("%(time)s %(message)s"), {
2538                         "time": sprintf(translate("\\[%(time)s]"), {
2539                                 "time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm"))
2540                         }),
2541                         "message": text
2542                 });
2544         g_ChatMessages.push(text);
2546         Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
2549 function resetCivilizations()
2551         for (let i in g_GameAttributes.settings.PlayerData)
2552                 g_GameAttributes.settings.PlayerData[i].Civ = "random";
2554         updateGameAttributes();
2557 function resetTeams()
2559         for (let i in g_GameAttributes.settings.PlayerData)
2560                 g_GameAttributes.settings.PlayerData[i].Team = -1;
2562         updateGameAttributes();
2565 function toggleReady()
2567         setReady((g_IsReady + 1) % 3, true);
2570 function setReady(ready, sendMessage)
2572         g_IsReady = ready;
2574         if (sendMessage)
2575                 Engine.SendNetworkReady(g_IsReady);
2577         updateGUIObjects();
2580 function resetReadyData()
2582         if (g_GameStarted)
2583                 return;
2585         if (g_ReadyChanged < 1)
2586                 addChatMessage({ "type": "settings" });
2587         else if (g_ReadyChanged == 2 && !g_ReadyInit)
2588                 return; // duplicate calls on init
2589         else
2590                 g_ReadyInit = false;
2592         g_ReadyChanged = 2;
2593         if (!g_IsNetworked)
2594                 g_IsReady = 2;
2595         else if (g_IsController)
2596         {
2597                 Engine.ClearAllPlayerReady();
2598                 setReady(2, true);
2599         }
2600         else if (g_IsReady != 2)
2601                 setReady(0, false);
2605  * Send a list of playernames and distinct between players and observers.
2606  * Don't send teams, AIs or anything else until the game was started.
2607  * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
2608  */
2609 function formatClientsForStanza()
2611         let connectedPlayers = 0;
2612         let playerData = [];
2614         for (let guid in g_PlayerAssignments)
2615         {
2616                 let pData = { "Name": g_PlayerAssignments[guid].name };
2618                 if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
2619                         ++connectedPlayers;
2620                 else
2621                         pData.Team = "observer";
2623                 playerData.push(pData);
2624         }
2626         return {
2627                 "list": playerDataToStringifiedTeamList(playerData),
2628                 "connectedPlayers": connectedPlayers
2629         };
2633  * Send the relevant gamesettings to the lobbybot immediately.
2634  */
2635 function sendRegisterGameStanzaImmediate()
2637         if (!g_IsController || !Engine.HasXmppClient())
2638                 return;
2640         if (g_GameStanzaTimer !== undefined)
2641         {
2642                 clearTimeout(g_GameStanzaTimer);
2643                 g_GameStanzaTimer = undefined;
2644         }
2646         let clients = formatClientsForStanza();
2648         let stanza = {
2649                 "name": g_ServerName,
2650                 "port": g_ServerPort,
2651                 "hostUsername": Engine.LobbyGetNick(),
2652                 "mapName": g_GameAttributes.map,
2653                 "niceMapName": getMapDisplayName(g_GameAttributes.map),
2654                 "mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default",
2655                 "mapType": g_GameAttributes.mapType,
2656                 "victoryConditions": g_GameAttributes.settings.VictoryConditions.join(","),
2657                 "nbp": clients.connectedPlayers,
2658                 "maxnbp": g_GameAttributes.settings.PlayerData.length,
2659                 "players": clients.list,
2660                 "stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "",
2661                 "stunPort": g_StunEndpoint ? g_StunEndpoint.port : "",
2662                 "mods": JSON.stringify(Engine.GetEngineInfo().mods),
2663         };
2665         // Only send the stanza if the relevant settings actually changed
2666         if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop]))
2667                 return;
2669         g_LastGameStanza = stanza;
2670         Engine.SendRegisterGame(stanza);
2674  * Send the relevant gamesettings to the lobbybot in a deferred manner.
2675  */
2676 function sendRegisterGameStanza()
2678         if (!g_IsController || !Engine.HasXmppClient())
2679                 return;
2681         if (g_GameStanzaTimer !== undefined)
2682                 clearTimeout(g_GameStanzaTimer);
2684         g_GameStanzaTimer = setTimeout(sendRegisterGameStanzaImmediate, g_GameStanzaTimeout * 1000);
2688  * Figures out all strings that can be autocompleted and sorts
2689  * them by priority (so that playernames are always autocompleted first).
2690  */
2691 function updateAutocompleteEntries()
2693         let autocomplete = { "0": [] };
2695         for (let control of [g_Dropdowns, g_Checkboxes])
2696                 for (let name in control)
2697                         autocomplete[0] = autocomplete[0].concat(control[name].title());
2699         for (let dropdown of [g_Dropdowns, g_PlayerDropdowns])
2700                 for (let name in dropdown)
2701                 {
2702                         let priority = dropdown[name].autocomplete;
2703                         if (priority === undefined)
2704                                 continue;
2706                         autocomplete[priority] = (autocomplete[priority] || []).concat(dropdown[name].labels());
2707                 }
2709         g_Autocomplete = Object.keys(autocomplete).sort().reverse().reduce((all, priority) => all.concat(autocomplete[priority]), []);
2712 function storeCivInfoPage(data)
2714         g_CivInfo.code = data.civ;
2715         g_CivInfo.page = data.page;