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);
16 * Offer users to select playable civs only.
17 * Load unselectable civs as they could appear in scenario maps.
19 const g_CivData = loadCivData(false, false);
22 * Store civilization code and page (structree or history) opened in civilization info.
26 "page": "page_civinfo.xml"
30 * Highlight the "random" dropdownlist item.
32 var g_ColorRandom = "orange";
35 * Color for regular dropdownlist items.
37 var g_ColorRegular = "white";
40 * Color for "Unassigned"-placeholder item in the dropdownlist.
42 var g_PlayerAssignmentColors = {
43 "player": g_ColorRegular,
44 "observer": "170 170 250",
45 "unassigned": "140 140 140",
50 * Used for highlighting the sender of chat messages.
52 var g_SenderFont = "sans-bold-13";
55 * This yields [1, 2, ..., MaxPlayers].
57 var g_NumPlayersList = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1);
60 * Used for generating the botnames.
62 var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
64 var g_PlayerTeamList = prepareForDropdown([{
65 "label": translateWithContext("team", "None"),
68 Array(g_MaxTeams).fill(0).map((v, i) => ({
76 * Number of relics: [1, ..., NumCivs]
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,
86 Object.keys(g_CivData).filter(
87 civ => g_CivData[civ].SelectableInGameSetup
89 "name": g_CivData[civ].Name,
90 "tooltip": g_CivData[civ].History,
91 "color": g_ColorRegular,
93 })).sort(sortNameIgnoreCase)
98 * All selectable playercolors except gaia.
100 var g_PlayerColorPickerList = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color);
103 * Directory containing all maps of the given type.
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
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.")
126 "chat": translate("* %(username)s is ready!"),
127 "caption": translate("Stay ready"),
128 "tooltip": translate("Stay ready even when the game settings change.")
131 "color": "150 150 250",
133 "caption": translate("I'm not ready!"),
134 "tooltip": translate("State that you are not ready to play.")
139 * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
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
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 || "")
166 "ready": (msg, user) => sprintf(g_ReadyData[msg.status].chat, { "username": user }),
167 "clientlist": (msg, user) => getUsernameList(),
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),
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
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
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
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
204 "name": translate("All Maps"),
205 "tooltip": translateWithContext("map filter", "Every map of the chosen maptype."),
206 "filter": mapKeywords => true
211 * This contains only filters that have at least one map matching them.
216 * Array of biome identifiers supported by the currently selected map.
221 * Array of trigger difficulties identifiers supported by the currently selected map.
223 var g_TriggerDifficultyList;
226 * Whether this is a single- or multiplayer match.
231 * Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame).
236 * Whether this is a tutorial.
241 * To report the game to the lobby bot.
247 * IP address and port of the STUN endpoint.
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.
255 var g_IsInGuiUpdate = false;
258 * Whether the current player is ready to start the game.
266 * Ignore duplicate ready commands on init.
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
277 var g_ReadyChanged = 2;
280 * Used to prevent calling resetReadyData when starting a game.
282 var g_GameStarted = false;
285 * Selectable options (player, AI, unassigned) in the player assignment dropdowns and
286 * their colorized, textual representation.
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.
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).
304 var g_Autocomplete = [];
307 * Array of strings formatted as displayed, including playername.
309 var g_ChatMessages = [];
312 * Minimum amount of pixels required for the chat panel to be visible.
314 var g_MinChatWidth = 96;
317 * Horizontal space between chat window and settings.
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.
325 var g_MapSelectionList = [];
328 * Cache containing the mapsettings. Just-in-time loading.
333 * Wait one tick before initializing the GUI objects and
334 * don't process netmessages prior to that.
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.
341 var g_GameStanzaTimeout = 2;
344 * Index of the GUI timer.
346 var g_GameStanzaTimer;
349 * Only send a lobby update if something actually changed.
351 var g_LastGameStanza;
354 * Remembers if the current player viewed the AI settings of some playerslot.
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".
362 var g_PopulationCapacityRecommendation = 1200;
365 * Horizontal space between tab buttons and lobby button.
367 var g_LobbyButtonSpacing = 8;
370 * Vertical size of a tab button.
372 var g_TabButtonHeight = 30;
375 * Vertical space between two tab buttons.
377 var g_TabButtonDist = 4;
380 * Vertical size of a setting object.
382 var g_SettingHeight = 32;
385 * Vertical space between two setting objects.
387 var g_SettingDist = 2;
390 * Maximum width of a column in the settings panel.
392 var g_MaxColumnWidth = 470;
395 * Pixels per millisecond the settings panel slides when opening/closing.
397 var g_SlideSpeed = 1.2;
400 * Store last tick time.
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.
408 var g_SettingsTabsGUI = [
410 "label": translateWithContext("Match settings tab name", "Map"),
425 "label": translateWithContext("Match settings tab name", "Player"),
435 "label": translateWithContext("Match settings tab name", "Game Type"),
437 ...g_VictoryConditions.map(victoryCondition => victoryCondition.Name),
452 * Contains the logic of all multiple-choice gamesettings.
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.
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.
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) => {
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))
493 reloadMapFilterList();
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;
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,
521 "defined": () => g_GameAttributes.map !== undefined,
522 "get": () => g_GameAttributes.map,
523 "select": (itemIdx) => {
524 selectMap(g_MapSelectionList.file[itemIdx]);
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];
540 "hidden": () => g_GameAttributes.mapType != "random",
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 : [],
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];
556 "hidden": () => !g_BiomeList,
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 =
574 pData.concat(clone(g_DefaultPlayerData.slice(pData.length, num))) :
576 unassignInvalidPlayers(num);
577 sanitizePlayerData(g_GameAttributes.settings.PlayerData);
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.");
592 sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), {
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];
606 "enabled": () => g_GameAttributes.mapType != "scenario",
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]
616 translate("Select the game's starting resources.");
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];
626 "hidden": () => g_GameAttributes.mapType == "scenario",
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];
641 "enabled": () => g_GameAttributes.mapType != "scenario",
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];
655 "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
656 "enabled": () => g_GameAttributes.mapType != "scenario",
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];
670 "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("capture_the_relic") == -1,
671 "enabled": () => g_GameAttributes.mapType != "scenario",
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];
685 "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("wonder") == -1,
686 "enabled": () => g_GameAttributes.mapType != "scenario",
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,
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];
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];
716 "hidden": () => !g_TriggerDifficultyList,
722 * These dropdowns provide a setting that is repeated once for each player
723 * (where playerIdx is starting from 0 for player 1).
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;
743 "select": (selectedIdx, playerIdx) => {
745 let choice = g_PlayerAssignmentList.Choice[selectedIdx];
746 if (choice == "unassigned" || choice.startsWith("ai:"))
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) : "";
756 swapPlayers(choice.substr("guid:".length), playerIdx);
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;
769 "enabled": () => g_GameAttributes.mapType != "scenario",
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];
782 "enabled": () => g_GameAttributes.mapType != "scenario",
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));
798 sameColorPData.Color = playerData[playerIdx].Color;
800 playerData[playerIdx].Color = g_PlayerColorPickerList[selectedIdx];
801 ensureUniquePlayerColors(playerData);
803 "enabled": () => g_GameAttributes.mapType != "scenario",
808 * Contains the logic of all boolean gamesettings.
810 var g_Checkboxes = Object.assign(
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,
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]);
828 g_GameAttributes.settings.VictoryConditions = g_GameAttributes.settings.VictoryConditions.filter(victoryConditionName => victoryConditionName != victoryCondition.Name);
831 g_GameAttributes.mapType != "scenario" &&
832 (!victoryCondition.DisabledWhenChecked ||
833 victoryCondition.DisabledWhenChecked.every(victoryConditionName => g_GameAttributes.settings.VictoryConditions.indexOf(victoryConditionName) == -1))
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,
845 g_GameAttributes.settings.RegicideGarrison = checked;
847 "hidden": () => g_GameAttributes.settings.VictoryConditions.indexOf("regicide") == -1,
848 "enabled": () => g_GameAttributes.mapType != "scenario",
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,
858 g_GameAttributes.settings.Nomad = checked;
860 "hidden": () => g_GameAttributes.mapType != "random",
865 // Translation: Make sure to differentiate between the revealed map and explored map settings!
866 translate("Revealed Map"),
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,
874 g_GameAttributes.settings.RevealMap = checked;
877 g_Checkboxes.exploreMap.set(true);
879 "enabled": () => g_GameAttributes.mapType != "scenario",
884 // Translation: Make sure to differentiate between the revealed map and explored map settings!
885 () => translate("Explored Map"),
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,
893 g_GameAttributes.settings.ExploreMap = checked;
895 "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap,
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,
905 g_GameAttributes.settings.DisableTreasures = checked;
907 "enabled": () => g_GameAttributes.mapType != "scenario",
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,
917 g_GameAttributes.settings.DisableSpies = checked;
919 "enabled": () => g_GameAttributes.mapType != "scenario",
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,
929 g_GameAttributes.settings.LockTeams = checked;
930 g_GameAttributes.settings.LastManStanding = false;
933 g_GameAttributes.mapType != "scenario" &&
934 !g_GameAttributes.settings.RatingEnabled,
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,
944 g_GameAttributes.settings.LastManStanding = checked;
947 g_GameAttributes.mapType != "scenario" &&
948 !g_GameAttributes.settings.LockTeams,
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,
959 g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked ||
960 checked && !g_GameAttributes.settings.RatingEnabled;
962 "enabled": () => !g_GameAttributes.settings.RatingEnabled,
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,
973 g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined;
974 Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
977 g_Checkboxes.lockTeams.set(true);
978 g_Checkboxes.enableCheats.set(false);
987 * For setting up arbitrary GUI objects.
989 var g_MiscControls = {
995 let size = Engine.GetGUIObjectByName("chatPanel").getComputedSize();
996 return size.right - size.left < g_MinChatWidth;
1000 "tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete playernames or settings.")),
1002 "cheatWarningText": {
1003 "hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled,
1007 Engine.HasXmppClient() ?
1008 translate("Return to the lobby.") :
1009 translate("Return to the main menu."),
1013 g_IsController ? translate("Start Game!") : g_ReadyData[g_IsReady].caption,
1014 "tooltip": (hoverIdx) =>
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),
1026 !g_PlayerAssignments[Engine.GetPlayerGUID()] ||
1027 g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1 && !g_IsController,
1030 "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
1032 "teamResetButton": {
1033 "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController,
1036 "onPress": () => function() {
1037 if (Engine.HasXmppClient())
1038 Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
1040 "hidden": () => !Engine.HasXmppClient()
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;
1054 * Contains gui elements that are repeated for every player.
1056 var g_PlayerMiscElements = {
1058 "size": (playerIdx) => ["0", 32 * playerIdx, "100%", 32 * (playerIdx + 1)].join(" "),
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);
1070 name = coloredText(name, g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color);
1076 "sprite": (playerIdx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[playerIdx].Color, 100),
1079 "hidden": (playerIdx) => !g_GameAttributes.settings.PlayerData[playerIdx].AI,
1080 "onPress": (playerIdx) => function() {
1081 openAIConfig(playerIdx);
1083 "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(description)s."), {
1084 "description": translateAISettings(g_GameAttributes.settings.PlayerData[playerIdx])
1090 * Initializes some globals without touching the GUI.
1092 * @param {Object} attribs - context data sent by the lobby / mainmenu
1094 function init(attribs)
1102 if (["offline", "server", "client"].indexOf(attribs.type) == -1)
1104 error("Unexpected 'type' in gamesetup init: " + attribs.type);
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;
1117 g_PlayerAssignments = {
1119 "name": singleplayerName(),
1124 // Replace empty playername when entering a singleplayermatch for the first time
1126 saveSettingAndWriteToUserConfig("playername.singleplayer", singleplayerName());
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")
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)
1151 g_DefaultPlayerData[i].Civ = "random";
1152 g_DefaultPlayerData[i].Team = -1;
1153 g_DefaultPlayerData[i].AIDiff = aiDifficulty;
1154 g_DefaultPlayerData[i].AIBehavior = aiBehavior;
1157 deepfreeze(g_DefaultPlayerData);
1161 * Sets default values for all g_GameAttribute settings which don't have a value set.
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.
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
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);
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;
1233 selectPanel(category == g_TabCategorySelected ? undefined : category);
1237 Engine.GetGUIObjectByName("settingsPanel").hidden = false;
1242 loadPersistMatchSettings();
1243 updateGameAttributes();
1244 sendRegisterGameStanzaImmediate();
1252 // Don't lift the curtain until the controls are updated the first time
1254 hideLoadingWindow();
1258 * Slide settings panel.
1259 * @param {number} dt - Time in milliseconds since last call.
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;
1268 if (g_TabCategorySelected === undefined)
1270 let maxOffset = rightBorder - settingsPanel.size.left;
1272 offset = Math.min(slideSpeed * dt, maxOffset);
1274 else if (rightBorder > settingsPanel.size.right)
1275 offset = Math.min(slideSpeed * dt, rightBorder - settingsPanel.size.right);
1278 let maxOffset = settingsPanel.size.right - rightBorder;
1280 offset = -Math.min(slideSpeed * dt, maxOffset);
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)
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.
1321 function getGUIObjectNameFromSetting(setting)
1324 for (let category of g_SettingsTabsGUI)
1326 let idx = category.settings.indexOf(setting);
1330 g_Dropdowns[setting] ? "Dropdown" : "Checkbox",
1331 "[" + (idx + idxOffset) + "]"
1333 idxOffset += category.settings.length;
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]) :
1353 dropdown.list_data = data.ids(playerIdx);
1355 dropdown.onSelectionChange = function() {
1357 if (!g_IsController ||
1359 !this.list_data[this.selected] ||
1360 data.hidden && data.hidden(playerIdx) ||
1361 data.enabled && !data.enabled(playerIdx))
1364 data.select(this.selected, playerIdx);
1366 supplementDefaults();
1367 updateGameAttributes();
1371 dropdown.onHoverChange = function() {
1372 this.tooltip = data.tooltip(this.hovered, playerIdx);
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 ||
1391 obj.enabled && !obj.enabled() ||
1392 obj.hidden && obj.hidden())
1395 obj.set(this.checked);
1397 supplementDefaults();
1398 updateGameAttributes();
1402 function initSPTips()
1404 if (g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true")
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.
1416 function distributeSettings()
1418 let setupWindowSize = Engine.GetGUIObjectByName("setupWindow").getComputedSize();
1419 let columnWidth = Math.min(
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;
1433 let settingsPanelSize = settingsPanel.size;
1434 for (let child of settingsPanel.children)
1439 if (thisColumn >= perColumn)
1441 yPos = g_SettingDist;
1446 let childSize = child.size;
1447 child.size = new GUISize(
1448 column * columnWidth,
1450 column * columnWidth + columnWidth - 10,
1451 yPos + g_SettingHeight - g_SettingDist);
1453 yPos += g_SettingHeight;
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.
1465 function handleNetStatusMessage(message)
1467 if (message.status != "disconnected")
1469 error("Unrecognised netstatus type " + message.status);
1474 reportDisconnect(message.reason, true);
1478 * Called whenever a client clicks on ready (or not ready).
1480 function handleReadyMessage(message)
1484 if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1)
1487 "status": message.status,
1488 "guid": message.guid
1491 g_PlayerAssignments[message.guid].status = message.status;
1496 * Called after every player is ready and the host decided to finally start the game.
1498 function handleGamestartMessage(message)
1500 // Immediately inform the lobby server instead of waiting for the load to finish
1501 if (g_IsController && Engine.HasXmppClient())
1503 sendRegisterGameStanzaImmediate();
1504 let clients = formatClientsForStanza();
1505 Engine.SendChangeStateGame(clients.connectedPlayers, clients.list);
1508 Engine.SwitchGuiPage("page_loading.xml", {
1509 "attribs": g_GameAttributes,
1510 "isNetworked": g_IsNetworked,
1511 "playerAssignments": g_PlayerAssignments,
1512 "isController": g_IsController
1517 * Called whenever the host changed any setting.
1519 function handleGamesetupMessage(message)
1524 g_GameAttributes = message.data;
1526 if (!!g_GameAttributes.settings.RatingEnabled)
1528 g_GameAttributes.settings.CheatsEnabled = false;
1529 g_GameAttributes.settings.LockTeams = true;
1530 g_GameAttributes.settings.LastManStanding = false;
1533 Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled);
1539 hideLoadingWindow();
1543 * Called whenever a client joins/leaves or any gamesetting is changed.
1545 function handlePlayerAssignmentMessage(message)
1547 let playerChange = false;
1549 for (let guid in message.newAssignments)
1550 if (!g_PlayerAssignments[guid])
1552 onClientJoin(guid, message.newAssignments);
1553 playerChange = true;
1556 for (let guid in g_PlayerAssignments)
1557 if (!message.newAssignments[guid])
1559 onClientLeave(guid);
1560 playerChange = true;
1563 g_PlayerAssignments = message.newAssignments;
1565 sanitizePlayerData(g_GameAttributes.settings.PlayerData);
1569 sendRegisterGameStanzaImmediate();
1571 sendRegisterGameStanza();
1574 function onClientJoin(newGUID, newAssignments)
1576 let playername = newAssignments[newGUID].name;
1581 "username": playername
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)
1589 let assignOption = Engine.ConfigDB_GetValue("user", "gui.gamesetup.assignplayers");
1590 if (assignOption == "disabled" ||
1591 assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(playername).nick) == -1)
1595 let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v, i) =>
1596 Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i + 1)
1599 // Client is not and cannot become assigned as player
1600 if (!isRejoiningPlayer && freeSlot == -1)
1603 // Assign the joining client to the free slot
1604 if (g_IsController && !isRejoiningPlayer)
1605 Engine.AssignNetworkPlayer(freeSlot + 1, newGUID);
1610 function onClientLeave(guid)
1613 "type": "disconnect",
1617 if (g_PlayerAssignments[guid].player != -1)
1622 * Doesn't translate, so that lobby clients can do that locally
1623 * (even if they don't have that map).
1625 function getMapDisplayName(map)
1627 if (map == "random")
1630 let mapData = loadMapData(map);
1631 if (!mapData || !mapData.settings || !mapData.settings.Name)
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.
1656 function getFilteredMaps(filterFunc)
1658 if (!g_MapPath[g_GameAttributes.mapType])
1660 error("Unexpected map type: " + g_GameAttributes.mapType);
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))
1668 if (mapFile.startsWith("_"))
1671 let file = g_GameAttributes.mapPath + mapFile;
1672 let mapData = loadMapData(file);
1674 if (!mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || []))
1679 "name": translate(getMapDisplayName(file)),
1680 "color": g_ColorRegular,
1681 "description": translate(mapData.settings.Description)
1688 * Initialize the dropdown containing all map filters for the selected maptype.
1690 function reloadMapFilterList()
1692 g_MapFilterList = prepareForDropdown(g_MapFilters.filter(
1693 mapFilter => getFilteredMaps(mapFilter.filter).length
1696 initDropdown("mapFilter");
1701 * Initialize the dropdown containing all maps for the selected maptype and mapfilter.
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")
1712 "name": translateWithContext("map selection", "Random"),
1713 "color": g_ColorRandom,
1714 "description": translate("Pick any of the given maps at random.")
1717 g_MapSelectionList = prepareForDropdown(mapList);
1718 initDropdown("mapSelection");
1722 * Initialize the dropdowns specific to each map.
1724 function reloadMapSpecific()
1727 reloadTriggerDifficulties();
1730 function reloadBiomeList()
1734 if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes)
1736 if (typeof g_GameAttributes.settings.SupportedBiomes == "string")
1737 biomeList = g_Settings.Biomes.filter(biome => biome.Id.startsWith(g_GameAttributes.settings.SupportedBiomes));
1739 biomeList = g_Settings.Biomes.filter(
1740 biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1);
1743 g_BiomeList = biomeList && prepareForDropdown(
1746 "Title": translateWithContext("biome", "Random"),
1747 "Description": translate("Pick a biome at random."),
1748 "Color": g_ColorRandom
1749 }].concat(biomeList.map(biome => ({
1751 "Title": biome.Title,
1752 "Description": biome.Description,
1753 "Color": g_ColorRegular
1756 initDropdown("biome");
1757 updateGUIDropdown("biome");
1760 function reloadTriggerDifficulties()
1762 g_TriggerDifficultyList = undefined;
1764 if (!g_GameAttributes.settings.SupportedTriggerDifficulties)
1767 let triggerDifficultyList;
1768 if (g_GameAttributes.settings.SupportedTriggerDifficulties.Values === true)
1769 triggerDifficultyList = g_Settings.TriggerDifficulties;
1772 triggerDifficultyList = g_Settings.TriggerDifficulties.filter(
1773 diff => g_GameAttributes.settings.SupportedTriggerDifficulties.Values.indexOf(diff.Name) != -1);
1774 if (!triggerDifficultyList.length)
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
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])
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.
1816 function loadPersistMatchSettings()
1818 if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial)
1821 let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
1822 if (!Engine.FileExists(settingsFile))
1825 let attrs = Engine.ReadJSONFile(settingsFile);
1826 if (!attrs || !attrs.settings)
1829 g_IsInGuiUpdate = true;
1831 let mapName = attrs.map || "";
1832 let mapSettings = attrs.settings;
1834 g_GameAttributes = attrs;
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)
1850 for (let prop in newMapData.settings)
1851 mapSettings[prop] = newMapData.settings[prop];
1854 mapSettings.PlayerData = playerData;
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()
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)
1883 if (playerData.length && !playerData[0])
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")
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);
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))
1906 ensureUniquePlayerColors(playerData);
1909 function cancelSetup()
1912 savePersistMatchSettings();
1914 Engine.DisconnectNetworkGame();
1916 if (Engine.HasXmppClient())
1918 Engine.LobbySetPlayerPresence("available");
1921 Engine.SendUnregisterGame();
1923 Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
1926 Engine.SwitchGuiPage("page_pregame.xml");
1930 * Can't init the GUI before the first tick.
1931 * Process netmessages afterwards.
1938 // First tick happens before first render, so don't load yet
1939 if (g_LoadingState == 0)
1941 else if (g_LoadingState == 1)
1946 else if (g_LoadingState == 2)
1947 handleNetMessages();
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.
1961 function handleNetMessages()
1963 while (g_IsNetworked)
1965 let message = Engine.PollNetworkClient();
1969 log("Net message: " + uneval(message));
1971 if (g_NetMessageTypes[message.type])
1972 g_NetMessageTypes[message.type](message);
1974 error("Unrecognised net message type " + message.type);
1979 * Called when the map or the number of players changes.
1981 function unassignInvalidPlayers(maxPlayers)
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")
2014 delete g_GameAttributes.settings.RelicDuration;
2015 delete g_GameAttributes.settings.WonderDuration;
2016 delete g_GameAttributes.settings.LastManStanding;
2017 delete g_GameAttributes.settings.RegicideGarrison;
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.
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) : "";
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)
2076 label.hidden = g_IsController && enabled || hidden;
2077 label.caption = selected == -1 ? translateWithContext("settings value", "Unknown") : dropdown.list[selected];
2082 * Not used for the player assignments, so playerCheckboxes are not implemented,
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;
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);
2124 warn("No GUI object with name '" + name + "'");
2126 let hide = isControlArrayElementHidden(playerIdx);
2127 control.hidden = hide;
2132 for (let property in obj)
2133 control[property] = obj[property](playerIdx);
2136 function launchGame()
2138 if (!g_IsController)
2140 error("Only host can start game");
2144 if (!g_GameAttributes.map)
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)),
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)
2178 // Pick a random civ of a random culture
2179 let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random";
2180 if (chosenCiv == "random")
2182 let culture = pickRandom(cultures);
2183 chosenCiv = pickRandom(Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture));
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)
2191 let chosenName = pickRandom(g_CivData[chosenCiv].AINames);
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]
2206 // Copy playernames for the purpose of replays
2207 for (let guid in g_PlayerAssignments)
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;
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();
2223 Engine.SetNetworkGameAttributes(g_GameAttributes);
2224 Engine.StartNetworkGame();
2228 // Find the player ID which the user has been assigned to
2230 for (let i in g_GameAttributes.settings.PlayerData)
2232 let assignBox = Engine.GetGUIObjectByName("playerAssignment[" + i + "]");
2233 if (assignBox.list_data[assignBox.selected] == "guid:local")
2237 Engine.StartGame(g_GameAttributes, playerID);
2238 Engine.SwitchGuiPage("page_loading.xml", {
2239 "attribs": g_GameAttributes,
2240 "isNetworked": g_IsNetworked,
2241 "playerAssignments": g_PlayerAssignments
2246 function launchTutorial()
2248 g_GameAttributes.mapType = "scenario";
2249 selectMap("maps/tutorials/starting_economy_walkthrough");
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.
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)
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);
2283 for (let i = 0; i < g_MaxPlayers; ++i)
2285 for (let name in g_PlayerDropdowns)
2286 updateGUIDropdown(name, i);
2288 for (let name in g_PlayerMiscElements)
2289 updateGUIMiscControl(name, i);
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)
2305 Engine.PopGuiPage();
2306 openAIConfig(g_LastViewedAIPlayer);
2310 function rightAlignCancelButton()
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"])
2325 let elementSize = Engine.GetGUIObjectByName(element).size;
2326 elementSize.right = right - (cancelGameSize.left - elementSize.right);
2327 Engine.GetGUIObjectByName(element).size = elementSize;
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.
2347 function updateGameAttributes()
2349 if (g_IsInGuiUpdate || !g_IsController)
2354 Engine.SetNetworkGameAttributes(g_GameAttributes);
2355 if (g_LoadingState >= 2)
2356 sendRegisterGameStanza();
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
2378 * Called after closing the dialog.
2380 function AIConfigCallback(ai)
2382 g_LastViewedAIPlayer = -1;
2384 if (!ai.save || !g_IsController)
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
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))
2406 "Choice": "ai:" + ai.id,
2407 "Name": sprintf(translate("AI: %(ai)s"), {
2408 "ai": translate(ai.data.name)
2410 "Color": g_PlayerAssignmentColors.AI
2413 let unassignedSlot = [{
2414 "Choice": "unassigned",
2415 "Name": translate("Unassigned"),
2416 "Color": g_PlayerAssignmentColors.unassigned
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.
2433 for (let guid in g_PlayerAssignments)
2435 // Move the player in the destination slot into the current slot.
2436 if (g_PlayerAssignments[guid].player != newPlayerID)
2440 Engine.AssignNetworkPlayer(playerID, guid);
2442 g_PlayerAssignments[guid].player = playerID;
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")
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];
2462 Engine.AssignNetworkPlayer(newPlayerID, guidToSwap);
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;
2476 chatInput.caption = "";
2478 if (!executeNetworkCommand(text))
2479 Engine.SendNetworkChat(text);
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?
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;
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 });
2513 return coloredText(username, color);
2516 function addChatMessage(msg)
2518 if (!g_FormatChatMessage[msg.type])
2521 if (msg.type == "chat")
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");
2529 let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || "");
2531 let text = g_FormatChatMessage[msg.type](msg, user);
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"))
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)
2575 Engine.SendNetworkReady(g_IsReady);
2580 function resetReadyData()
2585 if (g_ReadyChanged < 1)
2586 addChatMessage({ "type": "settings" });
2587 else if (g_ReadyChanged == 2 && !g_ReadyInit)
2588 return; // duplicate calls on init
2590 g_ReadyInit = false;
2595 else if (g_IsController)
2597 Engine.ClearAllPlayerReady();
2600 else if (g_IsReady != 2)
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.
2609 function formatClientsForStanza()
2611 let connectedPlayers = 0;
2612 let playerData = [];
2614 for (let guid in g_PlayerAssignments)
2616 let pData = { "Name": g_PlayerAssignments[guid].name };
2618 if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1])
2621 pData.Team = "observer";
2623 playerData.push(pData);
2627 "list": playerDataToStringifiedTeamList(playerData),
2628 "connectedPlayers": connectedPlayers
2633 * Send the relevant gamesettings to the lobbybot immediately.
2635 function sendRegisterGameStanzaImmediate()
2637 if (!g_IsController || !Engine.HasXmppClient())
2640 if (g_GameStanzaTimer !== undefined)
2642 clearTimeout(g_GameStanzaTimer);
2643 g_GameStanzaTimer = undefined;
2646 let clients = formatClientsForStanza();
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),
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]))
2669 g_LastGameStanza = stanza;
2670 Engine.SendRegisterGame(stanza);
2674 * Send the relevant gamesettings to the lobbybot in a deferred manner.
2676 function sendRegisterGameStanza()
2678 if (!g_IsController || !Engine.HasXmppClient())
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).
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)
2702 let priority = dropdown[name].autocomplete;
2703 if (priority === undefined)
2706 autocomplete[priority] = (autocomplete[priority] || []).concat(dropdown[name].labels());
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;