Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 20704) +++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 20705) @@ -1,427 +1,437 @@ /** * Highlights the victory condition in the game-description. */ var g_DescriptionHighlight = "orange"; /** * The rating assigned to lobby players who didn't complete a ranked 1v1 yet. */ var g_DefaultLobbyRating = 1200; /** * XEP-0172 doesn't restrict nicknames, but our lobby policy does. * So use this human readable delimiter to separate buddy names in the config file. */ var g_BuddyListDelimiter = ","; /** * Returns the nickname without the lobby rating. */ function splitRatingFromNick(playerName) { let result = /^(\S+)\ \((\d+)\)$/g.exec(playerName); return { "nick": result ? result[1] : playerName, "rating": result ? +result[2] : "" }; } /** * Array of playernames that the current user has marked as buddies. */ var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter); /** * Denotes which players are a lobby buddy of the current user. */ var g_BuddySymbol = '•'; /** * Returns map description and preview image or placeholder. */ function getMapDescriptionAndPreview(mapType, mapName) { let mapData; if (mapType == "random" && mapName == "random") mapData = { "settings": { "Description": translate("A randomly selected map.") } }; else if (mapType == "random" && Engine.FileExists(mapName + ".json")) mapData = Engine.ReadJSONFile(mapName + ".json"); else if (Engine.FileExists(mapName + ".xml")) mapData = Engine.LoadMapSettings(mapName + ".xml"); return deepfreeze({ "description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."), "preview": mapData && mapData.settings && mapData.settings.Preview ? mapData.settings.Preview : "nopreview.png" }); } /** * Sets the mappreview image correctly. * It needs to be cropped as the engine only allows loading square textures. * * @param {string} guiObject * @param {string} filename */ function setMapPreviewImage(guiObject, filename) { Engine.GetGUIObjectByName(guiObject).sprite = "cropped:" + 400 / 512 + "," + 300 / 512 + ":" + "session/icons/mappreview/" + filename; } /** * Returns a formatted string describing the player assignments. * Needs g_CivData to translate! * * @param {object} playerDataArray - As known from gamesetup and simstate. * @param {(string[]|false)} playerStates - One of "won", "defeated", "active" for each player. * @returns {string} */ function formatPlayerInfo(playerDataArray, playerStates) { let playerDescriptions = {}; let playerIdx = 0; for (let playerData of playerDataArray) { if (playerData == null || playerData.Civ && playerData.Civ == "gaia") continue; ++playerIdx; let teamIdx = playerData.Team; let isAI = playerData.AI && playerData.AI != ""; let playerState = playerStates && playerStates[playerIdx] || playerData.State; let isActive = !playerState || playerState == "active"; let playerDescription; if (isAI) { if (playerData.Civ) { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIbehavior)s %(AIname)s)"); + playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIbehavior)s %(AIname)s, %(state)s)"); + playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s, %(state)s)"); } else { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIbehavior)s %(AIname)s)"); + playerDescription = translate("%(playerName)s (%(AIdescription)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu - playerDescription = translate("%(playerName)s (%(AIdifficulty)s %(AIbehavior)s %(AIname)s, %(state)s)"); + playerDescription = translate("%(playerName)s (%(AIdescription)s, %(state)s)"); } } else { if (playerData.Offline) { // Can only occur in the lobby for now, so no strings with civ needed if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE, %(state)s)"); } else { if (playerData.Civ) if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)"); else if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(state)s)"); } } // Sort player descriptions by team if (!playerDescriptions[teamIdx]) playerDescriptions[teamIdx] = []; let playerNick = splitRatingFromNick(playerData.Name).nick; playerDescriptions[teamIdx].push(sprintf(playerDescription, { "playerName": coloredText( (g_Buddies.indexOf(playerNick) != -1 ? g_BuddySymbol + " " : "") + escapeText(playerData.Name), (typeof getPlayerColor == 'function' ? (isAI ? "white" : getPlayerColor(playerNick)) : rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color))), "civ": !playerData.Civ ? translate("Unknown Civilization") : g_CivData && g_CivData[playerData.Civ] && g_CivData[playerData.Civ].Name ? translate(g_CivData[playerData.Civ].Name) : playerData.Civ, "state": playerState == "defeated" ? translateWithContext("playerstate", "defeated") : translateWithContext("playerstate", "won"), - "AIname": isAI ? translateAIName(playerData.AI) : "", - "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : "", - "AIbehavior": isAI ? translateAIBehavior(playerData.AIBehavior) : "" + "AIdescription": translateAISettings(playerData) })); } let teams = Object.keys(playerDescriptions); if (teams.indexOf("observer") > -1) teams.splice(teams.indexOf("observer"), 1); let teamDescription = []; // If there are no teams, merge all playersDescriptions if (teams.length == 1) teamDescription.push(playerDescriptions[teams[0]].join("\n")); // If there are teams, merge "Team N:" + playerDescriptions else teamDescription = teams.map(team => { let teamCaption = team == -1 ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 }); // Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + teamCaption + "[/font]", "playerDescriptions": playerDescriptions[team].join("\n") }); }); if (playerDescriptions.observer) teamDescription.push(sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + translatePlural("Observer", "Observers", playerDescriptions.observer.length) + "[/font]", "playerDescriptions": playerDescriptions.observer.join("\n") })); return teamDescription.join("\n\n"); } /** * Sets an additional map label, map preview image and describes the chosen gamesettings more closely. * * Requires g_GameAttributes and g_VictoryConditions. */ function getGameDescription(extended = false) { let titles = []; let victoryIdx = g_VictoryConditions.Name.indexOf(g_GameAttributes.settings.GameType || g_VictoryConditions.Default); if (victoryIdx != -1) { let title = g_VictoryConditions.Title[victoryIdx]; if (g_VictoryConditions.Name[victoryIdx] == "wonder") title = sprintf( translatePluralWithContext( "victory condition", "Wonder (%(min)s minute)", "Wonder (%(min)s minutes)", g_GameAttributes.settings.WonderDuration ), { "min": g_GameAttributes.settings.WonderDuration } ); let isCaptureTheRelic = g_VictoryConditions.Name[victoryIdx] == "capture_the_relic"; if (isCaptureTheRelic) title = sprintf( translatePluralWithContext( "victory condition", "Capture the Relic (%(min)s minute)", "Capture the Relic (%(min)s minutes)", g_GameAttributes.settings.RelicDuration ), { "min": g_GameAttributes.settings.RelicDuration } ); titles.push({ "label": title, "value": g_VictoryConditions.Description[victoryIdx] }); if (isCaptureTheRelic) titles.push({ "label": translate("Relic Count"), "value": g_GameAttributes.settings.RelicCount }); if (g_VictoryConditions.Name[victoryIdx] == "regicide") if (g_GameAttributes.settings.RegicideGarrison) titles.push({ "label": translate("Hero Garrison"), "value": translate("Heroes can be garrisoned.") }); else titles.push({ "label": translate("Exposed Heroes"), "value": translate("Heroes cannot be garrisoned, and they are vulnerable to raids.") }); } if (g_GameAttributes.settings.RatingEnabled && g_GameAttributes.settings.PlayerData.length == 2) titles.push({ "label": translate("Rated game"), "value": translate("When the winner of this match is determined, the lobby score will be adapted.") }); if (g_GameAttributes.settings.LockTeams) titles.push({ "label": translate("Locked Teams"), "value": translate("Players can't change the initial teams.") }); else titles.push({ "label": translate("Diplomacy"), "value": translate("Players can make alliances and declare war on allies.") }); if (g_GameAttributes.settings.LastManStanding) titles.push({ "label": translate("Last Man Standing"), "value": translate("Only one player can win the game. If the remaining players are allies, the game continues until only one remains.") }); else titles.push({ "label": translate("Allied Victory"), "value": translate("If one player wins, his or her allies win too. If one group of allies remains, they win.") }); if (extended) { titles.push({ "label": translate("Ceasefire"), "value": g_GameAttributes.settings.Ceasefire == 0 ? translate("disabled") : sprintf(translatePlural( "For the first minute, other players will stay neutral.", "For the first %(min)s minutes, other players will stay neutral.", g_GameAttributes.settings.Ceasefire), { "min": g_GameAttributes.settings.Ceasefire }) }); titles.push({ "label": translate("Map Name"), "value": translate(g_GameAttributes.settings.Name) }); titles.push({ "label": translate("Map Type"), "value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)] }); if (g_GameAttributes.mapType == "random") { let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)]; if (mapSize) titles.push({ "label": translate("Map Size"), "value": mapSize }); } } titles.push({ "label": translate("Map Description"), "value": g_GameAttributes.map == "random" ? translate("Randomly selects a map from the list") : g_GameAttributes.settings.Description ? translate(g_GameAttributes.settings.Description) : translate("Sorry, no description available.") }); if (g_GameAttributes.settings.Biome) { let biome = g_Settings.Biomes.find(b => b.Id == g_GameAttributes.settings.Biome); titles.push({ "label": translate("Biome"), "value": biome ? biome.Title : translateWithContext("biome", "Random") }); } if (extended) { titles.push({ "label": translate("Starting Resources"), "value": sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), { "startingResourcesTitle": g_StartingResources.Title[ g_StartingResources.Resources.indexOf( g_GameAttributes.settings.StartingResources)], "amount": g_GameAttributes.settings.StartingResources }) }); titles.push({ "label": translate("Population Limit"), "value": g_PopulationCapacities.Title[ g_PopulationCapacities.Population.indexOf( g_GameAttributes.settings.PopulationCap)] }); titles.push({ "label": translate("Treasures"), "value": g_GameAttributes.settings.DisableTreasures ? translateWithContext("treasures", "Disabled") : translateWithContext("treasures", "As defined by the map.") }); titles.push({ "label": translate("Revealed Map"), "value": g_GameAttributes.settings.RevealMap }); titles.push({ "label": translate("Explored Map"), "value": g_GameAttributes.settings.ExploreMap }); titles.push({ "label": translate("Cheats"), "value": g_GameAttributes.settings.CheatsEnabled }); } return titles.map(title => sprintf(translate("%(label)s %(details)s"), { "label": coloredText(title.label, g_DescriptionHighlight), "details": title.value === true ? translateWithContext("gamesetup option", "enabled") : title.value || translateWithContext("gamesetup option", "disabled") })).join("\n"); } /** * Sets the win/defeat icon to indicate current player's state. * @param {string} state - The current in-game state of the player. * @param {string} imageID - The name of the XML image object to update. */ function setOutcomeIcon(state, imageID) { let image = Engine.GetGUIObjectByName(imageID); if (state == "won") { image.sprite = "stretched:session/icons/victory.png"; image.tooltip = translate("Victorious"); } else if (state == "defeated") { image.sprite = "stretched:session/icons/defeat.png"; image.tooltip = translate("Defeated"); } } + +function translateAISettings(playerData) +{ + if (!playerData.AI) + return ""; + + return sprintf(translate("%(AIdifficulty)s %(AIbehavior)s %(AIname)s"), { + "AIname": translateAIName(playerData.AI), + "AIdifficulty": translateAIDifficulty(playerData.AIDiff), + "AIbehavior": translateAIBehavior(playerData.AIBehavior), + }); +} Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 20704) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 20705) @@ -1,2453 +1,2451 @@ const g_MatchSettings_SP = "config/matchsettings.json"; const g_MatchSettings_MP = "config/matchsettings.mp.json"; const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions); const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations); var g_GameSpeeds = getGameSpeedChoices(false); /** * Offer users to select playable civs only. * Load unselectable civs as they could appear in scenario maps. */ const g_CivData = loadCivData(false, false); /** * Highlight the "random" dropdownlist item. */ var g_ColorRandom = "orange"; /** * Color for regular dropdownlist items. */ var g_ColorRegular = "white"; /** * Color for "Unassigned"-placeholder item in the dropdownlist. */ var g_PlayerAssignmentColors = { "player": g_ColorRegular, "observer": "170 170 250", "unassigned": "140 140 140", "AI": "70 150 70" }; /** * Used for highlighting the sender of chat messages. */ var g_SenderFont = "sans-bold-13"; /** * This yields [1, 2, ..., MaxPlayers]. */ var g_NumPlayersList = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); /** * Used for generating the botnames. */ var g_RomanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"]; var g_PlayerTeamList = prepareForDropdown([{ "label": translateWithContext("team", "None"), "id": -1 }].concat( Array(g_MaxTeams).fill(0).map((v, i) => ({ "label": i + 1, "id": i })) ) ); /** * Number of relics: [1, ..., NumCivs] */ var g_RelicCountList = Object.keys(g_CivData).map((civ, i) => i + 1); var g_PlayerCivList = g_CivData && prepareForDropdown([{ "name": translateWithContext("civilization", "Random"), "tooltip": translate("Picks one civilization at random when the game starts."), "color": g_ColorRandom, "code": "random" }].concat( Object.keys(g_CivData).filter( civ => g_CivData[civ].SelectableInGameSetup ).map(civ => ({ "name": g_CivData[civ].Name, "tooltip": g_CivData[civ].History, "color": g_ColorRegular, "code": civ })).sort(sortNameIgnoreCase) ) ); /** * All selectable playercolors except gaia. */ var g_PlayerColorPickerList = g_Settings && g_Settings.PlayerDefaults.slice(1).map(pData => pData.Color); /** * Directory containing all maps of the given type. */ var g_MapPath = { "random": "maps/random/", "scenario": "maps/scenarios/", "skirmish": "maps/skirmishes/" }; /** * Containing the colors to highlight the ready status of players, * the chat ready messages and * the tooltips and captions for the ready button */ var g_ReadyData = [ { "color": g_ColorRegular, "chat": translate("* %(username)s is not ready."), "caption": translate("I'm ready"), "tooltip": translate("State that you are ready to play.") }, { "color": "green", "chat": translate("* %(username)s is ready!"), "caption": translate("Stay ready"), "tooltip": translate("Stay ready even when the game settings change.") }, { "color": "150 150 250", "chat": "", "caption": translate("I'm not ready!"), "tooltip": translate("State that you are not ready to play.") } ]; /** * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. */ var g_NetMessageTypes = { "netstatus": msg => handleNetStatusMessage(msg), "netwarn": msg => addNetworkWarning(msg), "gamesetup": msg => handleGamesetupMessage(msg), "players": msg => handlePlayerAssignmentMessage(msg), "ready": msg => handleReadyMessage(msg), "start": msg => handleGamestartMessage(msg), "kicked": msg => addChatMessage({ "type": msg.banned ? "banned" : "kicked", "username": msg.username }), "chat": msg => addChatMessage({ "type": "chat", "guid": msg.guid, "text": msg.text }), }; var g_FormatChatMessage = { "system": (msg, user) => systemMessage(msg.text), "settings": (msg, user) => systemMessage(translate('Game settings have been changed')), "connect": (msg, user) => systemMessage(sprintf(translate("%(username)s has joined"), { "username": user })), "disconnect": (msg, user) => systemMessage(sprintf(translate("%(username)s has left"), { "username": user })), "kicked": (msg, user) => systemMessage(sprintf(translate("%(username)s has been kicked"), { "username": user })), "banned": (msg, user) => systemMessage(sprintf(translate("%(username)s has been banned"), { "username": user })), "chat": (msg, user) => sprintf(translate("%(username)s %(message)s"), { "username": senderFont(sprintf(translate("<%(username)s>"), { "username": user })), "message": escapeText(msg.text || "") }), "ready": (msg, user) => sprintf(g_ReadyData[msg.status].chat, { "username": user }), "clientlist": (msg, user) => getUsernameList(), }; var g_MapFilters = [ { "id": "default", "name": translateWithContext("map filter", "Default"), "tooltip": translateWithContext("map filter", "All maps except naval and demo maps."), "filter": mapKeywords => mapKeywords.every(keyword => ["naval", "demo", "hidden"].indexOf(keyword) == -1), "Default": true }, { "id": "naval", "name": translate("Naval Maps"), "tooltip": translateWithContext("map filter", "Maps where ships are needed to reach the enemy."), "filter": mapKeywords => mapKeywords.indexOf("naval") != -1 }, { "id": "demo", "name": translate("Demo Maps"), "tooltip": translateWithContext("map filter", "These maps are not playable but for demonstration purposes only."), "filter": mapKeywords => mapKeywords.indexOf("demo") != -1 }, { "id": "new", "name": translate("New Maps"), "tooltip": translateWithContext("map filter", "Maps that are brand new in this release of the game."), "filter": mapKeywords => mapKeywords.indexOf("new") != -1 }, { "id": "trigger", "name": translate("Trigger Maps"), "tooltip": translateWithContext("map filter", "Maps that come with scripted events and potentially spawn enemy units."), "filter": mapKeywords => mapKeywords.indexOf("trigger") != -1 }, { "id": "all", "name": translate("All Maps"), "tooltip": translateWithContext("map filter", "Every map of the chosen maptype."), "filter": mapKeywords => true }, ]; /** * This contains only filters that have at least one map matching them. */ var g_MapFilterList; /** * Array of biome identifiers supported by the currently selected map. */ var g_BiomeList; /** * Whether this is a single- or multiplayer match. */ var g_IsNetworked; /** * Is this user in control of game settings (i.e. singleplayer or host of a multiplayergame). */ var g_IsController; /** * Whether this is a tutorial. */ var g_IsTutorial; /** * To report the game to the lobby bot. */ var g_ServerName; var g_ServerPort; /** * IP address and port of the STUN endpoint. */ var g_StunEndpoint; /** * Current username. Cannot contain whitespace. */ var g_Username = Engine.LobbyGetNick(); /** * States whether the GUI is currently updated in response to network messages instead of user input * and therefore shouldn't send further messages to the network. */ var g_IsInGuiUpdate = false; /** * Whether the current player is ready to start the game. * 0 - not ready * 1 - ready * 2 - stay ready */ var g_IsReady = 0; /** * Ignore duplicate ready commands on init. */ var g_ReadyInit = true; /** * If noone has changed the ready status, we have no need to spam the settings changed message. * * <=0 - Suppressed settings message * 1 - Will show settings message * 2 - Host's initial ready, suppressed settings message */ var g_ReadyChanged = 2; /** * Used to prevent calling resetReadyData when starting a game. */ var g_GameStarted = false; /** * Selectable options (player, AI, unassigned) in the player assignment dropdowns and * their colorized, textual representation. */ var g_PlayerAssignmentList = {}; /** * Remembers which clients are assigned to which player slots and whether they are ready. * The keys are guids or "local" in Singleplayer. */ var g_PlayerAssignments = {}; var g_DefaultPlayerData = []; var g_GameAttributes = { "settings": {} }; /** * List of translated words that can be used to autocomplete titles of settings * and their values (for example playernames). */ var g_Autocomplete = []; /** * Array of strings formatted as displayed, including playername. */ var g_ChatMessages = []; /** * Filename and translated title of all maps, given the currently selected * maptype and filter. Sorted by title, shown in the dropdown. */ var g_MapSelectionList = []; /** * Cache containing the mapsettings. Just-in-time loading. */ var g_MapData = {}; /** * Wait one tick before initializing the GUI objects and * don't process netmessages prior to that. */ var g_LoadingState = 0; /** * Send the current gamesettings to the lobby bot if the settings didn't change for this number of seconds. */ var g_GameStanzaTimeout = 2; /** * Index of the GUI timer. */ var g_GameStanzaTimer; /** * Only send a lobby update if something actually changed. */ var g_LastGameStanza; /** * Remembers if the current player viewed the AI settings of some playerslot. */ var g_LastViewedAIPlayer = -1; /** * Total number of units that the engine can run with smoothly. * It means a 4v4 with 150 population can still run nicely, but more than that might "lag". */ var g_PopulationCapacityRecommendation = 1200; /** * Order in which the GUI elements will be shown. * All valid options are required to appear here. * The ones under "map" are shown in the map selection panel, * the others in the "more options" dialog. */ var g_OptionOrderGUI = { "map": [ "mapType", "mapFilter", "mapSelection", "numPlayers", "mapSize" ], "more": [ "biome", "gameSpeed", "victoryCondition", "relicCount", "relicDuration", "wonderDuration", "populationCap", "startingResources", "ceasefire", "regicideGarrison", "exploreMap", "revealMap", "disableTreasures", "disableSpies", "lockTeams", "lastManStanding", "enableCheats", "enableRating" ] }; /** * Contains the logic of all multiple-choice gamesettings. * * Logic * ids - Array of identifier strings that indicate the selected value. * default - Returns the index of the default value (not the value itself). * defined - Whether a value for the setting is actually specified. * get - The identifier of the currently selected value. * select - Saves and processes the value of the selected index of the ids array. * * GUI * title - The caption shown in the label. * tooltip - A description shown when hovering the dropdown or a specific item. * labels - Array of translated strings selectable for this dropdown. * colors - Optional array of colors to tint the according dropdown items with. * hidden - If hidden, both the label and dropdown won't be visible. (default: false) * enabled - Only the label will be shown if the setting is disabled. (default: true) * autocomplete - Marks whether to autocomplete translated values of the string. (default: undefined) * If not undefined, must be a number that denotes the priority (higher numbers come first). * If undefined, still autocompletes the translated title of the setting. * initOrder - Options with lower values will be initialized first. */ var g_Dropdowns = { "mapType": { "title": () => translate("Map Type"), "tooltip": (hoverIdx) => g_MapTypes.Description[hoverIdx] || translate("Select a map type."), "labels": () => g_MapTypes.Title, "ids": () => g_MapTypes.Name, "default": () => g_MapTypes.Default, "defined": () => g_GameAttributes.mapType !== undefined, "get": () => g_GameAttributes.mapType, "select": (itemIdx) => { g_MapData = {}; g_GameAttributes.mapType = g_MapTypes.Name[itemIdx]; g_GameAttributes.mapPath = g_MapPath[g_GameAttributes.mapType]; delete g_GameAttributes.map; if (g_GameAttributes.mapType != "scenario") g_GameAttributes.settings = { "PlayerData": clone(g_DefaultPlayerData.slice(0, 4)) }; reloadMapFilterList(); }, "autocomplete": 0, "initOrder": 1 }, "mapFilter": { "title": () => translate("Map Filter"), "tooltip": (hoverIdx) => g_MapFilterList.tooltip[hoverIdx] || translate("Select a map filter."), "labels": () => g_MapFilterList.name, "ids": () => g_MapFilterList.id, "default": () => g_MapFilterList.Default, "defined": () => g_MapFilterList.id.indexOf(g_GameAttributes.mapFilter || "") != -1, "get": () => g_GameAttributes.mapFilter, "select": (itemIdx) => { g_GameAttributes.mapFilter = g_MapFilterList.id[itemIdx]; delete g_GameAttributes.map; reloadMapList(); }, "autocomplete": 0, "initOrder": 2 }, "mapSelection": { "title": () => translate("Select Map"), "tooltip": (hoverIdx) => g_MapSelectionList.description[hoverIdx] || translate("Select a map to play on."), "labels": () => g_MapSelectionList.name, "colors": () => g_MapSelectionList.color, "ids": () => g_MapSelectionList.file, "default": () => 0, "defined": () => g_GameAttributes.map !== undefined, "get": () => g_GameAttributes.map, "select": (itemIdx) => { selectMap(g_MapSelectionList.file[itemIdx]); }, "autocomplete": 0, "initOrder": 3 }, "mapSize": { "title": () => translate("Map Size"), "tooltip": (hoverIdx) => g_MapSizes.Tooltip[hoverIdx] || translate("Select map size. (Larger sizes may reduce performance.)"), "labels": () => g_MapSizes.Name, "ids": () => g_MapSizes.Tiles, "default": () => g_MapSizes.Default, "defined": () => g_GameAttributes.settings.Size !== undefined, "get": () => g_GameAttributes.settings.Size, "select": (itemIdx) => { g_GameAttributes.settings.Size = g_MapSizes.Tiles[itemIdx]; }, "hidden": () => g_GameAttributes.mapType != "random", "autocomplete": 0, "initOrder": 1000 }, "biome": { "title": () => translate("Biome"), "tooltip": (hoverIdx) => g_BiomeList && g_BiomeList.Description && g_BiomeList.Description[hoverIdx] || translate("Select the flora and fauna."), "labels": () => g_BiomeList ? g_BiomeList.Title : [], "colors": (itemIdx) => g_BiomeList ? g_BiomeList.Color : [], "ids": () => g_BiomeList ? g_BiomeList.Id : [], "default": () => 0, "defined": () => g_GameAttributes.settings.Biome !== undefined, "get": () => g_GameAttributes.settings.Biome, "select": (itemIdx) => { g_GameAttributes.settings.Biome = g_BiomeList && g_BiomeList.Id[itemIdx]; }, "hidden": () => !g_BiomeList, "autocomplete": 0, "initOrder": 1000 }, "numPlayers": { "title": () => translate("Number of Players"), "tooltip": (hoverIdx) => translate("Select number of players."), "labels": () => g_NumPlayersList, "ids": () => g_NumPlayersList, "default": () => g_MaxPlayers - 1, "defined": () => g_GameAttributes.settings.PlayerData !== undefined, "get": () => g_GameAttributes.settings.PlayerData.length, "enabled": () => g_GameAttributes.mapType == "random", "select": (itemIdx) => { let num = itemIdx + 1; let pData = g_GameAttributes.settings.PlayerData; g_GameAttributes.settings.PlayerData = num > pData.length ? pData.concat(clone(g_DefaultPlayerData.slice(pData.length, num))) : pData.slice(0, num); unassignInvalidPlayers(num); sanitizePlayerData(g_GameAttributes.settings.PlayerData); }, "initOrder": 1000 }, "populationCap": { "title": () => translate("Population Cap"), "tooltip": (hoverIdx) => { let popCap = g_PopulationCapacities.Population[hoverIdx]; let players = g_GameAttributes.settings.PlayerData.length; if (hoverIdx == -1 || popCap * players <= g_PopulationCapacityRecommendation) return translate("Select population limit."); return coloredText( sprintf(translate("Warning: There might be performance issues if all %(players)s players reach %(popCap)s population."), { "players": players, "popCap": popCap }), "orange"); }, "labels": () => g_PopulationCapacities.Title, "ids": () => g_PopulationCapacities.Population, "default": () => g_PopulationCapacities.Default, "defined": () => g_GameAttributes.settings.PopulationCap !== undefined, "get": () => g_GameAttributes.settings.PopulationCap, "select": (itemIdx) => { g_GameAttributes.settings.PopulationCap = g_PopulationCapacities.Population[itemIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "startingResources": { "title": () => translate("Starting Resources"), "tooltip": (hoverIdx) => { return hoverIdx >= 0 ? sprintf(translate("Initial amount of each resource: %(resources)s."), { "resources": g_StartingResources.Resources[hoverIdx] }) : translate("Select the game's starting resources."); }, "labels": () => g_StartingResources.Title, "ids": () => g_StartingResources.Resources, "default": () => g_StartingResources.Default, "defined": () => g_GameAttributes.settings.StartingResources !== undefined, "get": () => g_GameAttributes.settings.StartingResources, "select": (itemIdx) => { g_GameAttributes.settings.StartingResources = g_StartingResources.Resources[itemIdx]; }, "hidden": () => g_GameAttributes.mapType == "scenario", "autocomplete": 0, "initOrder": 1000 }, "ceasefire": { "title": () => translate("Ceasefire"), "tooltip": (hoverIdx) => translate("Set time where no attacks are possible."), "labels": () => g_Ceasefire.Title, "ids": () => g_Ceasefire.Duration, "default": () => g_Ceasefire.Default, "defined": () => g_GameAttributes.settings.Ceasefire !== undefined, "get": () => g_GameAttributes.settings.Ceasefire, "select": (itemIdx) => { g_GameAttributes.settings.Ceasefire = g_Ceasefire.Duration[itemIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "victoryCondition": { "title": () => translate("Victory Condition"), "tooltip": (hoverIdx) => g_VictoryConditions.Description[hoverIdx] || translate("Select victory condition."), "labels": () => g_VictoryConditions.Title, "ids": () => g_VictoryConditions.Name, "default": () => g_VictoryConditions.Default, "defined": () => g_GameAttributes.settings.GameType !== undefined, "get": () => g_GameAttributes.settings.GameType, "select": (itemIdx) => { g_GameAttributes.settings.GameType = g_VictoryConditions.Name[itemIdx]; g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[itemIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "autocomplete": 0, "initOrder": 1000 }, "relicCount": { "title": () => translate("Relic Count"), "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."), "labels": () => g_RelicCountList, "ids": () => g_RelicCountList, "default": () => g_RelicCountList.indexOf(2), "defined": () => g_GameAttributes.settings.RelicCount !== undefined, "get": () => g_GameAttributes.settings.RelicCount, "select": (itemIdx) => { g_GameAttributes.settings.RelicCount = g_RelicCountList[itemIdx]; }, "hidden": () => g_GameAttributes.settings.GameType != "capture_the_relic", "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "relicDuration": { "title": () => translate("Relic Duration"), "tooltip": (hoverIdx) => translate("Minutes until the player has achieved Relic Victory."), "labels": () => g_VictoryDurations.Title, "ids": () => g_VictoryDurations.Duration, "default": () => g_VictoryDurations.Default, "defined": () => g_GameAttributes.settings.RelicDuration !== undefined, "get": () => g_GameAttributes.settings.RelicDuration, "select": (itemIdx) => { g_GameAttributes.settings.RelicDuration = g_VictoryDurations.Duration[itemIdx]; }, "hidden": () => g_GameAttributes.settings.GameType != "capture_the_relic", "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "wonderDuration": { "title": () => translate("Wonder Duration"), "tooltip": (hoverIdx) => translate("Minutes until the player has achieved Wonder Victory."), "labels": () => g_VictoryDurations.Title, "ids": () => g_VictoryDurations.Duration, "default": () => g_VictoryDurations.Default, "defined": () => g_GameAttributes.settings.WonderDuration !== undefined, "get": () => g_GameAttributes.settings.WonderDuration, "select": (itemIdx) => { g_GameAttributes.settings.WonderDuration = g_VictoryDurations.Duration[itemIdx]; }, "hidden": () => g_GameAttributes.settings.GameType != "wonder", "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "gameSpeed": { "title": () => translate("Game Speed"), "tooltip": (hoverIdx) => translate("Select game speed."), "labels": () => g_GameSpeeds.Title, "ids": () => g_GameSpeeds.Speed, "default": () => g_GameSpeeds.Default, "defined": () => g_GameAttributes.gameSpeed !== undefined && g_GameSpeeds.Speed.indexOf(g_GameAttributes.gameSpeed) != -1, "get": () => g_GameAttributes.gameSpeed, "select": (itemIdx) => { g_GameAttributes.gameSpeed = g_GameSpeeds.Speed[itemIdx]; }, "initOrder": 1000 }, }; /** * These dropdowns provide a setting that is repeated once for each player * (where playerIdx is starting from 0 for player 1). */ var g_PlayerDropdowns = { "playerAssignment": { "labels": (playerIdx) => g_PlayerAssignmentList.Name || [], "colors": (playerIdx) => g_PlayerAssignmentList.Color || [], "ids": (playerIdx) => g_PlayerAssignmentList.Choice || [], "default": (playerIdx) => "ai:petra", "defined": (playerIdx) => playerIdx < g_GameAttributes.settings.PlayerData.length, "get": (playerIdx) => { for (let guid in g_PlayerAssignments) if (g_PlayerAssignments[guid].player == playerIdx + 1) return "guid:" + guid; for (let ai of g_Settings.AIDescriptions) if (g_GameAttributes.settings.PlayerData[playerIdx].AI == ai.id) return "ai:" + ai.id; return "unassigned"; }, "select": (selectedIdx, playerIdx) => { let choice = g_PlayerAssignmentList.Choice[selectedIdx]; if (choice == "unassigned" || choice.startsWith("ai:")) { if (g_IsNetworked) Engine.AssignNetworkPlayer(playerIdx+1, ""); else if (g_PlayerAssignments.local.player == playerIdx+1) g_PlayerAssignments.local.player = -1; g_GameAttributes.settings.PlayerData[playerIdx].AI = choice.startsWith("ai:") ? choice.substr(3) : ""; } else swapPlayers(choice.substr("guid:".length), playerIdx); }, "autocomplete": 100, }, "playerTeam": { "labels": (playerIdx) => g_PlayerTeamList.label, "ids": (playerIdx) => g_PlayerTeamList.id, "default": (playerIdx) => 0, "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team !== undefined, "get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Team, "select": (selectedIdx, playerIdx) => { g_GameAttributes.settings.PlayerData[playerIdx].Team = selectedIdx - 1; }, "enabled": () => g_GameAttributes.mapType != "scenario", }, "playerCiv": { "tooltip": (hoverIdx, playerIdx) => g_PlayerCivList.tooltip[hoverIdx] || translate("Chose the civilization for this player"), "labels": (playerIdx) => g_PlayerCivList.name, "colors": (playerIdx) => g_PlayerCivList.color, "ids": (playerIdx) => g_PlayerCivList.code, "default": (playerIdx) => 0, "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ !== undefined, "get": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Civ, "select": (selectedIdx, playerIdx) => { g_GameAttributes.settings.PlayerData[playerIdx].Civ = g_PlayerCivList.code[selectedIdx]; }, "enabled": () => g_GameAttributes.mapType != "scenario", "autocomplete": 0, }, "playerColorPicker": { "labels": (playerIdx) => g_PlayerColorPickerList.map(color => "■"), "colors": (playerIdx) => g_PlayerColorPickerList.map(color => rgbToGuiColor(color)), "ids": (playerIdx) => g_PlayerColorPickerList.map((color, index) => index), "default": (playerIdx) => playerIdx, "defined": (playerIdx) => g_GameAttributes.settings.PlayerData[playerIdx].Color !== undefined, "get": (playerIdx) => g_PlayerColorPickerList.indexOf(g_GameAttributes.settings.PlayerData[playerIdx].Color), "select": (selectedIdx, playerIdx) => { let playerData = g_GameAttributes.settings.PlayerData; // If someone else has that color, give that player the old color let sameColorPData = playerData.find(pData => sameColor(g_PlayerColorPickerList[selectedIdx], pData.Color)); if (sameColorPData) sameColorPData.Color = playerData[playerIdx].Color; playerData[playerIdx].Color = g_PlayerColorPickerList[selectedIdx]; ensureUniquePlayerColors(playerData); }, "enabled": () => g_GameAttributes.mapType != "scenario", }, }; /** * Contains the logic of all boolean gamesettings. */ var g_Checkboxes = { "regicideGarrison": { "title": () => translate("Hero Garrison"), "tooltip": () => translate("Toggle whether heroes can be garrisoned."), "default": () => false, "defined": () => g_GameAttributes.settings.RegicideGarrison !== undefined, "get": () => g_GameAttributes.settings.RegicideGarrison, "set": checked => { g_GameAttributes.settings.RegicideGarrison = checked; }, "hidden": () => g_GameAttributes.settings.GameType != "regicide", "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "revealMap": { "title": () => // Translation: Make sure to differentiate between the revealed map and explored map options! translate("Revealed Map"), "tooltip": // Translation: Make sure to differentiate between the revealed map and explored map options! () => translate("Toggle revealed map (see everything)."), "default": () => false, "defined": () => g_GameAttributes.settings.RevealMap !== undefined, "get": () => g_GameAttributes.settings.RevealMap, "set": checked => { g_GameAttributes.settings.RevealMap = checked; if (checked) g_Checkboxes.exploreMap.set(true); }, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "exploreMap": { "title": // Translation: Make sure to differentiate between the revealed map and explored map options! () => translate("Explored Map"), "tooltip": // Translation: Make sure to differentiate between the revealed map and explored map options! () => translate("Toggle explored map (see initial map)."), "default": () => false, "defined": () => g_GameAttributes.settings.ExploreMap !== undefined, "get": () => g_GameAttributes.settings.ExploreMap, "set": checked => { g_GameAttributes.settings.ExploreMap = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RevealMap, "initOrder": 1000 }, "disableTreasures": { "title": () => translate("Disable Treasures"), "tooltip": () => translate("Disable all treasures on the map."), "default": () => false, "defined": () => g_GameAttributes.settings.DisableTreasures !== undefined, "get": () => g_GameAttributes.settings.DisableTreasures, "set": checked => { g_GameAttributes.settings.DisableTreasures = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "disableSpies": { "title": () => translate("Disable Spies"), "tooltip": () => translate("Disable spies during the game."), "default": () => false, "defined": () => g_GameAttributes.settings.DisableSpies !== undefined, "get": () => g_GameAttributes.settings.DisableSpies, "set": checked => { g_GameAttributes.settings.DisableSpies = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario", "initOrder": 1000 }, "lockTeams": { "title": () => translate("Teams Locked"), "tooltip": () => translate("Toggle locked teams."), "default": () => Engine.HasXmppClient(), "defined": () => g_GameAttributes.settings.LockTeams !== undefined, "get": () => g_GameAttributes.settings.LockTeams, "set": checked => { g_GameAttributes.settings.LockTeams = checked; g_GameAttributes.settings.LastManStanding = false; }, "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.RatingEnabled, "initOrder": 1000 }, "lastManStanding": { "title": () => translate("Last Man Standing"), "tooltip": () => translate("Toggle whether the last remaining player or the last remaining set of allies wins."), "default": () => false, "defined": () => g_GameAttributes.settings.LastManStanding !== undefined, "get": () => g_GameAttributes.settings.LastManStanding, "set": checked => { g_GameAttributes.settings.LastManStanding = checked; }, "enabled": () => g_GameAttributes.mapType != "scenario" && !g_GameAttributes.settings.LockTeams, "initOrder": 1000 }, "enableCheats": { "title": () => translate("Cheats"), "tooltip": () => translate("Toggle the usability of cheats."), "default": () => !g_IsNetworked, "hidden": () => !g_IsNetworked, "defined": () => g_GameAttributes.settings.CheatsEnabled !== undefined, "get": () => g_GameAttributes.settings.CheatsEnabled, "set": checked => { g_GameAttributes.settings.CheatsEnabled = !g_IsNetworked || checked && !g_GameAttributes.settings.RatingEnabled; }, "enabled": () => !g_GameAttributes.settings.RatingEnabled, "initOrder": 1000 }, "enableRating": { "title": () => translate("Rated Game"), "tooltip": () => translate("Toggle if this game will be rated for the leaderboard."), "default": () => Engine.HasXmppClient(), "hidden": () => !Engine.HasXmppClient(), "defined": () => g_GameAttributes.settings.RatingEnabled !== undefined, "get": () => !!g_GameAttributes.settings.RatingEnabled, "set": checked => { g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient() ? checked : undefined; Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled); if (checked) { g_Checkboxes.lockTeams.set(true); g_Checkboxes.enableCheats.set(false); } }, "initOrder": 1000 }, }; /** * For setting up arbitrary GUI objects. */ var g_MiscControls = { "chatPanel": { "hidden": () => !g_IsNetworked, }, "chatInput": { "tooltip": () => colorizeAutocompleteHotkey(translate("Press %(hotkey)s to autocomplete playernames or settings.")), }, "cheatWarningText": { "hidden": () => !g_IsNetworked || !g_GameAttributes.settings.CheatsEnabled, }, "cancelGame": { "tooltip": () => Engine.HasXmppClient() ? translate("Return to the lobby.") : translate("Return to the main menu."), }, "startGame": { "caption": () => g_IsController ? translate("Start Game!") : g_ReadyData[g_IsReady].caption, "tooltip": (hoverIdx) => !g_IsController ? g_ReadyData[g_IsReady].tooltip : !g_IsNetworked || Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1) ? translate("Start a new game with the current settings.") : translate("Start a new game with the current settings (disabled until all players are ready)"), "enabled": () => !g_IsController || Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].status || g_PlayerAssignments[guid].player == -1 || guid == Engine.GetPlayerGUID() && g_IsController), "hidden": () => !g_PlayerAssignments[Engine.GetPlayerGUID()] || g_PlayerAssignments[Engine.GetPlayerGUID()].player == -1 && !g_IsController, }, "civResetButton": { "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController, }, "teamResetButton": { "hidden": () => g_GameAttributes.mapType == "scenario" || !g_IsController, }, // Display these after having hidden every GUI object in the "More Options" dialog "moreOptionsLabel": { "hidden": () => false, }, "hideMoreOptions": { "hidden": () => false, }, }; /** * Contains gui elements that are repeated for every player. */ var g_PlayerMiscElements = { "playerBox": { "size": (playerIdx) => ["0", 32 * playerIdx, "100%", 32 * (playerIdx + 1)].join(" "), }, "playerName": { "caption": (playerIdx) => { let pData = g_GameAttributes.settings.PlayerData[playerIdx]; let assignedGUID = Object.keys(g_PlayerAssignments).find( guid => g_PlayerAssignments[guid].player == playerIdx + 1); let name = translate(pData.Name || g_DefaultPlayerData[playerIdx].Name); if (g_IsNetworked) name = coloredText(name, g_ReadyData[assignedGUID ? g_PlayerAssignments[assignedGUID].status : 2].color); return name; }, }, "playerColor": { "sprite": (playerIdx) => "color:" + rgbToGuiColor(g_GameAttributes.settings.PlayerData[playerIdx].Color) + " 100", }, "playerConfig": { "hidden": (playerIdx) => !g_GameAttributes.settings.PlayerData[playerIdx].AI, "onPress": (playerIdx) => function() { openAIConfig(playerIdx); }, - "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(difficulty)s %(behavior)s %(name)s."), { - "name": translateAIName(g_GameAttributes.settings.PlayerData[playerIdx].AI), - "difficulty": translateAIDifficulty(g_GameAttributes.settings.PlayerData[playerIdx].AIDiff), - "behavior": translateAIBehavior(g_GameAttributes.settings.PlayerData[playerIdx].AIBehavior), + "tooltip": (playerIdx) => sprintf(translate("Configure AI: %(description)s."), { + "description": translateAISettings(g_GameAttributes.settings.PlayerData[playerIdx]) }), }, }; /** * Initializes some globals without touching the GUI. * * @param {Object} attribs - context data sent by the lobby / mainmenu */ function init(attribs) { if (!g_Settings) { cancelSetup(); return; } if (["offline", "server", "client"].indexOf(attribs.type) == -1) { error("Unexpected 'type' in gamesetup init: " + attribs.type); cancelSetup(); return; } g_IsNetworked = attribs.type != "offline"; g_IsController = attribs.type != "client"; g_IsTutorial = !!attribs.tutorial; g_ServerName = attribs.serverName; g_ServerPort = attribs.serverPort; g_StunEndpoint = attribs.stunEndpoint; if (!g_IsNetworked) g_PlayerAssignments = { "local": { "name": singleplayerName(), "player": 1 } }; // Replace empty playername when entering a singleplayermatch for the first time if (!g_IsNetworked) { Engine.ConfigDB_CreateValue("user", "playername.singleplayer", singleplayerName()); Engine.ConfigDB_WriteValueToFile("user", "playername.singleplayer", singleplayerName(), "config/user.cfg"); } initDefaults(); supplementDefaults(); setTimeout(displayGamestateNotifications, 1000); } function initDefaults() { // Remove gaia from both arrays g_DefaultPlayerData = clone(g_Settings.PlayerDefaults.slice(1)); let aiDifficulty = +Engine.ConfigDB_GetValue("user", "gui.gamesetup.aidifficulty"); let aiBehavior = Engine.ConfigDB_GetValue("user", "gui.gamesetup.aibehavior"); // Don't change the underlying defaults file, as Atlas uses that file too for (let i in g_DefaultPlayerData) { g_DefaultPlayerData[i].Civ = "random"; g_DefaultPlayerData[i].Team = -1; g_DefaultPlayerData[i].AIDiff = aiDifficulty; g_DefaultPlayerData[i].AIBehavior = aiBehavior; } deepfreeze(g_DefaultPlayerData); } /** * Sets default values for all g_GameAttribute settings which don't have a value set. */ function supplementDefaults() { for (let dropdown in g_Dropdowns) if (!g_Dropdowns[dropdown].defined()) g_Dropdowns[dropdown].select(g_Dropdowns[dropdown].default()); for (let checkbox in g_Checkboxes) if (!g_Checkboxes[checkbox].defined()) g_Checkboxes[checkbox].set(g_Checkboxes[checkbox].default()); for (let dropdown in g_PlayerDropdowns) for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i) if (!isControlArrayElementHidden(i) && !g_PlayerDropdowns[dropdown].defined(i)) g_PlayerDropdowns[dropdown].select(g_PlayerDropdowns[dropdown].default(i), i); } /** * Called after the first tick. */ function initGUIObjects() { // Copy all initOrder values into one object let initOrder = {}; for (let dropdown in g_Dropdowns) initOrder[dropdown] = g_Dropdowns[dropdown].initOrder; for (let checkbox in g_Checkboxes) initOrder[checkbox] = g_Checkboxes[checkbox].initOrder; // Sort the object on initOrder so we can init the options in an arbitrary order for (let option of Object.keys(initOrder).sort((a, b) => initOrder[a] - initOrder[b])) if (g_Dropdowns[option]) initDropdown(option); else if (g_Checkboxes[option]) initCheckbox(option); else warn('The option "' + option + '" is not defined.'); for (let dropdown in g_PlayerDropdowns) initPlayerDropdowns(dropdown); resizeMoreOptionsWindow(); initSPTips(); loadPersistMatchSettings(); updateGameAttributes(); sendRegisterGameStanzaImmediate(); if (g_IsTutorial) { launchTutorial(); return; } // Don't lift the curtain until the controls are updated the first time if (!g_IsNetworked) hideLoadingWindow(); } function hideLoadingWindow() { let loadingWindow = Engine.GetGUIObjectByName("loadingWindow"); if (loadingWindow.hidden) return; loadingWindow.hidden = true; Engine.GetGUIObjectByName("setupWindow").hidden = false; Engine.GetGUIObjectByName("chatInput").focus(); } /** * Options in the "More Options" or "Map" panel use a generic name. * Player settings use custom names. */ function getGUIObjectNameFromSetting(name) { for (let panel in g_OptionOrderGUI) { let idx = g_OptionOrderGUI[panel].indexOf(name); if (idx != -1) return [ panel + "Option", g_Dropdowns[name] ? "Dropdown" : "Checkbox", "[" + idx + "]" ]; } // Assume there is a GUI object with exactly that setting name return [name, "", ""]; } function initDropdown(name, playerIdx) { let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name); let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]"; let data = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name]; let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName); dropdown.list = data.labels(playerIdx).map((label, id) => data.colors && data.colors(playerIdx) ? coloredText(label, data.colors(playerIdx)[id]) : label); dropdown.list_data = data.ids(playerIdx); dropdown.onSelectionChange = function() { if (!g_IsController || g_IsInGuiUpdate || !this.list_data[this.selected] || data.hidden && data.hidden(playerIdx) || data.enabled && !data.enabled(playerIdx)) return; data.select(this.selected, playerIdx); supplementDefaults(); updateGameAttributes(); }; if (data.tooltip) dropdown.onHoverChange = function() { this.tooltip = data.tooltip(this.hovered, playerIdx); }; } function initPlayerDropdowns(name) { for (let i = 0; i < g_MaxPlayers; ++i) initDropdown(name, i); } function initCheckbox(name) { let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name); Engine.GetGUIObjectByName(guiName + guiType + guiIdx).onPress = function() { let obj = g_Checkboxes[name]; if (!g_IsController || g_IsInGuiUpdate || obj.enabled && !obj.enabled() || obj.hidden && obj.hidden()) return; obj.set(this.checked); supplementDefaults(); updateGameAttributes(); }; } function initSPTips() { if (g_IsNetworked || Engine.ConfigDB_GetValue("user", "gui.gamesetup.enabletips") !== "true") return; Engine.GetGUIObjectByName("spTips").hidden = false; Engine.GetGUIObjectByName("displaySPTips").checked = true; Engine.GetGUIObjectByName("aiTips").caption = Engine.TranslateLines(Engine.ReadFile("gui/gamesetup/ai.txt")); } function saveSPTipsSetting() { let enabled = String(Engine.GetGUIObjectByName("displaySPTips").checked); Engine.ConfigDB_CreateValue("user", "gui.gamesetup.enabletips", enabled); Engine.ConfigDB_WriteValueToFile("user", "gui.gamesetup.enabletips", enabled, "config/user.cfg"); } function verticallyDistributeGUIObjects(parent, objectHeight, ignore) { let yPos; let parentObject = Engine.GetGUIObjectByName(parent); for (let child of parentObject.children) { if (ignore.indexOf(child.name) != -1) continue; let childSize = child.size; yPos = yPos || childSize.top; if (child.hidden) continue; childSize.top = yPos; childSize.bottom = yPos + objectHeight - 2; child.size = childSize; yPos += objectHeight; } return yPos; } /** * Remove empty space in case of hidden options (like cheats, rating or victory duration) */ function resizeMoreOptionsWindow() { verticallyDistributeGUIObjects("mapOptions", 32, []); let yPos = verticallyDistributeGUIObjects("moreOptions", 32, ["moreOptionsLabel"]); // Resize the vertically centered window containing the options let moreOptions = Engine.GetGUIObjectByName("moreOptions"); let mSize = moreOptions.size; mSize.bottom = mSize.top + yPos + 20; moreOptions.size = mSize; } /** * Called when the client disconnects. * The other cases from NetClient should never occur in the gamesetup. */ function handleNetStatusMessage(message) { if (message.status != "disconnected") { error("Unrecognised netstatus type " + message.status); return; } cancelSetup(); reportDisconnect(message.reason, true); } /** * Called whenever a client clicks on ready (or not ready). */ function handleReadyMessage(message) { --g_ReadyChanged; if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1) addChatMessage({ "type": "ready", "status": message.status, "guid": message.guid }); g_PlayerAssignments[message.guid].status = message.status; updateGUIObjects(); } /** * Called after every player is ready and the host decided to finally start the game. */ function handleGamestartMessage(message) { // Immediately inform the lobby server instead of waiting for the load to finish if (g_IsController && Engine.HasXmppClient()) { sendRegisterGameStanzaImmediate(); let clients = formatClientsForStanza(); Engine.SendChangeStateGame(clients.connectedPlayers, clients.list); } Engine.SwitchGuiPage("page_loading.xml", { "attribs": g_GameAttributes, "isNetworked": g_IsNetworked, "playerAssignments": g_PlayerAssignments, "isController": g_IsController }); } /** * Called whenever the host changed any setting. */ function handleGamesetupMessage(message) { if (!message.data) return; g_GameAttributes = message.data; if (!!g_GameAttributes.settings.RatingEnabled) { g_GameAttributes.settings.CheatsEnabled = false; g_GameAttributes.settings.LockTeams = true; g_GameAttributes.settings.LastManStanding = false; } Engine.SetRankedGame(!!g_GameAttributes.settings.RatingEnabled); resetReadyData(); updateGUIObjects(); hideLoadingWindow(); } /** * Called whenever a client joins/leaves or any gamesetting is changed. */ function handlePlayerAssignmentMessage(message) { let playerChange = false; for (let guid in message.newAssignments) if (!g_PlayerAssignments[guid]) { onClientJoin(guid, message.newAssignments); playerChange = true; } for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) { onClientLeave(guid); playerChange = true; } g_PlayerAssignments = message.newAssignments; sanitizePlayerData(g_GameAttributes.settings.PlayerData); updateGUIObjects(); if (playerChange) sendRegisterGameStanzaImmediate(); else sendRegisterGameStanza(); } function onClientJoin(newGUID, newAssignments) { let playername = newAssignments[newGUID].name; addChatMessage({ "type": "connect", "guid": newGUID, "username": playername }); let isRejoiningPlayer = newAssignments[newGUID].player != -1; // Assign the client (or only buddies if prefered) to an unused playerslot and rejoining players to their old slot if (!isRejoiningPlayer && playername != newAssignments[Engine.GetPlayerGUID()].name) { let assignOption = Engine.ConfigDB_GetValue("user", "gui.gamesetup.assignplayers"); if (assignOption == "disabled" || assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(playername).nick) == -1) return; } let freeSlot = g_GameAttributes.settings.PlayerData.findIndex((v, i) => Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player != i + 1) ); // Client is not and cannot become assigned as player if (!isRejoiningPlayer && freeSlot == -1) return; // Assign the joining client to the free slot if (g_IsController && !isRejoiningPlayer) Engine.AssignNetworkPlayer(freeSlot + 1, newGUID); resetReadyData(); } function onClientLeave(guid) { addChatMessage({ "type": "disconnect", "guid": guid }); if (g_PlayerAssignments[guid].player != -1) resetReadyData(); } /** * Doesn't translate, so that lobby clients can do that locally * (even if they don't have that map). */ function getMapDisplayName(map) { if (map == "random") return map; let mapData = loadMapData(map); if (!mapData || !mapData.settings || !mapData.settings.Name) return map; return mapData.settings.Name; } function getMapPreview(map) { let mapData = loadMapData(map); if (!mapData || !mapData.settings || !mapData.settings.Preview) return "nopreview.png"; return mapData.settings.Preview; } /** * Filter maps with filterFunc and by chosen map type. * * @param {function} filterFunc - Filter function that should be applied. * @return {Array} the maps that match the filterFunc and the chosen map type. */ function getFilteredMaps(filterFunc) { if (!g_MapPath[g_GameAttributes.mapType]) { error("Unexpected map type: " + g_GameAttributes.mapType); return []; } let maps = []; // TODO: Should verify these are valid maps before adding to list for (let mapFile of listFiles(g_GameAttributes.mapPath, g_GameAttributes.mapType == "random" ? ".json" : ".xml", false)) { if (mapFile.startsWith("_")) continue; let file = g_GameAttributes.mapPath + mapFile; let mapData = loadMapData(file); if (!mapData.settings || filterFunc && !filterFunc(mapData.settings.Keywords || [])) continue; maps.push({ "file": file, "name": translate(getMapDisplayName(file)), "color": g_ColorRegular, "description": translate(mapData.settings.Description) }); } return maps; } /** * Initialize the dropdown containing all map filters for the selected maptype. */ function reloadMapFilterList() { g_MapFilterList = prepareForDropdown(g_MapFilters.filter( mapFilter => getFilteredMaps(mapFilter.filter).length )); initDropdown("mapFilter"); reloadMapList(); } /** * Initialize the dropdown containing all maps for the selected maptype and mapfilter. */ function reloadMapList() { let filterID = g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter); let filterFunc = g_MapFilterList.filter[filterID]; let mapList = getFilteredMaps(filterFunc).sort(sortNameIgnoreCase); if (g_GameAttributes.mapType == "random") mapList.unshift({ "file": "random", "name": translateWithContext("map selection", "Random"), "color": g_ColorRandom, "description": translate("Pick any of the given maps at random.") }); g_MapSelectionList = prepareForDropdown(mapList); initDropdown("mapSelection"); } function reloadBiomeList() { let biomeList; if (g_GameAttributes.mapType == "random" && g_GameAttributes.settings.SupportedBiomes) { if (g_GameAttributes.settings.SupportedBiomes === true) biomeList = g_Settings.Biomes; else biomeList = g_Settings.Biomes.filter( biome => g_GameAttributes.settings.SupportedBiomes.indexOf(biome.Id) != -1); } g_BiomeList = biomeList && prepareForDropdown( [{ "Id": "random", "Title": translateWithContext("biome", "Random"), "Description": translate("Pick a biome at random."), "Color": g_ColorRandom }].concat(biomeList.map(biome => ({ "Id": biome.Id, "Title": biome.Title, "Description": biome.Description, "Color": g_ColorRegular })))); initDropdown("biome"); } function reloadGameSpeedChoices() { g_GameSpeeds = getGameSpeedChoices(Object.keys(g_PlayerAssignments).every(guid => g_PlayerAssignments[guid].player == -1)); initDropdown("gameSpeed"); supplementDefaults(); } function loadMapData(name) { if (!name || !g_MapPath[g_GameAttributes.mapType]) return undefined; if (name == "random") return { "settings": { "Name": "", "Description": "" } }; if (!g_MapData[name]) g_MapData[name] = g_GameAttributes.mapType == "random" ? Engine.ReadJSONFile(name + ".json") : Engine.LoadMapSettings(name); return g_MapData[name]; } /** * Sets the gameattributes the way they were the last time the user left the gamesetup. */ function loadPersistMatchSettings() { if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial) return; let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP; if (!Engine.FileExists(settingsFile)) return; let attrs = Engine.ReadJSONFile(settingsFile); if (!attrs || !attrs.settings) return; g_IsInGuiUpdate = true; let mapName = attrs.map || ""; let mapSettings = attrs.settings; g_GameAttributes = attrs; if (!g_IsNetworked) mapSettings.CheatsEnabled = true; // Replace unselectable civs with random civ let playerData = mapSettings.PlayerData; if (playerData && g_GameAttributes.mapType != "scenario") for (let i in playerData) if (!g_CivData[playerData[i].Civ] || !g_CivData[playerData[i].Civ].SelectableInGameSetup) playerData[i].Civ = "random"; // Apply map settings let newMapData = loadMapData(mapName); if (newMapData && newMapData.settings) { for (let prop in newMapData.settings) mapSettings[prop] = newMapData.settings[prop]; if (playerData) mapSettings.PlayerData = playerData; } if (mapSettings.PlayerData) sanitizePlayerData(mapSettings.PlayerData); // Reload, as the maptype or mapfilter might have changed reloadMapFilterList(); reloadBiomeList(); g_GameAttributes.settings.RatingEnabled = Engine.HasXmppClient(); Engine.SetRankedGame(g_GameAttributes.settings.RatingEnabled); supplementDefaults(); g_IsInGuiUpdate = false; } function savePersistMatchSettings() { if (g_IsTutorial) return; let attributes = Engine.ConfigDB_GetValue("user", "persistmatchsettings") == "true" ? g_GameAttributes : {}; Engine.WriteJSONFile(g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP, attributes); } function sanitizePlayerData(playerData) { // Remove gaia if (playerData.length && !playerData[0]) playerData.shift(); playerData.forEach((pData, index) => { // Use defaults if the map doesn't specify a value for (let prop in g_DefaultPlayerData[index]) if (!(prop in pData)) pData[prop] = clone(g_DefaultPlayerData[index][prop]); // Replace colors with the best matching color of PlayerDefaults if (g_GameAttributes.mapType != "scenario") { let colorDistances = g_PlayerColorPickerList.map(color => colorDistance(color, pData.Color)); let smallestDistance = colorDistances.find(distance => colorDistances.every(distance2 => (distance2 >= distance))); pData.Color = g_PlayerColorPickerList.find(color => colorDistance(color, pData.Color) == smallestDistance); } // If there is a player in that slot, then there can't be an AI if (Object.keys(g_PlayerAssignments).some(guid => g_PlayerAssignments[guid].player == index + 1)) pData.AI = ""; }); ensureUniquePlayerColors(playerData); } function cancelSetup() { if (g_IsController) savePersistMatchSettings(); Engine.DisconnectNetworkGame(); if (Engine.HasXmppClient()) { Engine.LobbySetPlayerPresence("available"); if (g_IsController) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_lobby.xml"); } else Engine.SwitchGuiPage("page_pregame.xml"); } /** * Can't init the GUI before the first tick. * Process netmessages afterwards. */ function onTick() { if (!g_Settings) return; // First tick happens before first render, so don't load yet if (g_LoadingState == 0) ++g_LoadingState; else if (g_LoadingState == 1) { initGUIObjects(); ++g_LoadingState; } else if (g_LoadingState == 2) handleNetMessages(); updateTimers(); } /** * Handles all pending messages sent by the net client. */ function handleNetMessages() { while (g_IsNetworked) { let message = Engine.PollNetworkClient(); if (!message) break; log("Net message: " + uneval(message)); if (g_NetMessageTypes[message.type]) g_NetMessageTypes[message.type](message); else error("Unrecognised net message type " + message.type); } } /** * Called when the map or the number of players changes. */ function unassignInvalidPlayers(maxPlayers) { if (g_IsNetworked) // Remove invalid playerIDs from the servers playerassignments copy for (let playerID = +maxPlayers + 1; playerID <= g_MaxPlayers; ++playerID) Engine.AssignNetworkPlayer(playerID, ""); else if (g_PlayerAssignments.local.player > maxPlayers) g_PlayerAssignments.local.player = -1; } function ensureUniquePlayerColors(playerData) { for (let i = playerData.length - 1; i >= 0; --i) // If someone else has that color, assign an unused color if (playerData.some((pData, j) => i != j && sameColor(playerData[i].Color, pData.Color))) playerData[i].Color = g_PlayerColorPickerList.find(color => playerData.every(pData => !sameColor(color, pData.Color))); } function selectMap(name) { // Reset some map specific properties which are not necessarily redefined on each map for (let prop of ["TriggerScripts", "CircularMap", "Garrison", "DisabledTemplates", "Biome", "SupportedBiomes"]) g_GameAttributes.settings[prop] = undefined; let mapData = loadMapData(name); let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {}; // Reset victory conditions if (g_GameAttributes.mapType != "random") { let victoryIdx = g_VictoryConditions.Name.indexOf(mapSettings.GameType || "") != -1 ? g_VictoryConditions.Name.indexOf(mapSettings.GameType) : g_VictoryConditions.Default; g_GameAttributes.settings.GameType = g_VictoryConditions.Name[victoryIdx]; g_GameAttributes.settings.VictoryScripts = g_VictoryConditions.Scripts[victoryIdx]; } if (g_GameAttributes.mapType == "scenario") { delete g_GameAttributes.settings.RelicDuration; delete g_GameAttributes.settings.WonderDuration; delete g_GameAttributes.settings.LastManStanding; delete g_GameAttributes.settings.RegicideGarrison; } if (mapSettings.PlayerData) sanitizePlayerData(mapSettings.PlayerData); // Copy any new settings g_GameAttributes.map = name; g_GameAttributes.script = mapSettings.Script; if (g_GameAttributes.map !== "random") for (let prop in mapSettings) g_GameAttributes.settings[prop] = mapSettings[prop]; reloadBiomeList(); unassignInvalidPlayers(g_GameAttributes.settings.PlayerData.length); supplementDefaults(); } function isControlArrayElementHidden(playerIdx) { return playerIdx !== undefined && playerIdx >= g_GameAttributes.settings.PlayerData.length; } /** * @param playerIdx - Only specified for dropdown arrays. */ function updateGUIDropdown(name, playerIdx = undefined) { let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name); let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]"; let dropdown = Engine.GetGUIObjectByName(guiName + guiType + guiIdx + idxName); let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx + idxName); let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx + idxName); let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx + idxName); if (guiType == "Dropdown") Engine.GetGUIObjectByName(guiName + "Checkbox" + guiIdx).hidden = true; let indexHidden = isControlArrayElementHidden(playerIdx); let obj = (playerIdx === undefined ? g_Dropdowns : g_PlayerDropdowns)[name]; let selected = indexHidden ? -1 : dropdown.list_data.indexOf(String(obj.get(playerIdx))); let enabled = !indexHidden && (!obj.enabled || obj.enabled(playerIdx)); let hidden = indexHidden || obj.hidden && obj.hidden(playerIdx); dropdown.hidden = !g_IsController || !enabled || hidden; dropdown.selected = selected; dropdown.tooltip = !indexHidden && obj.tooltip ? obj.tooltip(-1, playerIdx) : ""; if (frame) frame.hidden = hidden; if (title && obj.title && !indexHidden) title.caption = sprintf(translate("%(option)s:"), { "option": obj.title(playerIdx) }); if (label && !indexHidden) { label.hidden = g_IsController && enabled || hidden; label.caption = selected == -1 ? translateWithContext("option value", "Unknown") : dropdown.list[selected]; } } /** * Not used for the player assignments, so playerCheckboxes are not implemented, * hence no index. */ function updateGUICheckbox(name) { let obj = g_Checkboxes[name]; let checked = obj.get(); let hidden = obj.hidden && obj.hidden(); let enabled = !obj.enabled || obj.enabled(); let [guiName, guiType, guiIdx] = getGUIObjectNameFromSetting(name); let checkbox = Engine.GetGUIObjectByName(guiName + guiType + guiIdx); let label = Engine.GetGUIObjectByName(guiName + "Text" + guiIdx); let frame = Engine.GetGUIObjectByName(guiName + "Frame" + guiIdx); let title = Engine.GetGUIObjectByName(guiName + "Title" + guiIdx); if (guiType == "Checkbox") Engine.GetGUIObjectByName(guiName + "Dropdown" + guiIdx).hidden = true; checkbox.checked = checked; checkbox.enabled = enabled; checkbox.hidden = hidden || !g_IsController; checkbox.tooltip = obj.tooltip ? obj.tooltip() : ""; label.caption = checked ? translate("Yes") : translate("No"); label.hidden = hidden || g_IsController; if (frame) frame.hidden = hidden; if (title && obj.title) title.caption = sprintf(translate("%(option)s:"), { "option": obj.title() }); } function updateGUIMiscControl(name, playerIdx) { let idxName = playerIdx === undefined ? "" : "[" + playerIdx + "]"; let obj = (playerIdx === undefined ? g_MiscControls : g_PlayerMiscElements)[name]; let control = Engine.GetGUIObjectByName(name + idxName); if (!control) warn("No GUI object with name '" + name + "'"); let hide = isControlArrayElementHidden(playerIdx); control.hidden = hide; if (hide) return; for (let property in obj) control[property] = obj[property](playerIdx); } function launchGame() { if (!g_IsController) { error("Only host can start game"); return; } if (!g_GameAttributes.map) return; savePersistMatchSettings(); // Select random map if (g_GameAttributes.map == "random") { let victoryScriptsSelected = g_GameAttributes.settings.VictoryScripts; let gameTypeSelected = g_GameAttributes.settings.GameType; selectMap(pickRandom(g_Dropdowns.mapSelection.ids().slice(1))); g_GameAttributes.settings.VictoryScripts = victoryScriptsSelected; g_GameAttributes.settings.GameType = gameTypeSelected; } if (g_GameAttributes.settings.Biome == "random") g_GameAttributes.settings.Biome = pickRandom( g_GameAttributes.settings.SupportedBiomes === true ? g_BiomeList.Id.slice(1) : g_GameAttributes.settings.SupportedBiomes); g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts || []); // Prevent reseting the readystate g_GameStarted = true; g_GameAttributes.settings.mapType = g_GameAttributes.mapType; // Get a unique array of selectable cultures let cultures = Object.keys(g_CivData).filter(civ => g_CivData[civ].SelectableInGameSetup).map(civ => g_CivData[civ].Culture); cultures = cultures.filter((culture, index) => cultures.indexOf(culture) === index); // Determine random civs and botnames for (let i in g_GameAttributes.settings.PlayerData) { // Pick a random civ of a random culture let chosenCiv = g_GameAttributes.settings.PlayerData[i].Civ || "random"; if (chosenCiv == "random") { let culture = pickRandom(cultures); chosenCiv = pickRandom(Object.keys(g_CivData).filter(civ => g_CivData[civ].Culture == culture)); } g_GameAttributes.settings.PlayerData[i].Civ = chosenCiv; // Pick one of the available botnames for the chosen civ if (g_GameAttributes.mapType === "scenario" || !g_GameAttributes.settings.PlayerData[i].AI) continue; let chosenName = pickRandom(g_CivData[chosenCiv].AINames); if (!g_IsNetworked) chosenName = translate(chosenName); // Count how many players use the chosenName let usedName = g_GameAttributes.settings.PlayerData.filter(pData => pData.Name && pData.Name.indexOf(chosenName) !== -1).length; g_GameAttributes.settings.PlayerData[i].Name = !usedName ? chosenName : sprintf(translate("%(playerName)s %(romanNumber)s"), { "playerName": chosenName, "romanNumber": g_RomanNumbers[usedName+1] }); } // Copy playernames for the purpose of replays for (let guid in g_PlayerAssignments) { let player = g_PlayerAssignments[guid]; if (player.player > 0) // not observer or GAIA g_GameAttributes.settings.PlayerData[player.player - 1].Name = player.name; } // Seed used for both map generation and simulation g_GameAttributes.settings.Seed = randIntExclusive(0, Math.pow(2, 32)); g_GameAttributes.settings.AISeed = randIntExclusive(0, Math.pow(2, 32)); // Used for identifying rated game reports for the lobby g_GameAttributes.matchID = Engine.GetMatchID(); if (g_IsNetworked) { Engine.SetNetworkGameAttributes(g_GameAttributes); Engine.StartNetworkGame(); } else { // Find the player ID which the user has been assigned to let playerID = -1; for (let i in g_GameAttributes.settings.PlayerData) { let assignBox = Engine.GetGUIObjectByName("playerAssignment[" + i + "]"); if (assignBox.list_data[assignBox.selected] == "guid:local") playerID = +i + 1; } Engine.StartGame(g_GameAttributes, playerID); Engine.SwitchGuiPage("page_loading.xml", { "attribs": g_GameAttributes, "isNetworked": g_IsNetworked, "playerAssignments": g_PlayerAssignments }); } } function launchTutorial() { g_GameAttributes.mapType = "scenario"; selectMap("maps/tutorials/starting_economy_walkthrough"); launchGame(); } /** * Don't set any attributes here, just show the changes in the GUI. * * Unless the mapsettings don't specify a property and the user didn't set it in g_GameAttributes previously. */ function updateGUIObjects() { g_IsInGuiUpdate = true; reloadMapFilterList(); reloadBiomeList(); reloadGameSpeedChoices(); reloadPlayerAssignmentChoices(); // Hide exceeding dropdowns and checkboxes for (let panel in g_OptionOrderGUI) for (let child of Engine.GetGUIObjectByName(panel + "Options").children) child.hidden = true; // Show the relevant ones for (let name in g_Dropdowns) updateGUIDropdown(name); for (let name in g_Checkboxes) updateGUICheckbox(name); for (let i = 0; i < g_MaxPlayers; ++i) { for (let name in g_PlayerDropdowns) updateGUIDropdown(name, i); for (let name in g_PlayerMiscElements) updateGUIMiscControl(name, i); } for (let name in g_MiscControls) updateGUIMiscControl(name); updateGameDescription(); resizeMoreOptionsWindow(); rightAlignCancelButton(); updateAutocompleteEntries(); g_IsInGuiUpdate = false; // Refresh AI config page if (g_LastViewedAIPlayer != -1) { Engine.PopGuiPage(); openAIConfig(g_LastViewedAIPlayer); } } function rightAlignCancelButton() { let offset = 10; let startGame = Engine.GetGUIObjectByName("startGame"); let right = startGame.hidden ? startGame.size.right : startGame.size.left - offset; let cancelGame = Engine.GetGUIObjectByName("cancelGame"); let cancelGameSize = cancelGame.size; let buttonWidth = cancelGameSize.right - cancelGameSize.left; cancelGameSize.right = right; right -= buttonWidth; for (let element of ["cheatWarningText", "onscreenToolTip"]) { let elementSize = Engine.GetGUIObjectByName(element).size; elementSize.right = right - (cancelGameSize.left - elementSize.right); Engine.GetGUIObjectByName(element).size = elementSize; } cancelGameSize.left = right; cancelGame.size = cancelGameSize; } function updateGameDescription() { setMapPreviewImage("mapPreview", getMapPreview(g_GameAttributes.map)); Engine.GetGUIObjectByName("mapInfoName").caption = translateMapTitle(getMapDisplayName(g_GameAttributes.map)); Engine.GetGUIObjectByName("mapInfoDescription").caption = getGameDescription(); } /** * Broadcast the changed settings to all clients and the lobbybot. */ function updateGameAttributes() { if (g_IsInGuiUpdate || !g_IsController) return; if (g_IsNetworked) { Engine.SetNetworkGameAttributes(g_GameAttributes); if (g_LoadingState >= 2) sendRegisterGameStanza(); resetReadyData(); } else updateGUIObjects(); } function openAIConfig(playerSlot) { g_LastViewedAIPlayer = playerSlot; Engine.PushGuiPage("page_aiconfig.xml", { "callback": "AIConfigCallback", "isController": g_IsController, "playerSlot": playerSlot, "id": g_GameAttributes.settings.PlayerData[playerSlot].AI, "difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff, "behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior }); } /** * Called after closing the dialog. */ function AIConfigCallback(ai) { g_LastViewedAIPlayer = -1; if (!ai.save || !g_IsController) return; g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id; g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty; g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior; updateGameAttributes(); } function reloadPlayerAssignmentChoices() { let playerChoices = sortGUIDsByPlayerID().map(guid => ({ "Choice": "guid:" + guid, "Color": g_PlayerAssignments[guid].player == -1 ? g_PlayerAssignmentColors.observer : g_PlayerAssignmentColors.player, "Name": g_PlayerAssignments[guid].name })); // Only display hidden AIs if the map preselects them let aiChoices = g_Settings.AIDescriptions .filter(ai => !ai.data.hidden || g_GameAttributes.settings.PlayerData.some(pData => pData.AI == ai.id)) .map(ai => ({ "Choice": "ai:" + ai.id, "Name": sprintf(translate("AI: %(ai)s"), { "ai": translate(ai.data.name) }), "Color": g_PlayerAssignmentColors.AI })); let unassignedSlot = [{ "Choice": "unassigned", "Name": translate("Unassigned"), "Color": g_PlayerAssignmentColors.unassigned }]; g_PlayerAssignmentList = prepareForDropdown(playerChoices.concat(aiChoices).concat(unassignedSlot)); initPlayerDropdowns("playerAssignment"); } function swapPlayers(guidToSwap, newSlot) { // Player slots are indexed from 0 as Gaia is omitted. let newPlayerID = newSlot + 1; let playerID = g_PlayerAssignments[guidToSwap].player; // Attempt to swap the player or AI occupying the target slot, // if any, into the slot this player is currently in. if (playerID != -1) { for (let guid in g_PlayerAssignments) { // Move the player in the destination slot into the current slot. if (g_PlayerAssignments[guid].player != newPlayerID) continue; if (g_IsNetworked) Engine.AssignNetworkPlayer(playerID, guid); else g_PlayerAssignments[guid].player = playerID; break; } // Transfer the AI from the target slot to the current slot. g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI; g_GameAttributes.settings.PlayerData[playerID - 1].AIDiff = g_GameAttributes.settings.PlayerData[newSlot].AIDiff; g_GameAttributes.settings.PlayerData[playerID - 1].AIBehavior = g_GameAttributes.settings.PlayerData[newSlot].AIBehavior; // Swap civilizations and colors if they aren't fixed if (g_GameAttributes.mapType != "scenario") { [g_GameAttributes.settings.PlayerData[playerID - 1].Civ, g_GameAttributes.settings.PlayerData[newSlot].Civ] = [g_GameAttributes.settings.PlayerData[newSlot].Civ, g_GameAttributes.settings.PlayerData[playerID - 1].Civ]; [g_GameAttributes.settings.PlayerData[playerID - 1].Color, g_GameAttributes.settings.PlayerData[newSlot].Color] = [g_GameAttributes.settings.PlayerData[newSlot].Color, g_GameAttributes.settings.PlayerData[playerID - 1].Color]; } } if (g_IsNetworked) Engine.AssignNetworkPlayer(newPlayerID, guidToSwap); else g_PlayerAssignments[guidToSwap].player = newPlayerID; g_GameAttributes.settings.PlayerData[newSlot].AI = ""; } function submitChatInput() { let input = Engine.GetGUIObjectByName("chatInput"); let text = input.caption; if (!text.length) return; input.caption = ""; if (executeNetworkCommand(text)) return; Engine.SendNetworkChat(text); } function senderFont(text) { return '[font="' + g_SenderFont + '"]' + text + '[/font]'; } function systemMessage(message) { return senderFont(sprintf(translate("== %(message)s"), { "message": message })); } function colorizePlayernameByGUID(guid, username = "") { // TODO: Maybe the host should have the moderator-prefix? if (!username) username = g_PlayerAssignments[guid] ? escapeText(g_PlayerAssignments[guid].name) : translate("Unknown Player"); let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; let color = g_ColorRegular; if (playerID > 0) { color = g_GameAttributes.settings.PlayerData[playerID - 1].Color; // Enlighten playercolor to improve readability let [h, s, l] = rgbToHsl(color.r, color.g, color.b); let [r, g, b] = hslToRgb(h, s, Math.max(0.6, l)); color = rgbToGuiColor({ "r": r, "g": g, "b": b }); } return coloredText(username, color); } function addChatMessage(msg) { if (!g_FormatChatMessage[msg.type]) return; if (msg.type == "chat") { let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name; if (userName != g_PlayerAssignments[msg.guid].name && msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1) soundNotification("nick"); } let user = colorizePlayernameByGUID(msg.guid || -1, msg.username || ""); let text = g_FormatChatMessage[msg.type](msg, user); if (!text) return; if (Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true") text = sprintf(translate("%(time)s %(message)s"), { "time": sprintf(translate("\\[%(time)s]"), { "time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm")) }), "message": text }); g_ChatMessages.push(text); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } function showMoreOptions(show) { Engine.GetGUIObjectByName("moreOptionsFade").hidden = !show; Engine.GetGUIObjectByName("moreOptions").hidden = !show; } function resetCivilizations() { for (let i in g_GameAttributes.settings.PlayerData) g_GameAttributes.settings.PlayerData[i].Civ = "random"; updateGameAttributes(); } function resetTeams() { for (let i in g_GameAttributes.settings.PlayerData) g_GameAttributes.settings.PlayerData[i].Team = -1; updateGameAttributes(); } function toggleReady() { setReady((g_IsReady + 1) % 3, true); } function setReady(ready, sendMessage) { g_IsReady = ready; if (sendMessage) Engine.SendNetworkReady(g_IsReady); updateGUIObjects(); } function resetReadyData() { if (g_GameStarted) return; if (g_ReadyChanged < 1) addChatMessage({ "type": "settings" }); else if (g_ReadyChanged == 2 && !g_ReadyInit) return; // duplicate calls on init else g_ReadyInit = false; g_ReadyChanged = 2; if (!g_IsNetworked) g_IsReady = 2; else if (g_IsController) { Engine.ClearAllPlayerReady(); setReady(2, true); } else if (g_IsReady != 2) setReady(0, false); } /** * Send a list of playernames and distinct between players and observers. * Don't send teams, AIs or anything else until the game was started. * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data. */ function formatClientsForStanza() { let connectedPlayers = 0; let playerData = []; for (let guid in g_PlayerAssignments) { let pData = { "Name": g_PlayerAssignments[guid].name }; if (g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1]) ++connectedPlayers; else pData.Team = "observer"; playerData.push(pData); } return { "list": playerDataToStringifiedTeamList(playerData), "connectedPlayers": connectedPlayers }; } /** * Send the relevant gamesettings to the lobbybot immediately. */ function sendRegisterGameStanzaImmediate() { if (!g_IsController || !Engine.HasXmppClient()) return; if (g_GameStanzaTimer !== undefined) { clearTimeout(g_GameStanzaTimer); g_GameStanzaTimer = undefined; } let clients = formatClientsForStanza(); let stanza = { "name": g_ServerName, "port": g_ServerPort, "hostUsername": g_Username, "mapName": g_GameAttributes.map, "niceMapName": getMapDisplayName(g_GameAttributes.map), "mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default", "mapType": g_GameAttributes.mapType, "victoryCondition": g_GameAttributes.settings.GameType, "nbp": clients.connectedPlayers, "maxnbp": g_GameAttributes.settings.PlayerData.length, "players": clients.list, "stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "", "stunPort": g_StunEndpoint ? g_StunEndpoint.port : "", }; // Only send the stanza if the relevant settings actually changed if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop])) return; g_LastGameStanza = stanza; Engine.SendRegisterGame(stanza); } /** * Send the relevant gamesettings to the lobbybot in a deferred manner. */ function sendRegisterGameStanza() { if (!g_IsController || !Engine.HasXmppClient()) return; if (g_GameStanzaTimer !== undefined) clearTimeout(g_GameStanzaTimer); g_GameStanzaTimer = setTimeout(sendRegisterGameStanzaImmediate, g_GameStanzaTimeout * 1000); } /** * Figures out all strings that can be autocompleted and sorts * them by priority (so that playernames are always autocompleted first). */ function updateAutocompleteEntries() { let autocomplete = { "0": [] }; for (let control of [g_Dropdowns, g_Checkboxes]) for (let name in control) autocomplete[0] = autocomplete[0].concat(control[name].title()); for (let dropdown of [g_Dropdowns, g_PlayerDropdowns]) for (let name in dropdown) { let priority = dropdown[name].autocomplete; if (priority === undefined) continue; autocomplete[priority] = (autocomplete[priority] || []).concat(dropdown[name].labels()); } g_Autocomplete = Object.keys(autocomplete).sort().reverse().reduce((all, priority) => all.concat(autocomplete[priority]), []); } Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 20704) +++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 20705) @@ -1,1239 +1,1242 @@ // Menu / panel border size var MARGIN = 4; // Includes the main menu button const NUM_BUTTONS = 9; // Regular menu buttons var BUTTON_HEIGHT = 32; // The position where the bottom of the menu will end up (currently 228) const END_MENU_POSITION = (BUTTON_HEIGHT * NUM_BUTTONS) + MARGIN; // Menu starting position: bottom const MENU_BOTTOM = 0; // Menu starting position: top const MENU_TOP = MENU_BOTTOM - END_MENU_POSITION; // Number of pixels per millisecond to move var MENU_SPEED = 1.2; // Trade menu: step for probability changes var STEP = 5; // Shown in the trade dialog. var g_IdleTraderTextColor = "orange"; /** * The barter constants should match with the simulation * Quantity of goods to sell per click. */ const g_BarterResourceSellQuantity = 100; /** * Multiplier to be applied when holding the massbarter hotkey. */ const g_BarterMultiplier = 5; /** * Barter actions, as mapped to the names of GUI Buttons. */ const g_BarterActions = ["Buy", "Sell"]; /** * Currently selected resource type to sell in the barter GUI. */ var g_BarterSell; var g_IsMenuOpen = false; var g_IsDiplomacyOpen = false; var g_IsTradeOpen = false; var g_IsObjectivesOpen = false; /** * Used to disable a specific bribe button for the time we are waiting for the result of the bribe after it was clicked. * It contains an array per viewedPlayer. This array is a list of the players that were bribed. */ var g_BribeButtonsWaiting = {}; /** * Currently viewed summary panel. */ var g_SummarySelectedData = ""; // Redefined every time someone makes a tribute (so we can save some data in a closure). Called in input.js handleInputBeforeGui. var g_FlushTributing = function() {}; function initSessionMenuButtons() { initMenuPosition(); updateGameSpeedControl(); resizeDiplomacyDialog(); resizeTradeDialog(); } function initMenuPosition() { Engine.GetGUIObjectByName("menu").size = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM; } function updateMenuPosition(dt) { let menu = Engine.GetGUIObjectByName("menu"); let maxOffset = g_IsMenuOpen ? END_MENU_POSITION - menu.size.bottom : menu.size.top - MENU_TOP; if (maxOffset <= 0) return; let offset = Math.min(MENU_SPEED * dt, maxOffset) * (g_IsMenuOpen ? +1 : -1); let size = menu.size; size.top += offset; size.bottom += offset; menu.size = size; } // Opens the menu by revealing the screen which contains the menu function openMenu() { g_IsMenuOpen = true; } // Closes the menu and resets position function closeMenu() { g_IsMenuOpen = false; } function toggleMenu() { g_IsMenuOpen = !g_IsMenuOpen; } function optionsMenuButton() { closeOpenDialogs(); openOptions(); } function chatMenuButton() { closeOpenDialogs(); openChat(); } function diplomacyMenuButton() { closeOpenDialogs(); openDiplomacy(); } function pauseMenuButton() { togglePause(); } function resignMenuButton() { closeOpenDialogs(); pauseGame(); messageBox( 400, 200, translate("Are you sure you want to resign?"), translate("Confirmation"), [translate("No"), translate("Yes")], [resumeGame, resignGame] ); } function exitMenuButton() { closeOpenDialogs(); pauseGame(); let messageTypes = { "host": { "caption": translate("Are you sure you want to quit? Leaving will disconnect all other players."), "buttons": [resumeGame, leaveGame] }, "client": { "caption": translate("Are you sure you want to quit?"), "buttons": [resumeGame, resignQuestion] }, "singleplayer": { "caption": translate("Are you sure you want to quit?"), "buttons": [resumeGame, leaveGame] } }; let messageType = g_IsNetworked && g_IsController ? "host" : (g_IsNetworked && !g_IsObserver ? "client" : "singleplayer"); messageBox( 400, 200, messageTypes[messageType].caption, translate("Confirmation"), [translate("No"), translate("Yes")], messageTypes[messageType].buttons ); } function resignQuestion() { messageBox( 400, 200, translate("Do you want to resign or will you return soon?"), translate("Confirmation"), [translate("I will return"), translate("I resign")], [leaveGame, resignGame], [true, false] ); } function openDeleteDialog(selection) { closeOpenDialogs(); let deleteSelectedEntities = function(selectionArg) { Engine.PostNetworkCommand({ "type": "delete-entities", "entities": selectionArg }); }; messageBox( 400, 200, translate("Destroy everything currently selected?"), translate("Delete"), [translate("No"), translate("Yes")], [resumeGame, deleteSelectedEntities], [null, selection] ); } function openSave() { closeOpenDialogs(); pauseGame(); Engine.PushGuiPage("page_savegame.xml", { "savedGameData": getSavedGameData(), "callback": "resumeGame" }); } function openOptions() { closeOpenDialogs(); pauseGame(); Engine.PushGuiPage("page_options.xml", { "callback": "resumeGame" }); } function openChat(command = "") { if (g_Disconnected) return; closeOpenDialogs(); let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); chatAddressee.selected = chatAddressee.list_data.indexOf(command); Engine.GetGUIObjectByName("chatInput").focus(); Engine.GetGUIObjectByName("chatDialogPanel").hidden = false; updateChatHistory(); } function closeChat() { Engine.GetGUIObjectByName("chatInput").caption = ""; Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus Engine.GetGUIObjectByName("chatDialogPanel").hidden = true; } function resizeDiplomacyDialog() { let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel"); let size = dialog.size; let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size; let widthOffset = g_ResourceData.GetCodes().length * (tribSize.right - tribSize.left) / 2; size.left -= widthOffset; size.right += widthOffset; let firstRow = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size; let heightOffset = (g_Players.length - 1) * (firstRow.bottom - firstRow.top) / 2; size.top -= heightOffset; size.bottom += heightOffset; dialog.size = size; } function initChatWindow() { let filters = prepareForDropdown(g_ChatHistoryFilters); let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter"); chatHistoryFilter.list = filters.text; chatHistoryFilter.list_data = filters.key; chatHistoryFilter.selected = 0; Engine.GetGUIObjectByName("extendedChat").checked = Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true"; resizeChatWindow(); } function resizeChatWindow() { // Hide/show the panel let chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage"); let extended = Engine.GetGUIObjectByName("extendedChat").checked; chatHistoryPage.hidden = !extended; // Resize the window let chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel"); if (extended) { chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelLarge").size; // Adjust the width so that the chat history is in the golden ratio let chatHistory = Engine.GetGUIObjectByName("chatHistory"); let height = chatHistory.getComputedSize().bottom - chatHistory.getComputedSize().top; let width = (1 + Math.sqrt(5)) / 2 * height; let size = chatDialogPanel.size; size.left = -width / 2 - chatHistory.size.left; size.right = width / 2 + chatHistory.size.left; chatDialogPanel.size = size; } else chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelSmall").size; } function updateChatHistory() { if (Engine.GetGUIObjectByName("chatDialogPanel").hidden || !Engine.GetGUIObjectByName("extendedChat").checked) return; let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter"); let selected = chatHistoryFilter.list_data[chatHistoryFilter.selected]; Engine.GetGUIObjectByName("chatHistory").caption = g_ChatHistory.filter(msg => msg.filter[selected]).map(msg => Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ? sprintf(translate("%(time)s %(message)s"), { "time": msg.timePrefix, "message": msg.txt }) : msg.txt ).join("\n"); } function onToggleChatWindowExtended() { // Save user preference let extended = Engine.GetGUIObjectByName("extendedChat").checked.toString(); Engine.ConfigDB_CreateValue("user", "chat.session.extended", extended); Engine.ConfigDB_WriteValueToFile("user", "chat.session.extended", extended, "config/user.cfg"); resizeChatWindow(); Engine.GetGUIObjectByName("chatInput").focus(); } function openDiplomacy() { closeOpenDialogs(); if (g_ViewedPlayer < 1) return; g_IsDiplomacyOpen = true; updateDiplomacy(true); Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false; } function closeDiplomacy() { g_IsDiplomacyOpen = false; Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = true; } function toggleDiplomacy() { let open = g_IsDiplomacyOpen; closeOpenDialogs(); if (!open) openDiplomacy(); } function updateDiplomacy(opening = false) { if (g_ViewedPlayer < 1 || !g_IsDiplomacyOpen) return; let simState = GetSimState(); let isCeasefireActive = simState.ceasefireActive; let hasSharedLos = GetSimState().players[g_ViewedPlayer].hasSharedLos; // Get offset for one line let onesize = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size; let rowsize = onesize.bottom - onesize.top; // We don't include gaia for (let i = 1; i < g_Players.length; ++i) { let myself = i == g_ViewedPlayer; let playerInactive = isPlayerObserver(g_ViewedPlayer) || isPlayerObserver(i); let hasAllies = g_Players.filter(player => player.isMutualAlly[g_ViewedPlayer]).length > 1; diplomacySetupTexts(i, rowsize); diplomacyFormatStanceButtons(i, myself || playerInactive || isCeasefireActive || g_Players[g_ViewedPlayer].teamsLocked); // Tribute buttons do not need to be updated onTick, and should not because of massTributing if (opening) diplomacyFormatTributeButtons(i, myself || playerInactive); diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]); diplomacyFormatSpyRequestButton(i, myself || playerInactive || g_Players[i].isMutualAlly[g_ViewedPlayer] && hasSharedLos); } let diplomacyCeasefireCounter = Engine.GetGUIObjectByName("diplomacyCeasefireCounter"); diplomacyCeasefireCounter.caption = sprintf( translateWithContext("ceasefire", "Time remaining until ceasefire is over: %(time)s."), { "time": timeToString(simState.ceasefireTimeRemaining) } ); diplomacyCeasefireCounter.hidden = !isCeasefireActive; } function diplomacySetupTexts(i, rowsize) { // Apply offset let row = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]"); let size = row.size; size.top = rowsize * (i - 1); size.bottom = rowsize * i; row.size = size; row.hidden = false; row.sprite = "color: " + rgbToGuiColor(g_Players[i].color) + " 32"; setOutcomeIcon(g_Players[i].state, "diplomacyPlayerOutcome[" + (i - 1) + "]"); - Engine.GetGUIObjectByName("diplomacyPlayerName[" + (i - 1) + "]").caption = colorizePlayernameByID(i); + let diplomacyPlayerName = Engine.GetGUIObjectByName("diplomacyPlayerName[" + (i - 1) + "]"); + diplomacyPlayerName.caption = colorizePlayernameByID(i); + diplomacyPlayerName.tooltip = translateAISettings(g_GameAttributes.settings.PlayerData[i]); + Engine.GetGUIObjectByName("diplomacyPlayerCiv[" + (i - 1) + "]").caption = g_CivData[g_Players[i].civ].Name; Engine.GetGUIObjectByName("diplomacyPlayerTeam[" + (i - 1) + "]").caption = g_Players[i].team < 0 ? translateWithContext("team", "None") : g_Players[i].team + 1; Engine.GetGUIObjectByName("diplomacyPlayerTheirs[" + (i - 1) + "]").caption = i == g_ViewedPlayer ? "" : g_Players[i].isAlly[g_ViewedPlayer] ? translate("Ally") : g_Players[i].isNeutral[g_ViewedPlayer] ? translate("Neutral") : translate("Enemy"); } function diplomacyFormatStanceButtons(i, hidden) { for (let stance of ["Ally", "Neutral", "Enemy"]) { let button = Engine.GetGUIObjectByName("diplomacyPlayer" + stance + "[" + (i - 1) + "]"); button.hidden = hidden; if (hidden) continue; let isCurrentStance = g_Players[g_ViewedPlayer]["is" + stance][i]; button.caption = isCurrentStance ? translate("x") : ""; button.enabled = controlsPlayer(g_ViewedPlayer) && !isCurrentStance; button.onPress = (function(player, stance) { return function() { Engine.PostNetworkCommand({ "type": "diplomacy", "player": i, "to": stance.toLowerCase() }); }; })(i, stance); } } function diplomacyFormatTributeButtons(i, hidden) { let resCodes = g_ResourceData.GetCodes(); let r = 0; for (let resCode of resCodes) { let button = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]"); if (!button) { warn("Current GUI limits prevent displaying more than " + r + " tribute buttons!"); break; } Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]_image").sprite = "stretched:session/icons/resources/" + resCode + ".png"; button.hidden = hidden; setPanelObjectPosition(button, r, r + 1, 0); ++r; if (hidden) continue; button.enabled = controlsPlayer(g_ViewedPlayer); button.tooltip = formatTributeTooltip(i, resCode, 100); button.onPress = (function(i, resCode, button) { // Shift+click to send 500, shift+click+click to send 1000, etc. // See INPUT_MASSTRIBUTING in input.js let multiplier = 1; return function() { let isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute"); if (isBatchTrainPressed) { inputState = INPUT_MASSTRIBUTING; multiplier += multiplier == 1 ? 4 : 5; } let amounts = {}; for (let res of resCodes) amounts[res] = 0; amounts[resCode] = 100 * multiplier; button.tooltip = formatTributeTooltip(i, resCode, amounts[resCode]); // This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some // evil global variable hackery. g_FlushTributing = function() { Engine.PostNetworkCommand({ "type": "tribute", "player": i, "amounts": amounts }); multiplier = 1; button.tooltip = formatTributeTooltip(i, resCode, 100); }; if (!isBatchTrainPressed) g_FlushTributing(); }; })(i, resCode, button); } } function diplomacyFormatAttackRequestButton(i, hidden) { let button = Engine.GetGUIObjectByName("diplomacyAttackRequest[" + (i - 1) + "]"); button.hidden = hidden; if (hidden) return; button.enabled = controlsPlayer(g_ViewedPlayer); button.tooltip = translate("Request your allies to attack this enemy"); button.onPress = (function(i) { return function() { Engine.PostNetworkCommand({ "type": "attack-request", "source": g_ViewedPlayer, "player": i }); }; })(i); } function diplomacyFormatSpyRequestButton(i, hidden) { let button = Engine.GetGUIObjectByName("diplomacySpyRequest[" + (i - 1) + "]"); let template = GetTemplateData("special/spy"); button.hidden = hidden || !template || GetSimState().players[g_ViewedPlayer].disabledTemplates["special/spy"]; if (button.hidden) return; button.enabled = controlsPlayer(g_ViewedPlayer) && !(g_BribeButtonsWaiting[g_ViewedPlayer] && g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) != -1); let modifier = ""; let tooltips = [translate("Bribe a random unit from this player and share its vision during a limited period.")]; if (!button.enabled) modifier = "color:0 0 0 127:grayscale:"; else { if (template.requiredTechnology) { let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": g_ViewedPlayer }); if (!technologyEnabled) { modifier = "color:0 0 0 127:grayscale:"; button.enabled = false; tooltips.push(getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[g_ViewedPlayer].civ)); } } if (template.cost) { let modifiedTemplate = clone(template); for (let res in template.cost) modifiedTemplate.cost[res] = Math.floor(GetSimState().players[i].spyCostMultiplier * template.cost[res]); tooltips.push(getEntityCostTooltip(modifiedTemplate)); let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": modifiedTemplate.cost, "player": g_ViewedPlayer }); let costRatio = Engine.GetTemplate("special/spy").VisionSharing.FailureCostRatio; if (costRatio > 0) { tooltips.push(translate("A failed bribe will cost you:")); for (let res in modifiedTemplate.cost) modifiedTemplate.cost[res] = Math.floor(costRatio * modifiedTemplate.cost[res]); tooltips.push(getEntityCostTooltip(modifiedTemplate)); } if (neededResources) { if (button.enabled) modifier = resourcesToAlphaMask(neededResources) + ":"; button.enabled = false; tooltips.push(getNeededResourcesTooltip(neededResources)); } } } let icon = Engine.GetGUIObjectByName("diplomacySpyRequestImage[" + (i - 1) + "]"); icon.sprite = modifier + "stretched:session/icons/bribes.png"; button.tooltip = tooltips.filter(tip => tip).join("\n"); button.onPress = (function(i, button) { return function() { Engine.PostNetworkCommand({ "type": "spy-request", "source": g_ViewedPlayer, "player": i }); if (!g_BribeButtonsWaiting[g_ViewedPlayer]) g_BribeButtonsWaiting[g_ViewedPlayer] = []; // Don't push i twice if (g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) == -1) g_BribeButtonsWaiting[g_ViewedPlayer].push(i); diplomacyFormatSpyRequestButton(i, false); }; })(i, button); } function resizeTradeDialog() { let dialog = Engine.GetGUIObjectByName("tradeDialogPanel"); let size = dialog.size; let width = size.right - size.left; let tradeSize = Engine.GetGUIObjectByName("tradeResource[0]").size; width += g_ResourceData.GetCodes().length * (tradeSize.right - tradeSize.left); size.left = -width / 2; size.right = width / 2; dialog.size = size; } function openTrade() { closeOpenDialogs(); if (g_ViewedPlayer < 1) return; g_IsTradeOpen = true; let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer); let button = {}; let resCodes = g_ResourceData.GetCodes(); let currTradeSelection = resCodes[0]; let updateTradeButtons = function() { for (let res in button) { button[res].label.caption = proba[res] + "%"; button[res].sel.hidden = !controlsPlayer(g_ViewedPlayer) || res != currTradeSelection; button[res].up.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 100 || proba[currTradeSelection] == 0; button[res].dn.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 0 || proba[currTradeSelection] == 100; } }; hideRemaining("tradeResources", resCodes.length); Engine.GetGUIObjectByName("tradeHelp").hidden = false; for (let i = 0; i < resCodes.length; ++i) { let resCode = resCodes[i]; let barterResource = Engine.GetGUIObjectByName("barterResource[" + i + "]"); if (!barterResource) { warn("Current GUI limits prevent displaying more than " + i + " resources in the barter dialog!"); break; } // Barter: barterOpenCommon(resCode, i, "barter"); setPanelObjectPosition(barterResource, i, i + 1); // Trade: let tradeResource = Engine.GetGUIObjectByName("tradeResource[" + i + "]"); if (!tradeResource) { warn("Current GUI limits prevent displaying more than " + i + " resources in the trading goods selection dialog!"); break; } setPanelObjectPosition(tradeResource, i, i + 1); let icon = Engine.GetGUIObjectByName("tradeResourceIcon[" + i + "]"); icon.sprite = "stretched:session/icons/resources/" + resCode + ".png"; let buttonUp = Engine.GetGUIObjectByName("tradeArrowUp[" + i + "]"); let buttonDn = Engine.GetGUIObjectByName("tradeArrowDn[" + i + "]"); button[resCode] = { "up": buttonUp, "dn": buttonDn, "label": Engine.GetGUIObjectByName("tradeResourceText[" + i + "]"), "sel": Engine.GetGUIObjectByName("tradeResourceSelection[" + i + "]") }; proba[resCode] = proba[resCode] || 0; let buttonResource = Engine.GetGUIObjectByName("tradeResourceButton[" + i + "]"); buttonResource.enabled = controlsPlayer(g_ViewedPlayer); buttonResource.onPress = (resource => { return () => { if (Engine.HotkeyIsPressed("session.fulltradeswap")) { for (let res of resCodes) proba[res] = 0; proba[resource] = 100; Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); } currTradeSelection = resource; updateTradeButtons(); }; })(resCode); buttonUp.enabled = controlsPlayer(g_ViewedPlayer); buttonUp.onPress = (resource => { return () => { proba[resource] += Math.min(STEP, proba[currTradeSelection]); proba[currTradeSelection] -= Math.min(STEP, proba[currTradeSelection]); Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); updateTradeButtons(); }; })(resCode); buttonDn.enabled = controlsPlayer(g_ViewedPlayer); buttonDn.onPress = (resource => { return () => { proba[currTradeSelection] += Math.min(STEP, proba[resource]); proba[resource] -= Math.min(STEP, proba[resource]); Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); updateTradeButtons(); }; })(resCode); } updateTradeButtons(); updateTraderTexts(); Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false; } function updateTraderTexts() { let traderNumber = Engine.GuiInterfaceCall("GetTraderNumber", g_ViewedPlayer); Engine.GetGUIObjectByName("traderCountText").caption = getIdleLandTradersText(traderNumber) + "\n\n" + getIdleShipTradersText(traderNumber); } /** * Code common to both the Barter Panel and the Trade/Barter Dialog, that * only needs to be run when the panel or dialog is opened by the player. * * @param {string} resourceCode * @param {number} idx - Element index within its set * @param {string} prefix - Common prefix of the gui elements to be worked upon */ function barterOpenCommon(resourceCode, idx, prefix) { let barterButton = {}; for (let action of g_BarterActions) barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]"); let resource = resourceNameWithinSentence(resourceCode); barterButton.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource }); barterButton.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource }); barterButton.Sell.onPress = function() { g_BarterSell = resourceCode; updateSelectionDetails(); updateBarterButtons(); }; } /** * Code common to both the Barter Panel and the Trade/Barter Dialog, that * needs to be run on simulation update and when relevant hotkeys * (i.e. massbarter) are pressed. * * @param {string} resourceCode * @param {number} idx - Element index within its set * @param {string} prefix - Common prefix of the gui elements to be worked upon * @param {number} player */ function barterUpdateCommon(resourceCode, idx, prefix, player) { let barterButton = {}; let barterIcon = {}; let barterAmount = {}; for (let action of g_BarterActions) { barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]"); barterIcon[action] = Engine.GetGUIObjectByName(prefix + action + "Icon[" + idx + "]"); barterAmount[action] = Engine.GetGUIObjectByName(prefix + action + "Amount[" + idx + "]"); } let selectionIcon = Engine.GetGUIObjectByName(prefix + "SellSelection[" + idx + "]"); let amountToSell = g_BarterResourceSellQuantity; if (Engine.HotkeyIsPressed("session.massbarter")) amountToSell *= g_BarterMultiplier; let isSelected = resourceCode == g_BarterSell; let grayscale = isSelected ? "color:0 0 0 100:grayscale:" : ""; // Select color of the sell button let neededRes = {}; neededRes[resourceCode] = amountToSell; let canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", { "cost": neededRes, "player": player }) ? "color:255 0 0 80:" : ""; // Select color of the buy button neededRes = {}; neededRes[g_BarterSell] = amountToSell; let canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", { "cost": neededRes, "player": player }) ? "color:255 0 0 80:" : ""; barterIcon.Sell.sprite = canSellCurrent + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png"; barterIcon.Buy.sprite = canBuyAny + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png"; barterAmount.Sell.caption = "-" + amountToSell; let prices = GetSimState().players[player].barterPrices; barterAmount.Buy.caption = "+" + Math.round(prices.sell[g_BarterSell] / prices.buy[resourceCode] * amountToSell); barterButton.Buy.onPress = function() { Engine.PostNetworkCommand({ "type": "barter", "sell": g_BarterSell, "buy": resourceCode, "amount": amountToSell }); }; barterButton.Buy.hidden = isSelected; barterButton.Buy.enabled = controlsPlayer(player); barterButton.Sell.hidden = false; selectionIcon.hidden = !isSelected; } function updateBarterButtons() { let playerState = GetSimState().players[g_ViewedPlayer]; if (!playerState) return; let canBarter = playerState.canBarter; Engine.GetGUIObjectByName("barterNoMarketsMessage").hidden = canBarter; Engine.GetGUIObjectByName("barterResources").hidden = !canBarter; Engine.GetGUIObjectByName("barterHelp").hidden = !canBarter; if (!canBarter) return; let resCodes = g_ResourceData.GetCodes(); for (let i = 0; i < resCodes.length; ++i) barterUpdateCommon(resCodes[i], i, "barter", g_ViewedPlayer); } function getIdleLandTradersText(traderNumber) { let active = traderNumber.landTrader.trading; let garrisoned = traderNumber.landTrader.garrisoned; let inactive = traderNumber.landTrader.total - active - garrisoned; let messageTypes = { "active": { "garrisoned": { "no-inactive": translate("%(openingTradingString)s, and %(garrisonedString)s."), "inactive": translate("%(openingTradingString)s, %(garrisonedString)s, and %(inactiveString)s.") }, "no-garrisoned": { "no-inactive": translate("%(openingTradingString)s."), "inactive": translate("%(openingTradingString)s, and %(inactiveString)s.") } }, "no-active": { "garrisoned": { "no-inactive": translate("%(openingGarrisonedString)s."), "inactive": translate("%(openingGarrisonedString)s, and %(inactiveString)s.") }, "no-garrisoned": { "inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive), "no-inactive": translate("There are no land traders.") } } }; let message = messageTypes[active ? "active" : "no-active"][garrisoned ? "garrisoned" : "no-garrisoned"][inactive ? "inactive" : "no-inactive"]; let activeString = sprintf( translatePlural( "There is %(numberTrading)s land trader trading", "There are %(numberTrading)s land traders trading", active ), { "numberTrading": active } ); let inactiveString = sprintf( active || garrisoned ? translatePlural( "%(numberOfLandTraders)s inactive", "%(numberOfLandTraders)s inactive", inactive ) : translatePlural( "%(numberOfLandTraders)s land trader inactive", "%(numberOfLandTraders)s land traders inactive", inactive ), { "numberOfLandTraders": inactive } ); let garrisonedString = sprintf( active || inactive ? translatePlural( "%(numberGarrisoned)s garrisoned on a trading merchant ship", "%(numberGarrisoned)s garrisoned on a trading merchant ship", garrisoned ) : translatePlural( "There is %(numberGarrisoned)s land trader garrisoned on a trading merchant ship", "There are %(numberGarrisoned)s land traders garrisoned on a trading merchant ship", garrisoned ), { "numberGarrisoned": garrisoned } ); return sprintf(message, { "openingTradingString": activeString, "openingGarrisonedString": garrisonedString, "garrisonedString": garrisonedString, "inactiveString": coloredText(inactiveString, g_IdleTraderTextColor) }); } function getIdleShipTradersText(traderNumber) { let active = traderNumber.shipTrader.trading; let inactive = traderNumber.shipTrader.total - active; let messageTypes = { "active": { "inactive": translate("%(openingTradingString)s, and %(inactiveString)s."), "no-inactive": translate("%(openingTradingString)s.") }, "no-active": { "inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive), "no-inactive": translate("There are no merchant ships.") } }; let message = messageTypes[active ? "active" : "no-active"][inactive ? "inactive" : "no-inactive"]; let activeString = sprintf( translatePlural( "There is %(numberTrading)s merchant ship trading", "There are %(numberTrading)s merchant ships trading", active ), { "numberTrading": active } ); let inactiveString = sprintf( active ? translatePlural( "%(numberOfShipTraders)s inactive", "%(numberOfShipTraders)s inactive", inactive ) : translatePlural( "%(numberOfShipTraders)s merchant ship inactive", "%(numberOfShipTraders)s merchant ships inactive", inactive ), { "numberOfShipTraders": inactive } ); return sprintf(message, { "openingTradingString": activeString, "inactiveString": coloredText(inactiveString, g_IdleTraderTextColor) }); } function closeTrade() { g_IsTradeOpen = false; Engine.GetGUIObjectByName("tradeDialogPanel").hidden = true; } function toggleTrade() { let open = g_IsTradeOpen; closeOpenDialogs(); if (!open) openTrade(); } function updateGameSpeedControl() { let player = g_Players[Engine.GetPlayerID()]; g_GameSpeeds = getGameSpeedChoices(!player || player.state != "active"); let gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.list = g_GameSpeeds.Title; gameSpeed.list_data = g_GameSpeeds.Speed; let simRate = Engine.GetSimRate(); let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(+simRate.toFixed(2)); if (gameSpeedIdx == -1) warn("Unknown gamespeed:" + simRate); gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default; gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); }; } function toggleGameSpeed() { let gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.hidden = !gameSpeed.hidden; } function toggleObjectives() { let open = g_IsObjectivesOpen; closeOpenDialogs(); if (!open) openObjectives(); } function openObjectives() { g_IsObjectivesOpen = true; let player = g_Players[Engine.GetPlayerID()]; let playerState = player && player.state; let isActive = !playerState || playerState == "active"; Engine.GetGUIObjectByName("gameDescriptionText").caption = getGameDescription(true); let objectivesPlayerstate = Engine.GetGUIObjectByName("objectivesPlayerstate"); objectivesPlayerstate.hidden = isActive; objectivesPlayerstate.caption = g_PlayerStateMessages[playerState] || ""; let gameDescription = Engine.GetGUIObjectByName("gameDescription"); let gameDescriptionSize = gameDescription.size; gameDescriptionSize.top = Engine.GetGUIObjectByName( isActive ? "objectivesTitle" : "objectivesPlayerstate").size.bottom; gameDescription.size = gameDescriptionSize; Engine.GetGUIObjectByName("objectivesPanel").hidden = false; } function closeObjectives() { g_IsObjectivesOpen = false; Engine.GetGUIObjectByName("objectivesPanel").hidden = true; } /** * Allows players to see their own summary. * If they have shared ally vision researched, they are able to see the summary of there allies too. */ function openGameSummary() { closeOpenDialogs(); pauseGame(); let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); Engine.PushGuiPage("page_summary.xml", { "sim": { "mapSettings": g_GameAttributes.settings, "playerStates": extendedSimState.players.filter((state, player) => g_IsObserver || player == 0 || player == g_ViewedPlayer || extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]), "timeElapsed": extendedSimState.timeElapsed }, "gui": { "isInGame": true }, "selectedData": g_SummarySelectedData, "callback": "resumeGameAndSaveSummarySelectedData" }); } function openStrucTree() { closeOpenDialogs(); pauseGame(); // TODO add info about researched techs and unlocked entities Engine.PushGuiPage("page_structree.xml", { "civ": g_Players[g_ViewedPlayer].civ, "callback": "resumeGame", }); } /** * Pause or resume the game. * * @param explicit - true if the player explicitly wants to pause or resume. * If this argument isn't set, a multiplayer game won't be paused and the pause overlay * won't be shown in single player. */ function pauseGame(pause = true, explicit = false) { // The NetServer only supports pausing after all clients finished loading the game. if (g_IsNetworked && (!explicit || !g_IsNetworkedActive)) return; if (explicit) g_Paused = pause; Engine.SetPaused(g_Paused || pause, !!explicit); if (g_IsNetworked) { setClientPauseState(Engine.GetPlayerGUID(), g_Paused); return; } updatePauseOverlay(); } function resumeGame(explicit = false) { pauseGame(false, explicit); } function resumeGameAndSaveSummarySelectedData(data) { g_SummarySelectedData = data.summarySelectedData; resumeGame(data.explicitResume); } /** * Called when the current player toggles a pause button. */ function togglePause() { if (!Engine.GetGUIObjectByName("pauseButton").enabled) return; closeOpenDialogs(); pauseGame(!g_Paused, true); } /** * Called when a client pauses or resumes in a multiplayer game. */ function setClientPauseState(guid, paused) { // Update the list of pausing clients. let index = g_PausingClients.indexOf(guid); if (paused && index == -1) g_PausingClients.push(guid); else if (!paused && index != -1) g_PausingClients.splice(index, 1); updatePauseOverlay(); Engine.SetPaused(!!g_PausingClients.length, false); } /** * Update the pause overlay. */ function updatePauseOverlay() { Engine.GetGUIObjectByName("pauseButton").caption = g_Paused ? translate("Resume") : translate("Pause"); Engine.GetGUIObjectByName("resumeMessage").hidden = !g_Paused; Engine.GetGUIObjectByName("pausedByText").hidden = !g_IsNetworked; Engine.GetGUIObjectByName("pausedByText").caption = sprintf(translate("Paused by %(players)s"), { "players": g_PausingClients.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of players", ", ")) }); Engine.GetGUIObjectByName("pauseOverlay").hidden = !(g_Paused || g_PausingClients.length); Engine.GetGUIObjectByName("pauseOverlay").onPress = g_Paused ? togglePause : function() {}; } function openManual() { closeOpenDialogs(); pauseGame(); Engine.PushGuiPage("page_manual.xml", { "page": "manual/intro", "title": translate("Manual"), "url": "http://trac.wildfiregames.com/wiki/0adManual", "callback": "resumeGame" }); } function toggleDeveloperOverlay() { if (!g_GameAttributes.settings.CheatsEnabled && !g_IsReplay) return; let devCommands = Engine.GetGUIObjectByName("devCommands"); devCommands.hidden = !devCommands.hidden; let message = devCommands.hidden ? markForTranslation("The Developer Overlay was closed.") : markForTranslation("The Developer Overlay was opened."); // Only players can send the simulation chat command if (Engine.GetPlayerID() == -1) submitChatDirectly(message); else Engine.PostNetworkCommand({ "type": "aichat", "message": message, "translateMessage": true, "translateParameters": [], "parameters": {} }); } function closeOpenDialogs() { closeMenu(); closeChat(); closeDiplomacy(); closeTrade(); closeObjectives(); } function formatTributeTooltip(playerID, resourceCode, amount) { return sprintf(translate("Tribute %(resourceAmount)s %(resourceType)s to %(playerName)s. Shift-click to tribute %(greaterAmount)s."), { "resourceAmount": amount, "resourceType": resourceNameWithinSentence(resourceCode), "playerName": colorizePlayernameByID(playerID), "greaterAmount": amount < 500 ? 500 : amount + 500 }); } Index: ps/trunk/binaries/data/mods/public/gui/summary/summary.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 20704) +++ ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 20705) @@ -1,543 +1,545 @@ const g_CivData = loadCivData(false, false); var g_ScorePanelsData; var g_MaxHeadingTitle = 9; var g_LongHeadingWidth = 250; var g_PlayerBoxYSize = 40; var g_PlayerBoxGap = 2; var g_PlayerBoxAlpha = 50; var g_TeamsBoxYStart = 40; var g_TypeColors = { "blue": "196 198 255", "green": "201 255 200", "red": "255 213 213", "yellow": "255 255 157" }; /** * Colors, captions and format used for units, buildings, etc. types */ var g_SummaryTypes = { "percent": { "color": "", "caption": "%", "postfix": "%" }, "trained": { "color": g_TypeColors.green, "caption": translate("Trained"), "postfix": " / " }, "constructed": { "color": g_TypeColors.green, "caption": translate("Constructed"), "postfix": " / " }, "gathered": { "color": g_TypeColors.green, "caption": translate("Gathered"), "postfix": " / " }, "sent": { "color": g_TypeColors.green, "caption": translate("Sent"), "postfix": " / " }, "bought": { "color": g_TypeColors.green, "caption": translate("Bought"), "postfix": " / " }, "income": { "color": g_TypeColors.green, "caption": translate("Income"), "postfix": " / " }, "captured": { "color": g_TypeColors.yellow, "caption": translate("Captured"), "postfix": " / " }, "succeeded": { "color": g_TypeColors.green, "caption": translate("Succeeded"), "postfix": " / " }, "destroyed": { "color": g_TypeColors.blue, "caption": translate("Destroyed"), "postfix": "\n" }, "killed": { "color": g_TypeColors.blue, "caption": translate("Killed"), "postfix": "\n" }, "lost": { "color": g_TypeColors.red, "caption": translate("Lost"), "postfix": "\n" }, "used": { "color": g_TypeColors.red, "caption": translate("Used"), "postfix": "\n" }, "received": { "color": g_TypeColors.red, "caption": translate("Received"), "postfix": "\n" }, "sold": { "color": g_TypeColors.red, "caption": translate("Sold"), "postfix": "\n" }, "outcome": { "color": g_TypeColors.red, "caption": translate("Outcome"), "postfix": "\n" }, "failed": { "color": g_TypeColors.red, "caption": translate("Failed"), "postfix": "\n" } }; // Translation: Unicode encoded infinity symbol indicating a division by zero in the summary screen. var g_InfinitySymbol = translate("\u221E"); var g_Teams = []; // TODO set g_PlayerCount as playerCounters.length var g_PlayerCount = 0; var g_GameData; var g_ResourceData = new Resources(); // Selected chart indexes var g_SelectedChart = { "category": [0, 0], "value": [0, 1], "type": [0, 0] }; /* * Array of the panel button names. */ var g_PanelButtons = []; /* * Remember the name of the currently opened view panel. */ var g_SelectedPanel = ""; function init(data) { // Fill globals g_GameData = data; g_ScorePanelsData = getScorePanelsData(); g_PanelButtons = Object.keys(g_ScorePanelsData).concat(["charts"]).map(panel => panel + "PanelButton"); g_SelectedPanel = g_PanelButtons[0]; if (data && data.selectedData) { g_SelectedPanel = data.selectedData.panel; g_SelectedChart = data.selectedData.charts; } initTeamData(); calculateTeamCounterDataHelper(); // Output globals initPlayerBoxPositions(); initGUICharts(); initGUILabelsAndButtons(); selectPanel(Engine.GetGUIObjectByName(g_SelectedPanel)); for (let button of g_PanelButtons) { let tab = Engine.GetGUIObjectByName(button); tab.onMouseWheelUp = () => selectNextTab(1); tab.onMouseWheelDown = () => selectNextTab(-1); } } /* * Show next/previous panel. * @param direction - 1/-1 forward, backward panel. */ function selectNextTab(direction) { selectPanel(Engine.GetGUIObjectByName(g_PanelButtons[ (g_PanelButtons.indexOf(g_SelectedPanel) + direction + g_PanelButtons.length) % g_PanelButtons.length])); } function selectPanel(panel) { // TODO: move panel buttons to a custom parent object for (let button of Engine.GetGUIObjectByName("summaryWindow").children) if (button.name.endsWith("PanelButton")) button.sprite = "ModernTabHorizontalBackground"; panel.sprite = "ModernTabHorizontalForeground"; adjustTabDividers(panel.size); let generalPanel = Engine.GetGUIObjectByName("generalPanel"); let chartsPanel = Engine.GetGUIObjectByName("chartsPanel"); let chartsHidden = panel.name != "chartsPanelButton"; generalPanel.hidden = !chartsHidden; chartsPanel.hidden = chartsHidden; if (chartsHidden) updatePanelData(g_ScorePanelsData[panel.name.substr(0, panel.name.length - "PanelButton".length)]); else [0, 1].forEach(updateCategoryDropdown); g_SelectedPanel = panel.name; } function initGUICharts() { let player_colors = []; for (let i = 1; i <= g_PlayerCount; ++i) { let playerState = g_GameData.sim.playerStates[i]; player_colors.push( Math.floor(playerState.color.r * 255) + " " + Math.floor(playerState.color.g * 255) + " " + Math.floor(playerState.color.b * 255) ); } for (let i = 0; i < 2; ++i) Engine.GetGUIObjectByName("chart[" + i + "]").series_color = player_colors; let chartLegend = Engine.GetGUIObjectByName("chartLegend"); chartLegend.caption = g_GameData.sim.playerStates.slice(1).map( (state, index) => coloredText("■", player_colors[index]) + state.name ).join(" "); let chart1Part = Engine.GetGUIObjectByName("chart[1]Part"); let chart1PartSize = chart1Part.size; chart1PartSize.rright += 50; chart1PartSize.rleft += 50; chart1PartSize.right -= 5; chart1PartSize.left -= 5; chart1Part.size = chart1PartSize; } function resizeDropdown(dropdown) { let size = dropdown.size; size.bottom = dropdown.size.top + (Engine.GetTextWidth(dropdown.font, dropdown.list[dropdown.selected]) > dropdown.size.right - dropdown.size.left - 32 ? 42 : 27); dropdown.size = size; } function updateCategoryDropdown(number) { let chartCategory = Engine.GetGUIObjectByName("chart[" + number + "]CategorySelection"); chartCategory.list_data = Object.keys(g_ScorePanelsData); chartCategory.list = Object.keys(g_ScorePanelsData).map(panel => g_ScorePanelsData[panel].caption); chartCategory.onSelectionChange = function() { if (!this.list_data[this.selected]) return; if (g_SelectedChart.category[number] != this.selected) { g_SelectedChart.category[number] = this.selected; g_SelectedChart.value[number] = 0; g_SelectedChart.type[number] = 0; } resizeDropdown(this); updateValueDropdown(number, this.list_data[this.selected]); }; chartCategory.selected = g_SelectedChart.category[number]; } function updateValueDropdown(number, category) { let chartValue = Engine.GetGUIObjectByName("chart[" + number + "]ValueSelection"); let list = g_ScorePanelsData[category].headings.map(heading => heading.caption); list.shift(); chartValue.list = list; let list_data = g_ScorePanelsData[category].headings.map(heading => heading.identifier); list_data.shift(); chartValue.list_data = list_data; chartValue.onSelectionChange = function() { if (!this.list_data[this.selected]) return; if (g_SelectedChart.value[number] != this.selected) { g_SelectedChart.value[number] = this.selected; g_SelectedChart.type[number] = 0; } resizeDropdown(this); updateTypeDropdown(number, category, this.list_data[this.selected], this.selected); }; chartValue.selected = g_SelectedChart.value[number]; } function updateTypeDropdown(number, category, item, itemNumber) { let testValue = g_ScorePanelsData[category].counters[itemNumber].fn(g_GameData.sim.playerStates[1], 0, item); let hide = !g_ScorePanelsData[category].counters[itemNumber].fn || typeof testValue != "object" || Object.keys(testValue).length < 2; Engine.GetGUIObjectByName("chart[" + number + "]TypeLabel").hidden = hide; let chartType = Engine.GetGUIObjectByName("chart[" + number + "]TypeSelection"); chartType.hidden = hide; if (hide) { updateChart(number, category, item, itemNumber, Object.keys(testValue)[0] || undefined); return; } chartType.list = Object.keys(testValue).map(type => g_SummaryTypes[type].caption); chartType.list_data = Object.keys(testValue); chartType.onSelectionChange = function() { if (!this.list_data[this.selected]) return; g_SelectedChart.type[number] = this.selected; resizeDropdown(this); updateChart(number, category, item, itemNumber, this.list_data[this.selected]); }; chartType.selected = g_SelectedChart.type[number]; } function updateChart(number, category, item, itemNumber, type) { if (!g_ScorePanelsData[category].counters[itemNumber].fn) return; let chart = Engine.GetGUIObjectByName("chart[" + number + "]"); let series = []; for (let j = 1; j <= g_PlayerCount; ++j) { let playerState = g_GameData.sim.playerStates[j]; let data = []; for (let index in playerState.sequences.time) { let value = g_ScorePanelsData[category].counters[itemNumber].fn(playerState, index, item); if (type) value = value[type]; data.push([playerState.sequences.time[index], value]); } series.push(data); } chart.series = series; } function adjustTabDividers(tabSize) { let leftSpacer = Engine.GetGUIObjectByName("tabDividerLeft"); let rightSpacer = Engine.GetGUIObjectByName("tabDividerRight"); leftSpacer.size = [ 20, leftSpacer.size.top, tabSize.left + 2, leftSpacer.size.bottom ].join(" "); rightSpacer.size = [ tabSize.right - 2, rightSpacer.size.top, "100%-20", rightSpacer.size.bottom ].join(" "); } function updatePanelData(panelInfo) { resetGeneralPanel(); updateGeneralPanelHeadings(panelInfo.headings); updateGeneralPanelTitles(panelInfo.titleHeadings); let rowPlayerObjectWidth = updateGeneralPanelCounter(panelInfo.counters); updateGeneralPanelTeams(); let index = g_GameData.sim.playerStates[1].sequences.time.length - 1; let playerBoxesCounts = []; for (let i = 0; i < g_PlayerCount; ++i) { let playerState = g_GameData.sim.playerStates[i + 1]; if (!playerBoxesCounts[playerState.team + 1]) playerBoxesCounts[playerState.team + 1] = 1; else playerBoxesCounts[playerState.team + 1] += 1; let positionObject = playerBoxesCounts[playerState.team + 1] - 1; let rowPlayer = "playerBox[" + positionObject + "]"; let playerOutcome = "playerOutcome[" + positionObject + "]"; let playerNameColumn = "playerName[" + positionObject + "]"; let playerCivicBoxColumn = "civIcon[" + positionObject + "]"; let playerCounterValue = "valueData[" + positionObject + "]"; if (playerState.team != -1) { rowPlayer = "playerBoxt[" + playerState.team + "][" + positionObject + "]"; playerOutcome = "playerOutcomet[" + playerState.team + "][" + positionObject + "]"; playerNameColumn = "playerNamet[" + playerState.team + "][" + positionObject + "]"; playerCivicBoxColumn = "civIcont[" + playerState.team + "][" + positionObject + "]"; playerCounterValue = "valueDataTeam[" + playerState.team + "][" + positionObject + "]"; } let colorString = "color: " + Math.floor(playerState.color.r * 255) + " " + Math.floor(playerState.color.g * 255) + " " + Math.floor(playerState.color.b * 255); let rowPlayerObject = Engine.GetGUIObjectByName(rowPlayer); rowPlayerObject.hidden = false; rowPlayerObject.sprite = colorString + " " + g_PlayerBoxAlpha; let boxSize = rowPlayerObject.size; boxSize.right = rowPlayerObjectWidth; rowPlayerObject.size = boxSize; setOutcomeIcon(playerState.state, playerOutcome); - Engine.GetGUIObjectByName(playerNameColumn).caption = g_GameData.sim.playerStates[i + 1].name; + playerNameColumn = Engine.GetGUIObjectByName(playerNameColumn); + playerNameColumn.caption = g_GameData.sim.playerStates[i + 1].name; + playerNameColumn.tooltip = translateAISettings(g_GameData.sim.mapSettings.PlayerData[i + 1]); let civIcon = Engine.GetGUIObjectByName(playerCivicBoxColumn); civIcon.sprite = "stretched:" + g_CivData[playerState.civ].Emblem; civIcon.tooltip = g_CivData[playerState.civ].Name; updateCountersPlayer(playerState, panelInfo.counters, panelInfo.headings, playerCounterValue, index); } let teamCounterFn = panelInfo.teamCounterFn; if (g_Teams && teamCounterFn) updateCountersTeam(teamCounterFn, panelInfo.counters, panelInfo.headings, index); } function confirmStartReplay() { if (Engine.HasXmppClient()) messageBox( 400, 200, translate("Are you sure you want to quit the lobby?"), translate("Confirmation"), [translate("No"), translate("Yes")], [null, startReplay] ); else startReplay(); } function continueButton() { let summarySelectedData = { "panel": g_SelectedPanel, "charts": g_SelectedChart }; if (g_GameData.gui.isInGame) Engine.PopGuiPageCB({ "explicitResume": 0, "summarySelectedData": summarySelectedData }); else if (g_GameData.gui.isReplay) Engine.SwitchGuiPage("page_replaymenu.xml", { "replaySelectionData": g_GameData.gui.replaySelectionData, "summarySelectedData": summarySelectedData }); else if (Engine.HasXmppClient()) Engine.SwitchGuiPage("page_lobby.xml"); else Engine.SwitchGuiPage("page_pregame.xml"); } function startReplay() { if (Engine.HasXmppClient()) Engine.StopXmppClient(); if (!Engine.StartVisualReplay(g_GameData.gui.replayDirectory)) { warn("Replay file not found!"); return; } Engine.SwitchGuiPage("page_loading.xml", { "attribs": Engine.GetReplayAttributes(g_GameData.gui.replayDirectory), "isNetworked": false, "playerAssignments": { "local": { "name": singleplayerName(), "player": -1 } }, "savedGUIData": "", "isReplay": true, "replaySelectionData": g_GameData.gui.replaySelectionData }); } function initGUILabelsAndButtons() { let assignedState = g_GameData.sim.playerStates[g_GameData.gui.assignedPlayer || -1]; Engine.GetGUIObjectByName("summaryText").caption = g_GameData.gui.isInGame ? translate("Current Scores") : g_GameData.gui.isReplay ? translate("Scores at the end of the game.") : g_GameData.gui.disconnected ? translate("You have been disconnected.") : !assignedState ? translate("You have left the game.") : assignedState.state == "won" ? translate("You have won the battle!") : assignedState.state == "defeated" ? translate("You have been defeated...") : translate("You have abandoned the game."); Engine.GetGUIObjectByName("timeElapsed").caption = sprintf( translate("Game time elapsed: %(time)s"), { "time": timeToString(g_GameData.sim.timeElapsed) }); let mapType = g_Settings.MapTypes.find(type => type.Name == g_GameData.sim.mapSettings.mapType); let mapSize = g_Settings.MapSizes.find(size => size.Tiles == g_GameData.sim.mapSettings.Size || 0); Engine.GetGUIObjectByName("mapName").caption = sprintf( translate("%(mapName)s - %(mapType)s"), { "mapName": translate(g_GameData.sim.mapSettings.Name), "mapType": mapSize ? mapSize.Name : (mapType ? mapType.Title : "") }); Engine.GetGUIObjectByName("replayButton").hidden = g_GameData.gui.isInGame || !g_GameData.gui.replayDirectory; } function initTeamData() { // Panels g_PlayerCount = g_GameData.sim.playerStates.length - 1; if (g_GameData.sim.mapSettings.LockTeams) { // Count teams for (let player = 1; player <= g_PlayerCount; ++player) { let playerTeam = g_GameData.sim.playerStates[player].team; if (!g_Teams[playerTeam]) g_Teams[playerTeam] = []; g_Teams[playerTeam].push(player); } if (g_Teams.every(team => team && team.length < 2)) g_Teams = false; // Each player has his own team. Displaying teams makes no sense. } else g_Teams = false; // Erase teams data if teams are not displayed if (!g_Teams) for (let p = 0; p < g_PlayerCount; ++p) g_GameData.sim.playerStates[p+1].team = -1; }