Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 27729) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 27730) @@ -1,881 +1,894 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_CivData = loadCivData(false, true); 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_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; var g_Ambient; var g_AutoFormation; var g_Chat; var g_Cheats; var g_DeveloperOverlay; var g_DiplomacyColors; var g_DiplomacyDialog; var g_GameSpeedControl; var g_Menu; var g_MiniMapPanel; var g_NetworkStatusOverlay; var g_NetworkDelayOverlay; var g_ObjectivesDialog; var g_OutOfSyncNetwork; var g_OutOfSyncReplay; var g_PanelEntityManager; var g_PauseControl; var g_PauseOverlay; var g_PlayerViewControl; var g_QuitConfirmationDefeat; var g_QuitConfirmationReplay; var g_RangeOverlayManager; var g_ResearchProgress; var g_TimeNotificationOverlay; var g_TopPanel; var g_TradeDialog; /** * Map, player and match settings set in game setup. */ const g_InitAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes")); /** * True if this is a multiplayer game. */ const g_IsNetworked = Engine.HasNetClient(); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController = !g_IsNetworked || Engine.IsNetController(); /** * Whether we have finished the synchronization and * can start showing simulation related message boxes. */ var g_IsNetworkedActive = false; /** * True if the connection to the server has been lost. */ var g_Disconnected = false; /** * True if the current user has observer capabilities. */ var g_IsObserver = false; /** * True if the current user has rejoined (or joined the game after it started). */ var g_HasRejoined = false; /** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); /** * True if the camera should focus on attacks and player commands * and select the affected units. */ var g_FollowPlayer = false; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var g_LastTickTime = Date.now(); /** * Recalculate which units have their status bars shown with this frequency in milliseconds. */ var g_StatusBarUpdate = 200; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; /** * Remembers which clients are assigned to which player slots. * The keys are GUIDs or "local" in single-player. */ var g_PlayerAssignments; /** * Whether the entire UI should be hidden (useful for promotional screenshots). * Can be toggled with a hotkey. */ var g_ShowGUI = true; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_ResourceData = new Resources(); /** * These handlers are called each time a new turn was simulated. * Use this as sparely as possible. */ var g_SimulationUpdateHandlers = new Set(); /** * These handlers are called after the player states have been initialized. */ var g_PlayersInitHandlers = new Set(); /** * These handlers are called when a player has been defeated or won the game. */ var g_PlayerFinishedHandlers = new Set(); /** * These events are fired whenever the player added or removed entities from the selection. */ var g_EntitySelectionChangeHandlers = new Set(); /** * These events are fired when the user has performed a hotkey assignment change. * Currently only fired on init, but to be fired from any hotkey editor dialog. */ var g_HotkeyChangeHandlers = new Set(); /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Order in which the panel entities are shown. */ var g_PanelEntityOrder = ["Hero", "Relic"]; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"]; /** * Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey. */ var g_MilitaryTypes = ["Melee", "Ranged"]; function GetSimState() { if (!g_SimState) g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState")); return g_SimState; } function GetMultipleEntityStates(ents) { if (!ents.length) return null; let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents); for (let item of entityStates) g_EntityStates[item.entId] = item.state && deepfreeze(item.state); return entityStates; } function GetEntityState(entId) { if (!g_EntityStates[entId]) { let entityState = Engine.GuiInterfaceCall("GetEntityState", entId); g_EntityStates[entId] = entityState && deepfreeze(entityState); } return g_EntityStates[entId]; } /** * Returns template data calling GetTemplateData defined in GuiInterface.js * and deepfreezing returned object. * @param {string} templateName - Data of this template will be returned. * @param {number|undefined} player - Modifications of this player will be applied to the template. * If undefined, id of player calling this method will be used. */ function GetTemplateData(templateName, player) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": player }); translateObjectKeys(template, ["specific", "generic", "tooltip"]); g_TemplateData[templateName] = deepfreeze(template); } return g_TemplateData[templateName]; } function GetTechnologyData(technologyName, civ) { if (!g_TechnologyData[civ]) g_TechnologyData[civ] = {}; if (!(technologyName in g_TechnologyData[civ])) { const tech = TechnologyTemplates.Get(technologyName); if (!tech) return; let template = GetTechnologyDataHelper(tech, civ, g_ResourceData); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); g_TechnologyData[civ][technologyName] = deepfreeze(template); } return g_TechnologyData[civ][technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } // Fallback used by atlas g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } }; // Fallback used by atlas and autostart games if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name) g_PlayerAssignments.local.name = singleplayerName(); if (initData) { g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); } if (g_InitAttributes.campaignData) g_CampaignSession = new CampaignSession(g_InitAttributes.campaignData); let mapCache = new MapCache(); g_Cheats = new Cheats(); g_DiplomacyColors = new DiplomacyColors(); g_PlayerViewControl = new PlayerViewControl(); g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects); g_PauseControl = new PauseControl(); g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay); g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates); g_Ambient = new Ambient(); g_AutoFormation = new AutoFormation(); g_Chat = new Chat(g_PlayerViewControl, g_Cheats); g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection); g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors); g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl); g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes); g_NetworkStatusOverlay = new NetworkStatusOverlay(); g_NetworkDelayOverlay = new NetworkDelayOverlay(); g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache); g_OutOfSyncNetwork = new OutOfSyncNetwork(); g_OutOfSyncReplay = new OutOfSyncReplay(); g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder); g_PauseOverlay = new PauseOverlay(g_PauseControl); g_QuitConfirmationDefeat = new QuitConfirmationDefeat(); g_QuitConfirmationReplay = new QuitConfirmationReplay(); g_RangeOverlayManager = new RangeOverlayManager(g_Selection); g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection); g_TradeDialog = new TradeDialog(g_PlayerViewControl); g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl); g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl); initBatchTrain(); initDisplayedNames(); initSelectionPanels(); LoadModificationTemplates(); updatePlayerData(); initializeMusic(); // before changing the perspective Engine.SetBoundingBoxDebugOverlay(false); for (let handler of g_PlayersInitHandlers) handler(); for (let handler of g_HotkeyChangeHandlers) handler(); if (hotloadData) { g_Selection.selected = hotloadData.selection; g_PlayerAssignments = hotloadData.playerAssignments; g_Players = hotloadData.player; } // TODO: use event instead onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); } function registerPlayersInitHandler(handler) { g_PlayersInitHandlers.add(handler); } function registerPlayersFinishedHandler(handler) { g_PlayerFinishedHandlers.add(handler); } function registerSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.add(handler); } function unregisterSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.delete(handler); } function registerEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.add(handler); } function unregisterEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.delete(handler); } function registerHotkeyChangeHandler(handler) { g_HotkeyChangeHandlers.add(handler); } function updatePlayerData() { let simState = GetSimState(); if (!simState) return; let playerData = []; for (let i = 0; i < simState.players.length; ++i) { let playerState = simState.players[i]; playerData.push({ "name": playerState.name, "civ": playerState.civ, "color": { "r": playerState.color.r * 255, "g": playerState.color.g * 255, "b": playerState.color.b * 255, "a": playerState.color.a * 255 }, "team": playerState.team, "teamLocked": playerState.teamLocked, "cheatsEnabled": playerState.cheatsEnabled, "state": playerState.state, "isAlly": playerState.isAlly, "isMutualAlly": playerState.isMutualAlly, "isNeutral": playerState.isNeutral, "isEnemy": playerState.isEnemy, "guid": undefined, // network guid for players controlled by hosts "offline": g_Players[i] && !!g_Players[i].offline }); } for (let guid in g_PlayerAssignments) { let playerID = g_PlayerAssignments[guid].player; if (!playerData[playerID]) continue; playerData[playerID].guid = guid; playerData[playerID].name = g_PlayerAssignments[guid].name; } g_Players = playerData; } /** * @param {number} ent - The entity to get its ID for. * @return {number} - The entity ID of the entity or of its garrisonHolder. */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.garrisonable && entState.garrisonable.holder != INVALID_ENTITY) return getEntityOrHolder(entState.garrisonable.holder); return ent; } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); } function resetTemplates() { // Update GUI and clear player-dependent cache g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); // TODO: do this more selectively onSimulationUpdate(); } /** * Returns true if the player with that ID is in observermode. */ function isPlayerObserver(playerID) { let playerStates = GetSimState().players; return !playerStates[playerID] || playerStates[playerID].state != "active"; } /** * Returns true if the current user can issue commands for that player. */ function controlsPlayer(playerID) { let playerStates = GetSimState().players; return !!playerStates[Engine.GetPlayerID()] && playerStates[Engine.GetPlayerID()].controlsAll || Engine.GetPlayerID() == playerID && !!playerStates[playerID] && playerStates[playerID].state != "defeated"; } /** * Called when one or more players have won or were defeated. * * @param {array} - IDs of the players who have won or were defeated. * @param {Object} - a plural string stating the victory reason. * @param {boolean} - whether these players have won or lost. */ function playersFinished(players, victoryString, won) { addChatMessage({ "type": "playerstate", "message": victoryString, "players": players }); updatePlayerData(); // TODO: The other calls in this function should move too for (let handler of g_PlayerFinishedHandlers) handler(players, won); if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning()) return; global.music.setState( won ? global.music.states.VICTORY : global.music.states.DEFEAT ); } function resumeGame() { g_PauseControl.implicitResume(); } function closeOpenDialogs() { g_Menu.close(); g_Chat.closePage(); g_DiplomacyDialog.close(); g_ObjectivesDialog.close(); g_TradeDialog.close(); } function endGame(showSummary) { // Before ending the game let replayDirectory = Engine.GetCurrentReplayDirectory(); let simData = Engine.GuiInterfaceCall("GetReplayMetadata"); let playerID = Engine.GetPlayerID(); Engine.EndGame(); // After the replay file was closed in EndGame // Done here to keep EndGame small if (!g_IsReplay) Engine.AddReplayToCache(replayDirectory); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); let summaryData = { "sim": simData, "gui": { "dialog": false, "assignedPlayer": playerID, "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }; if (g_InitAttributes.campaignData) { let menu = g_CampaignSession.getMenu(); if (g_InitAttributes.campaignData.skipSummary) { Engine.SwitchGuiPage(menu); return; } summaryData.campaignData = { "filename": g_InitAttributes.campaignData.run }; summaryData.nextPage = menu; } if (showSummary) Engine.SwitchGuiPage("page_summary.xml", summaryData); else if (g_InitAttributes.campaignData) Engine.SwitchGuiPage(summaryData.nextPage, summaryData.campaignData); else if (Engine.HasXmppClient()) Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false }); else if (g_IsReplay) Engine.SwitchGuiPage("page_replaymenu.xml"); else Engine.SwitchGuiPage("page_pregame.xml"); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected, "playerAssignments": g_PlayerAssignments, "player": g_Players, }; } function getSavedGameData() { return { "groups": g_Groups.groups }; } function restoreSavedGameData(data) { // Restore camera if any if (data.camera) Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ, data.camera.RotX, data.camera.RotY, data.camera.Zoom); // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (let groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } /** * Called every frame. */ function onTick() { if (!g_Settings) return; let now = Date.now(); let tickLength = now - g_LastTickTime; g_LastTickTime = now; handleNetMessages(); updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; // When selection changed, get the entityStates of new entities GetMultipleEntityStates(g_Selection.filter(entId => !g_EntityStates[entId])); for (let handler of g_EntitySelectionChangeHandlers) handler(); updateGUIObjects(); // Display rally points for selected structures. if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); updateTimers(); Engine.GuiInterfaceCall("ClearRenamedEntities"); let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; if (isPlayingCinemaPath) updateCinemaOverlay(); } function onSimulationUpdate() { // Templates change depending on technologies and auras, so they have to be reloaded after such a change. // g_TechnologyData data never changes, so it shouldn't be deleted. g_EntityStates = {}; if (Engine.GuiInterfaceCall("IsTemplateModified")) { g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); } g_SimState = undefined; // Some changes may require re-rendering the selection. if (Engine.GuiInterfaceCall("IsSelectionDirty")) { g_Selection.onChange(); Engine.GuiInterfaceCall("ResetSelectionDirty"); } if (!GetSimState()) return; GetMultipleEntityStates(g_Selection.toList()); for (let handler of g_SimulationUpdateHandlers) handler(); // TODO: Move to handlers updateCinemaPath(); handleNotifications(); updateGUIObjects(); } function toggleGUI() { g_ShowGUI = !g_ShowGUI; updateCinemaPath(); } +var g_HasHiddenSilhouettes = false; function updateCinemaPath() { let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath; Engine.GetGUIObjectByName("cinemaOverlay").hidden = !isPlayingCinemaPath; - Engine.ConfigDB_CreateValue("user", "silhouettes", !isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true" ? "true" : "false"); + // TODO: This isn't great and should use a different system. + if (isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true") + { + Engine.ConfigDB_CreateValue("user", "silhouettes", "false"); + g_HasHiddenSilhouettes = true; + } + else if (!isPlayingCinemaPath && g_HasHiddenSilhouettes) + { + // TODO: Keyboard shortcuts can still try to toggle silhouettes + // which would behave incorrectly on reset. + Engine.ConfigDB_Reload(); + g_HasHiddenSilhouettes = false; + } } function updateCinemaOverlay() { let cinemaOverlay = Engine.GetGUIObjectByName("cinemaOverlay"); let width = cinemaOverlay.getComputedSize().right; let height = cinemaOverlay.getComputedSize().bottom; let barHeight = (height - width / 2.39) / 2; if (barHeight < 0) barHeight = 0; let cinemaBarTop = Engine.GetGUIObjectByName("cinemaBarTop"); let cinemaBarTopSize = cinemaBarTop.size; cinemaBarTopSize.bottom = barHeight; cinemaBarTop.size = cinemaBarTopSize; let cinemaBarBottom = Engine.GetGUIObjectByName("cinemaBarBottom"); let cinemaBarBottomSize = cinemaBarBottom.size; cinemaBarBottomSize.top = -barHeight; cinemaBarBottom.size = cinemaBarBottomSize; } // TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateGroups(); updateSelectionDetails(); updateBuildingPlacementPreview(); if (!g_IsObserver) { // Update music state on basis of battle state. let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer); if (battleState) global.music.setState(global.music.states[battleState]); } } function updateGroups() { g_Groups.update(); // Determine the sum of the costs of a given template let getCostSum = (ent) => { let cost = GetTemplateData(GetEntityState(ent).template).cost; return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0; }; for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children) { Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = +i + 1; let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]"); button.hidden = g_Groups.groups[i].getTotalCount() == 0; button.onPress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i); button.onDoublePress = (function(i) { return function() { performGroup("snap", i); }; })(i); button.onPressRight = (function(i) { return function() { performGroup("breakUp", i); }; })(i); // Choose the icon of the most common template (or the most costly if it's not unique) if (g_Groups.groups[i].getTotalCount() > 0) { let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => { if (pre.ents.length == cur.ents.length) return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur; return pre.ents.length > cur.ents.length ? pre : cur; }).ents[0]).template).icon; Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite = icon ? ("stretched:session/portraits/" + icon) : "groupsIcon"; } setPanelObjectPosition(button, i, 1); } } /** * Toggles the display of status bars for all of the player's entities. * * @param {boolean} remove - Whether to hide all previously shown status bars. */ function recalculateStatusBarDisplay(remove = false) { let entities; if (g_ShowAllStatusBars && !remove) entities = g_ViewedPlayer == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer); else { let selected = g_Selection.toList(); for (const ent of g_Selection.highlighted) selected.push(ent); // Remove selected entities from the 'all entities' array, // to avoid disabling their status bars. entities = Engine.GuiInterfaceCall( g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", { "viewedPlayer": g_ViewedPlayer }).filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars && !remove, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } function removeStatusBarDisplay() { if (g_ShowAllStatusBars) recalculateStatusBarDisplay(true); } /** * Updates the primary/secondary names in the simulation and GUI. */ function updateDisplayedNames() { g_SpecificNamesPrimary = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 2; g_ShowSecondaryNames = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 1; } /** * Inverts the given configuration boolean and returns the current state. * For example "silhouettes". */ function toggleConfigBool(configName) { let enabled = Engine.ConfigDB_GetValue("user", configName) != "true"; Engine.ConfigDB_CreateAndSaveValue("user", configName, String(enabled)); return enabled; } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { let entsAdd = []; // list of entities units to be highlighted let entsRemove = []; let highlighted = g_Selection.toList(); for (const ent of g_Selection.highlighted) highlighted.push(ent); if (g_ShowGuarding) // flag the guarding entities to add in this additional highlight for (const sel of g_Selection.toList()) { const state = GetEntityState(sel); if (!state.guard || !state.guard.entities.length) continue; for (let ent of state.guard.entities) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } if (g_ShowGuarded) // flag the guarded entities to add in this additional highlight for (const sel of g_Selection.toList()) { const state = GetEntityState(sel); if (!state.unitAI || !state.unitAI.isGuarding) continue; let ent = state.unitAI.isGuarding; if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } // flag the entities to remove (from the previously added) from this additional highlight for (let ent of g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd, g_HighlightedAlpha, true); _setHighlight(entsRemove, 0, false); g_AdditionalHighlight = entsAdd; }