Index: binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -1246,6 +1246,9 @@ */ function updateSettingsPanelPosition(offset) { + if (offset === 0) + return; + let settingsPanel = Engine.GetGUIObjectByName("settingsPanel"); let settingsPanelSize = settingsPanel.size; settingsPanelSize.left += offset; @@ -2407,6 +2410,40 @@ updateGUIObjects(); } +function MapBrowserOpen() +{ + Engine.PushGuiPage("page_mapbrowser.xml", + { + "callback" : "MapBrowserCallback", + "mapSelected" : true, + "map" : { + "type" : g_GameAttributes.mapType, + "filter" : g_GameAttributes.mapFilter, + "name" : g_GameAttributes.map // includes the map path + } + }); +} + +/** + * Called after closing the dialog. + */ +function MapBrowserCallback(data) +{ + if (!g_IsController || !data.mapSelected) + return; + + const typeId = g_Dropdowns.mapType.ids().indexOf(data.map.type); + g_Dropdowns.mapType.select(typeId); + + const filterId = g_Dropdowns.mapFilter.ids().indexOf(data.map.filter); + g_Dropdowns.mapFilter.select(filterId); + + const mapId = g_Dropdowns.mapSelection.ids().indexOf(data.map.path + data.map.name); + g_Dropdowns.mapSelection.select(mapId); + + updateGUIObjects(); +} + function openAIConfig(playerSlot) { g_LastViewedAIPlayer = playerSlot; Index: binaries/data/mods/public/gui/gamesetup/gamesetup.xml =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.xml +++ binaries/data/mods/public/gui/gamesetup/gamesetup.xml @@ -139,6 +139,9 @@ + + MapBrowserOpen(); + Index: binaries/data/mods/public/gui/mapbrowser/mapbrowser.js =================================================================== --- binaries/data/mods/public/gui/mapbrowser/mapbrowser.js +++ binaries/data/mods/public/gui/mapbrowser/mapbrowser.js @@ -0,0 +1,393 @@ +function GameMap(fileName, type) +{ + this.loaded = false; + this.type = type; + this.file = { + path: GameMap.types[ type ].path, + name: fileName, + extension: GameMap.types[ type ].extension + }; +} + +GameMap.types = { + "random": + { + "path": "maps/random/", + "extension": ".json" + }, + "scenario": + { + "path": "maps/scenarios/", + "extension": ".xml" + }, + "skirmish": + { + "path": "maps/skirmishes/", + "extension": ".xml" + } +}; + +GameMap.typesList = Object.keys(GameMap.types); + +GameMap.filters = { + "default": + { + "name": translateWithContext("map filter", "Default"), + "tooltip": translateWithContext("map filter", "All maps except naval and demo maps."), + "filter": mapFilters => mapFilters.every(filter => [ "naval", "demo", "hidden" ].indexOf(filter) == -1), + "Default": true + }, + "naval": + { + "name": translate("Naval Maps"), + "tooltip": translateWithContext("map filter", "Maps where ships are needed to reach the enemy."), + "filter": mapFilters => mapFilters.indexOf("naval") != -1 + }, + "demo": + { + "name": translate("Demo Maps"), + "tooltip": translateWithContext("map filter", "These maps are not playable but for demonstration purposes only."), + "filter": mapFilters => mapFilters.indexOf("demo") != -1 + }, + "new": + { + "name": translate("New Maps"), + "tooltip": translateWithContext("map filter", "Maps that are brand new in this release of the game."), + "filter": mapFilters => mapFilters.indexOf("new") != -1 + }, + "trigger": + { + "name": translate("Trigger Maps"), + "tooltip": translateWithContext("map filter", "Maps that come with scripted events and potentially spawn enemy units."), + "filter": mapFilters => mapFilters.indexOf("trigger") != -1 + }, + "all": + { + "name": translate("All Maps"), + "tooltip": translateWithContext("map filter", "Every map of the chosen maptype."), + "filter": mapFilters => true + } +}; + +GameMap.filtersList = Object.keys(GameMap.filters); + +//returns a list of GameMap objects of all maps of given type +GameMap.getMapsOfType = function (type) +{ + let maps = []; + + if (!GameMap.types[ type ]) + { return maps; } + + const filePath = GameMap.types[ type ].path; + const fileExtension = GameMap.types[ type ].extension; + + for (let fileName of listFiles(filePath, fileExtension, false)) + { + if (fileName.startsWith("_")) + { continue; } + maps.push(new GameMap(fileName, type)); + } + return maps; +}; + +GameMap.getMapsAll = function () +{ + let mapList = []; + for (let type of GameMap.typesList) + mapList.push(...GameMap.getMapsOfType(type)); + return mapList; +}; + +/* Returns a dictionary: each key is the filter with its corresponding map list + * mapList is a list of GameMap objects + */ +GameMap.getMapsByFilter = function (mapList) +{ + let mapsFiltered = {}; + GameMap.filtersList.forEach((filter) => mapsFiltered[ filter ] = []); + for (let map of mapList) + { + if (!map.loaded) + map.load(); + + for (let filter of GameMap.filtersList) + { + if (GameMap.filters[ filter ].filter(map.filter)) + mapsFiltered[ filter ].push(map); + } + } + return mapsFiltered; +}; + +// load data from settings +GameMap.prototype.load = function () +{ + this.loaded = true; + this.data = (this.type == "random") ? + Engine.ReadJSONFile(this.file.path + this.file.name + this.file.extension) : + Engine.LoadMapSettings(this.file.path + this.file.name); + + this.loadName(); + this.loadDescription(); + this.loadPreview(); + this.loadFilters(); + + return this.data; +}; + +GameMap.prototype.loadName = function () +{ + return this.name = (!this.data || !this.data.settings || !this.data.settings.Name) ? + translate("No map name.") : + translate(this.data.settings.Name); +}; + +GameMap.prototype.loadDescription = function () +{ + return this.description = (!this.data || !this.data.settings || !this.data.settings.Description) ? + translate("No map description.") : + translate(this.data.settings.Description); +}; + +GameMap.prototype.loadPreview = function () +{ + const path = "cropped:0.78125,0.5859375:session/icons/mappreview/"; + return this.preview = (!this.data || !this.data.settings || !this.data.settings.Preview) ? + path + "nopreview.png" : + path + this.data.settings.Preview; +}; + +GameMap.prototype.loadFilters = function () +{ + let filter = !!this.data.settings ? this.data.settings.Keywords || [] : []; + if (filter.indexOf("all") == -1) + filter.push("all") + return this.filter = filter; +} + + +let mapBrowser; // GridBrowser instance, manages the maps previews +let mapList; // Dictionary of maps by filter (of current map type list) +let mapFilter; // List of current selected filter from mapList +let mapFilterDropdown; + +let mapSelected = {}; +mapSelected.set = function (map, filter = undefined) +{ + this.map = map; + this.filter = (filter !== undefined) ? filter : mapFilterDropdown.list_data[ mapFilterDropdown.selected ]; + + if (!map.loaded) + map.load(); + + Engine.GetGUIObjectByName("nameSelected").caption = map.name; + Engine.GetGUIObjectByName("previewSelected").sprite = map.preview; + Engine.GetGUIObjectByName("descriptionSelected").caption = map.description; +} + +function childFunction(map, pageIndex, pageList, listIndex, list, childObject) +{ + if (!map.loaded) + map.load(); + + const id = (name, index = pageIndex) => `${name}[${index}]`; + + const image = Engine.GetGUIObjectByName(id("mapPreview")); + const button = Engine.GetGUIObjectByName(id("mapButton")); + const mapBox = Engine.GetGUIObjectByName(id("mapBox")); + const mapName = Engine.GetGUIObjectByName(id("mapName")); + + const animeButtonSettingsSelected = { + "color": "255 0 0 100", + "size": "4 4 -4 -4" + }; + const animeButtonSettingsUnselected = { + "color": "255 0 0 0", + "size": "8 8 -8 -8" + }; + + // Draw box outline if this box is the current selected map + if (mapSelected.map.file.name === map.file.name) + new AnimateGUI(button, animeButtonSettingsSelected); + else + new AnimateGUI(button, animeButtonSettingsUnselected); + + mapName.caption = map.name; + + image.sprite = map.preview; + + button.tooltip = map.description; + button.onMouseLeftPress = () => + { + mapSelected.set(map); + + // if previously was selected then unselect (meaning it has a sprite) + if (this.getPageIndexOfSelected() != -1) + { + let lastButtonSelected = Engine.GetGUIObjectByName(id("mapButton", this.getPageIndexOfSelected())); + new AnimateGUI(lastButtonSelected, animeButtonSettingsUnselected); + } + new AnimateGUI(button, animeButtonSettingsSelected); + + new AnimateGUI(image, { + "size": "3 3 -3 -3", + "duration": 100 + }); + + this.setIndexOfSelected(listIndex) + }; + button.onMouseLeftRelease = () => + { + new AnimateGUI(image, { + "size": "1 1 -1 -1", + "duration": 100 + }); + }; + button.onMouseLeftDoubleClick = () => + { + mapSelected.set(map); + this.setIndexOfSelected(listIndex) + mapBrowserReturn(true); + }; + button.onMouseEnter = () => + { + new AnimateGUI(mapBox, { "size": "-10 -10 10 10" }); + } + button.onMouseLeave = () => + { + new AnimateGUI(mapBox, { "size": "0 0 0 0" }); + new AnimateGUI(image, { + "size": "1 1 -1 -1", + "duration": 100 + }); + } +}; + +let mapZoom = {}; +mapZoom.originalSize = { + width: 400, + height: 300 +}; +mapZoom.levels = [ 0.35, 0.40, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 2.0, 2.4, 3.3 ]; +mapZoom.selected = mapZoom.levels.indexOf(0.6); +mapZoom.getLevel = function () +{ + return mapZoom.levels[ mapZoom.selected ]; +} +mapZoom.getSize = function () +{ + const level = mapZoom.getLevel(); + return { + width: mapZoom.originalSize.width * level, + height: mapZoom.originalSize.height * level + }; +} +mapZoom.in = function () +{ + if (mapZoom.selected == mapZoom.levels.length - 1) + return; + ++mapZoom.selected; + let size = mapZoom.getSize(); + mapBrowser.setChildDimensions(size.width, size.height); +} +mapZoom.out = function () +{ + if (mapZoom.selected == 0) + return; + --mapZoom.selected; + let size = mapZoom.getSize(); + mapBrowser.setChildDimensions(size.width, size.height); +} + +function init(data) +{ + // Sets map type, filter depending of data input + const type = data.mapSelected ? data.map.type : "random"; + const filter = data.mapSelected ? data.map.filter : "default"; + + // Get all maps names and make lists + mapList = GameMap.getMapsByFilter(GameMap.getMapsOfType(type)); + mapFilter = mapList[ filter ]; + + // Edge case: "random" is not a map + const validMapSelected = data.mapSelected && data.map.name != "random"; + + // Map name from gamesetup has path included + const fullMapName = validMapSelected ? data.map.name : mapFilter[ 0 ].file.path + mapFilter[ 0 ].file.name; + let mapSelectedIndex = mapFilter.map(map => map.file.path + map.file.name).indexOf(fullMapName); + + // Get childen size (with zoom set) + const childSize = mapZoom.getSize(); + // Set the first selected map so childFunction doesn't read an undefined value from mapSelected + mapSelected.set(mapFilter[ mapSelectedIndex ], filter); + // Create maps' grid + mapBrowser = new GridBrowser("MapBrowserContainer", "currentPageNum", mapFilter, mapSelectedIndex, childSize.width, childSize.height, childFunction); + + // Map filter type dropdown: set list, currently selected and behaviour + mapFilterDropdown = Engine.GetGUIObjectByName("mapFilterDropdown"); + mapFilterDropdown.list = GameMap.filtersList.map(filter => GameMap.filters[ filter ].name); + mapFilterDropdown.list_data = GameMap.filtersList; + mapFilterDropdown.selected = mapFilterDropdown.list_data.indexOf(filter); + mapFilterDropdown.onSelectionChange = () => + { + const filter = mapFilterDropdown.list_data[ mapFilterDropdown.selected ]; + mapFilter = mapList[ filter ]; + mapBrowser.setList(mapFilter); + mapBrowser.goToPage(0); + }; + + // Map type dropdown: set list, currently selected and behaviour + const mapTypeDropdown = Engine.GetGUIObjectByName("mapTypeDropdown"); + mapTypeDropdown.list = g_Settings.MapTypes.map(type => type.Title); + mapTypeDropdown.list_data = g_Settings.MapTypes.map(type => type.Name); + mapTypeDropdown.selected = mapTypeDropdown.list_data.indexOf(type); + mapTypeDropdown.onSelectionChange = () => + { + const type = mapTypeDropdown.list_data[ mapTypeDropdown.selected ]; + const filter = mapFilterDropdown.list_data[ mapFilterDropdown.selected ]; + mapList = GameMap.getMapsByFilter(GameMap.getMapsOfType(type)); + mapFilter = mapList[ filter ]; + mapBrowser.setList(mapFilter); + mapBrowser.goToPage(0); + }; + + // Zoom buttons + Engine.GetGUIObjectByName("mapsZoomIn").onPress = mapZoom.in; + Engine.GetGUIObjectByName("mapsZoomOut").onPress = mapZoom.out; + + // Tooltips + let goldenText = (text) => "[color=\"255 251 131\"]" + text + "[/color]"; + Engine.GetGUIObjectByName("mapsZoomIn").tooltip = translate(goldenText("\\[MouseWheelUp]") + ": Make map previews bigger."); + Engine.GetGUIObjectByName("mapsZoomOut").tooltip = translate(goldenText("\\[MouseWheelDown]") + ": Make map previews smaller?"); + Engine.GetGUIObjectByName("prevButton").tooltip = colorizeHotkey(translate("%(hotkey)s: Goes to previous page."), "tab.prev"); + Engine.GetGUIObjectByName("nextButton").tooltip = colorizeHotkey(translate("%(hotkey)s: Goes to next page."), "tab.next"); + Engine.GetGUIObjectByName("selectMapButton").tooltip = translate("Select map."); + Engine.GetGUIObjectByName("closeButton").tooltip = colorizeHotkey(translate("%(hotkey)s: Close map browser."), "cancel"); +} + +function mapBrowserReturn(sendSelected = false) +{ + // Pop the page before calling the callback, so the callback runs + // in the parent GUI page's context + const data = sendSelected ? + { + "mapSelected": true, + "map": { + "path": mapSelected.map.file.path, + "name": mapSelected.map.file.name, + "type": mapSelected.map.type, + "filter": mapSelected.filter + } + } : + { + "mapSelected": false + }; + + Engine.PopGuiPageCB(data); +} + +function onTick() +{ + AnimateGUI.onTick(); +} \ No newline at end of file Index: binaries/data/mods/public/gui/mapbrowser/mapbrowser.xml =================================================================== --- binaries/data/mods/public/gui/mapbrowser/mapbrowser.xml +++ binaries/data/mods/public/gui/mapbrowser/mapbrowser.xml @@ -0,0 +1,121 @@ + + + + +