Index: ps/trunk/binaries/data/mods/public/gui/common/color.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/color.js (revision 22654) +++ ps/trunk/binaries/data/mods/public/gui/common/color.js (revision 22655) @@ -1,191 +1,205 @@ /** * Used to highlight hotkeys in tooltip descriptions. */ var g_HotkeyTags = {"color": "255 251 131" }; /** * Concatenate integer color values to a string (for use in GUI objects) * * @param {Object} color * @param {number} alpha * @returns {string} */ function rgbToGuiColor(color, alpha) { let ret = "0 0 0"; if (color && ("r" in color) && ("g" in color) && ("b" in color)) ret = color.r + " " + color.g + " " + color.b; if (alpha !== undefined) ret += " " + alpha; return ret; } function guiToRgbColor(string) { let color = string.split(" "); if (color.length != 3 && color.length != 4 || color.some(num => !Number.isInteger(+num) || num < 0 || num > 255)) return undefined; return { "r": +color[0], "g": +color[1], "b": +color[2], "a": color.length == 4 ? +color[3] : undefined }; } /** * True if the colors are identical. * * @param {Object} color1 * @param {Object} color2 * @returns {boolean} */ function sameColor(color1, color2) { return color1.r === color2.r && color1.g === color2.g && color1.b === color2.b; } /** * Computes the euclidian distance between the two colors. * The smaller the return value, the close the colors. Zero if identical. * * @param {Object} color1 * @param {Object} color2 * @returns {number} */ function colorDistance(color1, color2) { return Math.euclidDistance3D(color1.r, color1.g, color1.b, color2.r, color2.g, color2.b); } /** * Ensure `value` is between 0 and 1. * * @param {number} value * @returns {number} */ function clampColorValue(value) { return Math.abs(1 - Math.abs(value - 1)); } /** * Convert color value from RGB to HSL space. * * @see {@link https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion} * @param {number} r - red * @param {number} g - green * @param {number} b - blue * @returns {Array} */ function rgbToHsl(r, g, b) { r /= 255; g /= 255; b /= 255; let max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max == min) - h = s = 0; // achromatic + { + // achromatic + h = 0; + s = 0; + } else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; + default: + error("rgbToHsl could not determine maximum!"); + break; } + h /= 6; } return [h, s, l]; } /** * Convert color value from HSL to RGB space. * * @see {@link https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion} * @param {number} h - hueness * @param {number} s - saturation * @param {number} l - lightness * @returns {Array} */ function hslToRgb(h, s, l) { function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } [h, s, l] = [h, s, l].map(clampColorValue); let r, g, b; if (s == 0) - r = g = b = l; // achromatic - else { + { + // achromatic + b = l; + r = l; + g = l; + } + else + { let q = l < 0.5 ? l * (1 + s) : l + s - l * s; let p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [r, g, b].map(n => Math.round(n * 255)); } function colorizeHotkey(text, hotkey) { let key = Engine.ConfigDB_GetValue("user", "hotkey." + hotkey); if (!key || key.toLowerCase() == "unused") key = sprintf(translate("Unassigned hotkey: %(hotkeyName)s"), { "hotkeyName": hotkey }); return sprintf(text, { "hotkey": setStringTags("\\[" + key + "]", g_HotkeyTags) }); } /** * The autocomplete hotkey is hardcoded in SDLK_TAB of CInput.cpp, * as we don't want hotkeys interfering with typing text. */ function colorizeAutocompleteHotkey(string) { return sprintf(string || translate("Press %(hotkey)s to autocomplete player names."), { "hotkey": setStringTags("\\[" + translateWithContext("hotkey", "Tab") + "]", g_HotkeyTags) }); } /** * Adds grey font if savegame/replay is not compatible. */ function compatibilityColor(text, isCompatible) { return isCompatible ? text : coloredText(text, "96 96 96"); } Index: ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js (revision 22654) +++ ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js (revision 22655) @@ -1,212 +1,216 @@ /** * Used for acoustic GUI notifications. * Define the soundfile paths and specific time thresholds (avoid spam). * And store the timestamp of last interaction for each notification. */ var g_SoundNotifications = { "nick": { "soundfile": "audio/interface/ui/chat_alert.ogg", "threshold": 3000 }, "gamesetup.join": { "soundfile": "audio/interface/ui/gamesetup_join.ogg", "threshold": 0 } }; /** * Returns translated history and gameplay data of all civs, optionally including a mock gaia civ. */ function loadCivData(selectableOnly, gaia) { let civData = loadCivFiles(selectableOnly); translateObjectKeys(civData, ["Name", "Description", "History", "Special"]); if (gaia) civData.gaia = { "Code": "gaia", "Name": translate("Gaia") }; return deepfreeze(civData); } // A sorting function for arrays of objects with 'name' properties, ignoring case function sortNameIgnoreCase(x, y) { let lowerX = x.name.toLowerCase(); let lowerY = y.name.toLowerCase(); if (lowerX < lowerY) return -1; if (lowerX > lowerY) return 1; return 0; } /** * Escape tag start and escape characters, so users cannot use special formatting. */ function escapeText(text) { return text.replace(/\\/g, "\\\\").replace(/\[/g, "\\["); } function unescapeText(text) { return text.replace(/\\\\/g, "\\").replace(/\\\[/g, "\["); } /** * Merge players by team to remove duplicate Team entries, thus reducing the packet size of the lobby report. */ function playerDataToStringifiedTeamList(playerData) { let teamList = {}; for (let pData of playerData) { let team = pData.Team === undefined ? -1 : pData.Team; if (!teamList[team]) teamList[team] = []; teamList[team].push(pData); delete teamList[team].Team; } return escapeText(JSON.stringify(teamList)); } function stringifiedTeamListToPlayerData(stringifiedTeamList) { let teamList = {}; try { teamList = JSON.parse(unescapeText(stringifiedTeamList)); } - catch (e) {} + catch (e) + { + // Ignore invalid input from remote users + return []; + } let playerData = []; for (let team in teamList) for (let pData of teamList[team]) { pData.Team = team; playerData.push(pData); } return playerData; } function translateMapTitle(mapTitle) { return mapTitle == "random" ? translateWithContext("map selection", "Random") : translate(mapTitle); } function removeDupes(array) { // loop backwards to make splice operations cheaper let i = array.length; while (i--) if (array.indexOf(array[i]) != i) array.splice(i, 1); } function singleplayerName() { return Engine.ConfigDB_GetValue("user", "playername.singleplayer") || Engine.GetSystemUsername(); } function multiplayerName() { return Engine.ConfigDB_GetValue("user", "playername.multiplayer") || Engine.GetSystemUsername(); } function tryAutoComplete(text, autoCompleteList) { if (!text.length) return text; var wordSplit = text.split(/\s/g); if (!wordSplit.length) return text; var lastWord = wordSplit.pop(); if (!lastWord.length) return text; for (var word of autoCompleteList) { if (word.toLowerCase().indexOf(lastWord.toLowerCase()) != 0) continue; text = wordSplit.join(" "); if (text.length > 0) text += " "; text += word; break; } return text; } function autoCompleteNick(guiObject, playernames) { let text = guiObject.caption; if (!text.length) return; let bufferPosition = guiObject.buffer_position; let textTillBufferPosition = text.substring(0, bufferPosition); let newText = tryAutoComplete(textTillBufferPosition, playernames); guiObject.caption = newText + text.substring(bufferPosition); guiObject.buffer_position = bufferPosition + (newText.length - textTillBufferPosition.length); } /** * Manage acoustic GUI notifications. * * @param {string} type - Notification type. */ function soundNotification(type) { if (Engine.ConfigDB_GetValue("user", "sound.notify." + type) != "true") return; let notificationType = g_SoundNotifications[type]; let timeNow = Date.now(); if (!notificationType.lastInteractionTime || timeNow > notificationType.lastInteractionTime + notificationType.threshold) Engine.PlayUISound(notificationType.soundfile, false); notificationType.lastInteractionTime = timeNow; } /** * Horizontally spaces objects within a parent * * @param margin The gap, in px, between the objects */ function horizontallySpaceObjects(parentName, margin = 0) { let objects = Engine.GetGUIObjectByName(parentName).children; for (let i = 0; i < objects.length; ++i) { let size = objects[i].size; let width = size.right - size.left; size.left = i * (width + margin) + margin; size.right = (i + 1) * (width + margin); objects[i].size = size; } } /** * Hide all children after a certain index */ function hideRemaining(parentName, start = 0) { let objects = Engine.GetGUIObjectByName(parentName).children; for (let i = start; i < objects.length; ++i) objects[i].hidden = true; } function getBuildString() { return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), { "buildDate": Engine.GetBuildDate(), "revision": Engine.GetBuildRevision() }); } Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 22654) +++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 22655) @@ -1,483 +1,483 @@ /** * 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 = '•'; var g_MapPreviewPath = "session/icons/mappreview/"; /** * Returns the biome specific mappreview image if it exists, or empty string otherwise. */ function getBiomePreview(mapName, biomeName) { let biomePreview = basename(mapName) + "_" + basename(biomeName) + ".png"; if (Engine.TextureExists("art/textures/ui/" + g_MapPreviewPath + biomePreview)) return biomePreview; return ""; } /** * Returns map description and preview image or placeholder. */ function getMapDescriptionAndPreview(mapType, mapName, gameAttributes = undefined) { 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"); let biomePreview = getBiomePreview(mapName, gameAttributes && gameAttributes.settings.Biome || ""); return deepfreeze({ "description": mapData && mapData.settings && mapData.settings.Description ? translate(mapData.settings.Description) : translate("Sorry, no description available."), "preview": biomePreview ? biomePreview : 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 + ":" + g_MapPreviewPath + 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, %(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, %(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 (%(AIdescription)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu 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"), "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() { let titles = []; if (!g_GameAttributes.settings.VictoryConditions.length) titles.push({ "label": translateWithContext("victory condition", "Endless Game"), "value": translate("No winner will be determined, even if everyone is defeated.") }); for (let victoryCondition of g_VictoryConditions) { if (g_GameAttributes.settings.VictoryConditions.indexOf(victoryCondition.Name) == -1) continue; let title = translateVictoryCondition(victoryCondition.Name); if (victoryCondition.Name == "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 = victoryCondition.Name == "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": victoryCondition.Description }); if (isCaptureTheRelic) titles.push({ "label": translate("Relic Count"), "value": g_GameAttributes.settings.RelicCount }); if (victoryCondition.Name == "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.") }); 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 }) }); if (g_GameAttributes.map == "random") titles.push({ "label": translateWithContext("Map Selection", "Random Map"), "value": translate("Randomly select a map from the list.") }); else { titles.push({ "label": translate("Map Name"), "value": translate(g_GameAttributes.settings.Name) }); titles.push({ "label": translate("Map Description"), "value": g_GameAttributes.settings.Description ? translate(g_GameAttributes.settings.Description) : translate("Sorry, no description available.") }); } titles.push({ "label": translate("Map Type"), "value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)] }); if (typeof g_MapFilterList !== "undefined") titles.push({ "label": translate("Map Filter"), "value": g_MapFilterList.name[g_MapFilterList.id.findIndex(id => id == g_GameAttributes.mapFilter)] }); 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 }); } if (g_GameAttributes.settings.Biome) { let biome = g_Settings.Biomes.find(b => b.Id == g_GameAttributes.settings.Biome); titles.push({ "label": biome ? biome.Title : translateWithContext("biome", "Random Biome"), "value": biome ? biome.Description : translate("Randomly select a biome from the list.") }); } if (g_GameAttributes.settings.TriggerDifficulty !== undefined) { - let difficulty = g_Settings.TriggerDifficulties.find(difficulty => difficulty.Difficulty == g_GameAttributes.settings.TriggerDifficulty); + let triggerDifficulty = g_Settings.TriggerDifficulties.find(difficulty => difficulty.Difficulty == g_GameAttributes.settings.TriggerDifficulty); titles.push({ - "label": difficulty.Title, - "value": difficulty.Tooltip + "label": triggerDifficulty.Title, + "value": triggerDifficulty.Tooltip }); } titles.push({ "label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"), "value": g_GameAttributes.settings.Nomad ? translate("Players start with only few units and have to find a suitable place to build their city.") : translate("Players start with a Civic Center.") }); 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/common/settings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 22654) +++ ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 22655) @@ -1,430 +1,430 @@ /** * The maximum number of players that the engine supports. * TODO: Maybe we can support more than 8 players sometime. */ const g_MaxPlayers = 8; /** * The maximum number of teams allowed. */ const g_MaxTeams = 4; /** * Directory containing all editable settings. */ const g_SettingsDirectory = "simulation/data/settings/"; /** * Directory containing all biomes supported for random map scripts. */ const g_BiomesDirectory = "maps/random/rmbiome/"; /** * An object containing all values given by setting name. * Used by lobby, gamesetup, session, summary screen and replay menu. */ const g_Settings = loadSettingsValues(); /** * Loads and translates all values of all settings which * can be configured by dropdowns in the gamesetup. * * @returns {Object|undefined} */ function loadSettingsValues() { var settings = { "AIDescriptions": loadAIDescriptions(), "AIDifficulties": loadAIDifficulties(), "AIBehaviors": loadAIBehaviors(), "Ceasefire": loadCeasefire(), "VictoryDurations": loadVictoryDuration(), "GameSpeeds": loadSettingValuesFile("game_speeds.json"), "MapTypes": loadMapTypes(), "MapSizes": loadSettingValuesFile("map_sizes.json"), "Biomes": loadBiomes(), "PlayerDefaults": loadPlayerDefaults(), "PopulationCapacities": loadPopulationCapacities(), "StartingResources": loadSettingValuesFile("starting_resources.json"), "VictoryConditions": loadVictoryConditions(), "TriggerDifficulties": loadSettingValuesFile("trigger_difficulties.json") }; if (Object.keys(settings).some(key => settings[key] === undefined)) return undefined; return deepfreeze(settings); } /** * Returns an array of objects reflecting all possible values for a given setting. * * @param {string} filename * @see simulation/data/settings/ * @returns {Array|undefined} */ function loadSettingValuesFile(filename) { var json = Engine.ReadJSONFile(g_SettingsDirectory + filename); if (!json || !json.Data) { error("Could not load " + filename + "!"); return undefined; } if (json.TranslatedKeys) { let keyContext = json.TranslatedKeys; if (json.TranslationContext) { keyContext = {}; for (let key of json.TranslatedKeys) keyContext[key] = json.TranslationContext; } translateObjectKeys(json.Data, keyContext); } return json.Data; } /** * Loads the descriptions as defined in simulation/ai/.../data.json and loaded by ICmpAIManager.cpp. * * @returns {Array} */ function loadAIDescriptions() { var ais = Engine.GetAIs(); translateObjectKeys(ais, ["name", "description"]); return ais.sort((a, b) => a.data.name.localeCompare(b.data.name)); } /** * Hardcoded, as modding is not supported without major changes. * Notice the AI code parses the difficulty level by the index, not by name. * * @returns {Array} */ function loadAIDifficulties() { return [ { "Name": "sandbox", "Title": translateWithContext("aiDiff", "Sandbox") }, { "Name": "very easy", "Title": translateWithContext("aiDiff", "Very Easy") }, { "Name": "easy", "Title": translateWithContext("aiDiff", "Easy") }, { "Name": "medium", "Title": translateWithContext("aiDiff", "Medium"), "Default": true }, { "Name": "hard", "Title": translateWithContext("aiDiff", "Hard") }, { "Name": "very hard", "Title": translateWithContext("aiDiff", "Very Hard") } ]; } function loadAIBehaviors() { return [ { "Name": "random", "Title": translateWithContext("aiBehavior", "Random"), "Default": true }, { "Name": "balanced", "Title": translateWithContext("aiBehavior", "Balanced"), }, { "Name": "defensive", "Title": translateWithContext("aiBehavior", "Defensive") }, { "Name": "aggressive", "Title": translateWithContext("aiBehavior", "Aggressive") } ]; } /** * Loads available victory times for victory conditions like Wonder and Capture the Relic. */ function loadVictoryDuration() { var jsonFile = "victory_times.json"; var json = Engine.ReadJSONFile(g_SettingsDirectory + jsonFile); if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times)) { error("Could not load " + jsonFile); return undefined; } return json.Times.map(duration => ({ "Duration": duration, "Default": duration == json.Default, "Title": sprintf(translatePluralWithContext("victory duration", "%(min)s minute", "%(min)s minutes", duration), { "min": duration }) })); } /** * Loads available ceasefire settings. * * @returns {Array|undefined} */ function loadCeasefire() { var json = Engine.ReadJSONFile(g_SettingsDirectory + "ceasefire.json"); if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times)) { error("Could not load ceasefire.json"); return undefined; } return json.Times.map(timeout => ({ "Duration": timeout, "Default": timeout == json.Default, "Title": timeout == 0 ? translateWithContext("ceasefire", "No ceasefire") : sprintf(translatePluralWithContext("ceasefire", "%(minutes)s minute", "%(minutes)s minutes", timeout), { "minutes": timeout }) })); } /** * Hardcoded, as modding is not supported without major changes. * * @returns {Array} */ function loadMapTypes() { return [ { "Name": "skirmish", "Title": translateWithContext("map", "Skirmish"), "Description": translate("A map with a predefined landscape and number of players. Freely select the other gamesettings."), "Default": true }, { "Name": "random", "Title": translateWithContext("map", "Random"), "Description": translate("Create a unique map with a different resource distribution each time. Freely select the number of players and teams.") }, { "Name": "scenario", "Title": translateWithContext("map", "Scenario"), "Description": translate("A map with a predefined landscape and matchsettings.") } ]; } function loadBiomes() { return listFiles(g_BiomesDirectory, ".json", true).filter(biomeID => biomeID != "defaultbiome").map(biomeID => { let description = Engine.ReadJSONFile(g_BiomesDirectory + biomeID + ".json").Description; return { "Id": biomeID, "Title": translateWithContext("biome definition", description.Title), "Description": description.Description ? translateWithContext("biome definition", description.Description) : "", "Preview": description.Preview || undefined }; }); } /** * Loads available victoryCondtions from json files. * * @returns {Array|undefined} */ function loadVictoryConditions() { let subdir = "victory_conditions/"; let victoryConditions = listFiles(g_SettingsDirectory + subdir, ".json", false).map(victoryScriptName => { let victoryCondition = loadSettingValuesFile(subdir + victoryScriptName + ".json"); if (victoryCondition) victoryCondition.Name = victoryScriptName; return victoryCondition; }); if (victoryConditions.some(victoryCondition => victoryCondition == undefined)) return undefined; return victoryConditions.sort((a, b) => a.GUIOrder - b.GUIOrder || (a.Title > b.Title ? 1 : a.Title > b.Title ? -1 : 0)); } /** * Loads the default player settings (like civs and colors). * * @returns {Array|undefined} */ function loadPlayerDefaults() { var json = Engine.ReadJSONFile(g_SettingsDirectory + "player_defaults.json"); if (!json || !json.PlayerData) { error("Could not load player_defaults.json"); return undefined; } return json.PlayerData; } /** * Loads available population capacities. * * @returns {Array|undefined} */ function loadPopulationCapacities() { var json = Engine.ReadJSONFile(g_SettingsDirectory + "population_capacities.json"); if (!json || json.Default === undefined || !json.PopulationCapacities || !Array.isArray(json.PopulationCapacities)) { error("Could not load population_capacities.json"); return undefined; } return json.PopulationCapacities.map(population => ({ "Population": population, "Default": population == json.Default, "Title": population < 10000 ? population : translate("Unlimited") })); } /** * Creates an object with all values of that property of the given setting and * finds the index of the default value. * * This allows easy copying of setting values to dropdown lists. * * @param {Array} settingValues * @returns {Object|undefined} */ function prepareForDropdown(settingValues) { if (!settingValues) return undefined; let settings = { "Default": 0 }; for (let index in settingValues) { for (let property in settingValues[index]) { if (property == "Default") continue; if (!settings[property]) settings[property] = []; // Switch property and index settings[property][index] = settingValues[index][property]; } // Copy default value if (settingValues[index].Default) settings.Default = +index; } return deepfreeze(settings); } function getGameSpeedChoices(allowFastForward) { return prepareForDropdown(g_Settings.GameSpeeds.filter(speed => !speed.FastForward || allowFastForward)); } /** * Returns title or placeholder. * * @param {string} aiName - for example "petra" */ function translateAIName(aiName) { let description = g_Settings.AIDescriptions.find(ai => ai.id == aiName); return description ? translate(description.data.name) : translateWithContext("AI name", "Unknown"); } /** * Returns title or placeholder. * * @param {Number} index - index of AIDifficulties */ function translateAIDifficulty(index) { let difficulty = g_Settings.AIDifficulties[index]; return difficulty ? difficulty.Title : translateWithContext("AI difficulty", "Unknown"); } /** * Returns title or placeholder. * * @param {string} aiBehavior - for example "defensive" */ function translateAIBehavior(aiBehavior) { let behavior = g_Settings.AIBehaviors.find(b => b.Name == aiBehavior); return behavior ? behavior.Title : translateWithContext("AI behavior", "Default"); } /** * Returns title or placeholder. * * @param {string} mapType - for example "skirmish" * @returns {string} */ function translateMapType(mapType) { let type = g_Settings.MapTypes.find(t => t.Name == mapType); return type ? type.Title : translateWithContext("map type", "Unknown"); } /** * Returns title or placeholder "Default". * * @param {Number} mapSize - tilecount * @returns {string} */ function translateMapSize(tiles) { let mapSize = g_Settings.MapSizes.find(size => size.Tiles == +tiles); return mapSize ? mapSize.Name : translateWithContext("map size", "Default"); } /** * Returns title or placeholder. * * @param {Number} population - for example 300 * @returns {string} */ function translatePopulationCapacity(population) { let popCap = g_Settings.PopulationCapacities.find(p => p.Population == population); return popCap ? popCap.Title : translateWithContext("population capacity", "Unknown"); } /** * Returns title or placeholder. * * @param {string} victoryConditionName - For example "conquest". * @returns {string} */ function translateVictoryCondition(victoryConditionName) { - let victoryCondition = g_Settings.VictoryConditions.find(victoryCondition => victoryCondition.Name == victoryConditionName); + let victoryCondition = g_Settings.VictoryConditions.find(condition => condition.Name == victoryConditionName); return victoryCondition ? victoryCondition.Title : translate("Unknown Victory Condition"); } Index: ps/trunk/binaries/data/mods/public/gui/common/tab_buttons.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/tab_buttons.js (revision 22654) +++ ps/trunk/binaries/data/mods/public/gui/common/tab_buttons.js (revision 22655) @@ -1,77 +1,79 @@ /** * Number of categories. */ var g_TabCategoryCount; /** * Index of the currently visible tab, set first tab as default. */ var g_TabCategorySelected = 0; /** * Function to be executed when selecting a tab. The new category index is passed. */ var g_OnSelectTab; /** * Create tab buttons. * * @param {Array} categoriesData - Arrays of objects containing for every tab a (translated) label and tooltip. * @param {number} buttonHeight - Vertical distance between the top and bottom of a button. * @param {number} spacing - Vertical distance between two buttons. * @param {function} onPress - Function to be executed when a button is pressed, it gets the new category index passed. * @param {function} onSelect - Function to be executed whenever the selection changes (so also for scrolling), it gets the new category index passed. */ function placeTabButtons(categoriesData, buttonHeight, spacing, onPress, onSelect) { g_OnSelectTab = onSelect; g_TabCategoryCount = categoriesData.length; for (let category in categoriesData) { let button = Engine.GetGUIObjectByName("tabButton[" + category + "]"); if (!button) { warn("Too few tab-buttons!"); break; } button.hidden = false; let size = button.size; size.top = category * (buttonHeight + spacing) + spacing / 2; size.bottom = size.top + buttonHeight; button.size = size; button.tooltip = categoriesData[category].tooltip || ""; - button.onPress = (category => function() { onPress(category); })(+category); + + let categoryNum = +category; + button.onPress = () => { onPress(categoryNum); }; Engine.GetGUIObjectByName("tabButtonText[" + category + "]").caption = categoriesData[category].label; } selectPanel(g_TabCategorySelected); } /** * Show next/previous panel. * @param direction - +1/-1 for forward/backward. */ function selectNextTab(direction) { if (g_TabCategoryCount) selectPanel(g_TabCategorySelected === undefined ? direction > 0 ? 0 : g_TabCategoryCount - 1 : (g_TabCategorySelected + direction + g_TabCategoryCount) % g_TabCategoryCount); } function selectPanel(category) { g_TabCategorySelected = category; Engine.GetGUIObjectByName("tabButtons").children.forEach((button, j) => { button.sprite = category == j ? "ModernTabVerticalForeground" : "ModernTabVerticalBackground"; }); if (g_OnSelectTab) g_OnSelectTab(category); }