Index: ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js (revision 18203) +++ ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js (revision 18204) @@ -1,169 +1,167 @@ // We want to pass callback functions for the different buttons in a convenient way. // Because passing functions accross compartment boundaries is a pain, we just store them here together with some optional arguments. // The messageBox page will return the code of the pressed button and the according function will be called. var g_MessageBoxBtnFunctions = []; var g_MessageBoxCallbackArgs = []; var g_MessageBoxCallbackFunction = function(btnCode) { if (btnCode !== undefined && g_MessageBoxBtnFunctions[btnCode]) { // Cache the variables to make it possible to call a messageBox from a callback function. let callbackFunction = g_MessageBoxBtnFunctions[btnCode]; let callbackArgs = g_MessageBoxCallbackArgs[btnCode]; g_MessageBoxBtnFunctions = []; g_MessageBoxCallbackArgs = []; if (callbackArgs !== undefined) callbackFunction(callbackArgs); else callbackFunction(); return; } g_MessageBoxBtnFunctions = []; g_MessageBoxCallbackArgs = []; }; function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs) { if (g_MessageBoxBtnFunctions && g_MessageBoxBtnFunctions.length) { warn("A messagebox was called when a previous callback function is still set, aborting!"); return; } g_MessageBoxBtnFunctions = mbBtnCode; g_MessageBoxCallbackArgs = mbCallbackArgs || g_MessageBoxCallbackArgs; Engine.PushGuiPage("page_msgbox.xml", { "width": mbWidth, "height": mbHeight, "message": mbMessage, "title": mbTitle, "buttonCaptions": mbButtonCaptions, "callback": mbBtnCode && "g_MessageBoxCallbackFunction" }); } function openURL(url) { Engine.OpenURL(url); messageBox( 600, 200, sprintf( translate("Opening %(url)s\n in default web browser. Please wait...."), { "url": url } ), translate("Opening page") ); } function updateCounters() { var caption = ""; var linesCount = 0; var researchCount = 0; if (Engine.ConfigDB_GetValue("user", "overlay.fps") === "true") { caption += sprintf(translate("FPS: %(fps)4s"), { "fps": Engine.GetFPS() }) + "\n"; ++linesCount; } if (Engine.ConfigDB_GetValue("user", "overlay.realtime") === "true") { caption += (new Date()).toLocaleTimeString() + "\n"; ++linesCount; } // If game has been started if (typeof g_SimState != "undefined") { if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true") { var currentSpeed = Engine.GetSimRate(); if (currentSpeed != 1.0) // Translation: The "x" means "times", with the mathematical meaning of multiplication. caption += sprintf(translate("%(time)s (%(speed)sx)"), { "time": timeToString(g_SimState.timeElapsed), "speed": Engine.FormatDecimalNumberIntoString(currentSpeed) }); else caption += timeToString(g_SimState.timeElapsed); caption += "\n"; ++linesCount; } var diplomacyCeasefireCounter = Engine.GetGUIObjectByName("diplomacyCeasefireCounter"); if (g_SimState.ceasefireActive) { // Update ceasefire counter in the diplomacy window var remainingTimeString = timeToString(g_SimState.ceasefireTimeRemaining); diplomacyCeasefireCounter.caption = sprintf( translateWithContext("ceasefire", "Time remaining until ceasefire is over: %(time)s."), { "time": remainingTimeString } ); // Update ceasefire overlay counter if (Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true") { caption += remainingTimeString + "\n"; ++linesCount; } } else if (!diplomacyCeasefireCounter.hidden) { diplomacyCeasefireCounter.hidden = true; updateDiplomacy(); } g_ResearchListTop = 4; if (linesCount) g_ResearchListTop += 14 * linesCount; } var dataCounter = Engine.GetGUIObjectByName("dataCounter"); dataCounter.caption = caption; dataCounter.size = sprintf("100%%-100 40 100%%-5 %(bottom)s", { "bottom": 40 + 14 * linesCount }); dataCounter.hidden = linesCount == 0; } /** * Update the overlay with the most recent network warning of each client. */ function displayGamestateNotifications() { let messages = []; let maxTextWidth = 0; - // TODO: Players who paused the game should be added here - // Add network warnings if (Engine.ConfigDB_GetValue("user", "overlay.netwarnings") == "true") { let netwarnings = getNetworkWarnings(); messages = messages.concat(netwarnings.messages); maxTextWidth = Math.max(maxTextWidth, netwarnings.maxTextWidth); } // Resize textbox let width = maxTextWidth + 20; let height = 14 * messages.length; // Position left of the dataCounter let top = "40"; let right = Engine.GetGUIObjectByName("dataCounter").hidden ? "100%-15" : "100%-110"; let bottom = top + "+" + height; let left = right + "-" + width; let gameStateNotifications = Engine.GetGUIObjectByName("gameStateNotifications"); gameStateNotifications.caption = messages.join("\n"); gameStateNotifications.hidden = !messages.length; gameStateNotifications.size = left + " " + top + " " + right + " " + bottom; setTimeout(displayGamestateNotifications, 1000); } Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 18203) +++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 18204) @@ -1,754 +1,803 @@ // Menu / panel border size const MARGIN = 4; // Includes the main menu button const NUM_BUTTONS = 9; // Regular menu buttons const 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; // Menu starting position: overall const INITIAL_MENU_POSITION = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM; // Number of pixels per millisecond to move const MENU_SPEED = 1.2; // Available resources in trade and tribute menu const RESOURCES = ["food", "wood", "stone", "metal"]; // Trade menu: step for probability changes const STEP = 5; // Shown in the trade dialog. const g_IdleTraderTextColor = "orange"; var g_IsMenuOpen = false; var g_IsDiplomacyOpen = false; var g_IsTradeOpen = false; // 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() {}; // Ignore size defined in XML and set the actual menu size here function initMenuPosition() { Engine.GetGUIObjectByName("menu").size = INITIAL_MENU_POSITION; } 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() { if (g_Disconnected) return; closeOpenDialogs(); setTeamChat(false); Engine.GetGUIObjectByName("chatInput").focus(); // Grant focus to the input area Engine.GetGUIObjectByName("chatDialogPanel").hidden = false; } function closeChat() { Engine.GetGUIObjectByName("chatInput").caption = ""; // Clear chat input Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus Engine.GetGUIObjectByName("chatDialogPanel").hidden = true; } /** * If the teamchat hotkey was pressed, set allies or observers as addressees, * otherwise send to everyone. */ function setTeamChat(teamChat = false) { let command = teamChat ? (g_IsObserver ? "/observers" : "/allies") : ""; let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); chatAddressee.selected = chatAddressee.list_data.indexOf(command); } /** * Opens chat-window or closes it and sends the userinput. */ function toggleChatWindow(teamChat) { if (g_Disconnected) return; let chatWindow = Engine.GetGUIObjectByName("chatDialogPanel"); let chatInput = Engine.GetGUIObjectByName("chatInput"); let hidden = chatWindow.hidden; closeOpenDialogs(); if (hidden) { setTeamChat(teamChat); chatInput.focus(); } else { if (chatInput.caption.length) { submitChatInput(); return; } chatInput.caption = ""; } chatWindow.hidden = !hidden; } function openDiplomacy() { closeOpenDialogs(); if (g_ViewedPlayer < 1) return; g_IsDiplomacyOpen = true; let isCeasefireActive = GetSimState().ceasefireActive; // 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); diplomacyFormatTributeButtons(i, myself || playerInactive); diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]); } Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false; } 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; // Set background color row.sprite = "color: " + rgbToGuiColor(g_Players[i].color) + " 32"; Engine.GetGUIObjectByName("diplomacyPlayerName["+(i-1)+"]").caption = colorizePlayernameByID(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; button.caption = g_Players[g_ViewedPlayer]["is" + stance][i] ? translate("x") : ""; button.enabled = controlsPlayer(g_ViewedPlayer); button.onpress = (function(player, stance) { return function() { Engine.PostNetworkCommand({ "type": "diplomacy", "player": i, "to": stance.toLowerCase() }); }; })(i, stance); } } function diplomacyFormatTributeButtons(i, hidden) { for (let resource of RESOURCES) { let button = Engine.GetGUIObjectByName("diplomacyPlayerTribute"+resource[0].toUpperCase()+resource.substring(1)+"["+(i-1)+"]"); button.hidden = hidden; if (hidden) continue; button.enabled = controlsPlayer(g_ViewedPlayer); button.tooltip = formatTributeTooltip(i, resource, 100); button.onpress = (function(i, resource, 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 type of RESOURCES) amounts[type] = 0; amounts[resource] = 100 * multiplier; button.tooltip = formatTributeTooltip(i, resource, amounts[resource]); // 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, resource, 100); }; if (!isBatchTrainPressed) g_FlushTributing(); }; })(i, resource, 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, "target": i }); }; })(i); } function closeDiplomacy() { g_IsDiplomacyOpen = false; Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = true; } function toggleDiplomacy() { let open = g_IsDiplomacyOpen; closeOpenDialogs(); if (!open) openDiplomacy(); } function openTrade() { closeOpenDialogs(); if (g_ViewedPlayer < 1) return; g_IsTradeOpen = true; var updateButtons = function() { for (var res in button) { button[res].label.caption = proba[res] + "%"; button[res].sel.hidden = !controlsPlayer(g_ViewedPlayer) || res != selec; button[res].up.hidden = !controlsPlayer(g_ViewedPlayer) || res == selec || proba[res] == 100 || proba[selec] == 0; button[res].dn.hidden = !controlsPlayer(g_ViewedPlayer) || res == selec || proba[res] == 0 || proba[selec] == 100; } }; var proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer); var button = {}; var selec = RESOURCES[0]; for (var i = 0; i < RESOURCES.length; ++i) { var buttonResource = Engine.GetGUIObjectByName("tradeResource["+i+"]"); if (i > 0) { var size = Engine.GetGUIObjectByName("tradeResource["+(i-1)+"]").size; var width = size.right - size.left; size.left += width; size.right += width; Engine.GetGUIObjectByName("tradeResource["+i+"]").size = size; } var resource = RESOURCES[i]; proba[resource] = (proba[resource] ? proba[resource] : 0); var buttonResource = Engine.GetGUIObjectByName("tradeResourceButton["+i+"]"); var icon = Engine.GetGUIObjectByName("tradeResourceIcon["+i+"]"); icon.sprite = "stretched:session/icons/resources/" + resource + ".png"; var label = Engine.GetGUIObjectByName("tradeResourceText["+i+"]"); var buttonUp = Engine.GetGUIObjectByName("tradeArrowUp["+i+"]"); var buttonDn = Engine.GetGUIObjectByName("tradeArrowDn["+i+"]"); var iconSel = Engine.GetGUIObjectByName("tradeResourceSelection["+i+"]"); button[resource] = { "up": buttonUp, "dn": buttonDn, "label": label, "sel": iconSel }; buttonResource.enabled = controlsPlayer(g_ViewedPlayer); buttonResource.onpress = (function(resource){ return function() { if (Engine.HotkeyIsPressed("session.fulltradeswap")) { for (var ress of RESOURCES) proba[ress] = 0; proba[resource] = 100; Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba}); } selec = resource; updateButtons(); }; })(resource); buttonUp.enabled = controlsPlayer(g_ViewedPlayer); buttonUp.onpress = (function(resource){ return function() { proba[resource] += Math.min(STEP, proba[selec]); proba[selec] -= Math.min(STEP, proba[selec]); Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba}); updateButtons(); }; })(resource); buttonDn.enabled = controlsPlayer(g_ViewedPlayer); buttonDn.onpress = (function(resource){ return function() { proba[selec] += Math.min(STEP, proba[resource]); proba[resource] -= Math.min(STEP, proba[resource]); Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba}); updateButtons(); }; })(resource); } updateButtons(); let traderNumber = Engine.GuiInterfaceCall("GetTraderNumber", g_ViewedPlayer); Engine.GetGUIObjectByName("landTraders").caption = getIdleLandTradersText(traderNumber); Engine.GetGUIObjectByName("shipTraders").caption = getIdleShipTradersText(traderNumber); Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false; } 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": "[color=\"" + g_IdleTraderTextColor + "\"]" + inactiveString + "[/color]" }); } 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": "[color=\"" + g_IdleTraderTextColor + "\"]" + inactiveString + "[/color]" }); } function closeTrade() { g_IsTradeOpen = false; Engine.GetGUIObjectByName("tradeDialogPanel").hidden = true; } function toggleTrade() { let open = g_IsTradeOpen; closeOpenDialogs(); if (!open) openTrade(); } function toggleGameSpeed() { let gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.hidden = !gameSpeed.hidden; } function openGameSummary() { closeOpenDialogs(); pauseGame(); let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); Engine.PushGuiPage("page_summary.xml", { "timeElapsed" : extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "players": g_Players, "mapSettings": Engine.GetMapSettings(), "isInGame": true, "gameResult": translate("Current Scores"), "callback": "resumeGame" }); } 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 the game in single player mode. + * 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() +function pauseGame(pause = true, explicit = false) { + if (g_IsNetworked && !explicit) + return; + + if (explicit) + g_Paused = pause; + + Engine.SetPaused(g_Paused || pause, !!explicit); + if (g_IsNetworked) + { + setClientPauseState(Engine.GetPlayerGUID(), g_Paused); return; + } - Engine.GetGUIObjectByName("pauseButtonText").caption = translate("Resume"); - Engine.GetGUIObjectByName("pauseOverlay").hidden = false; - Engine.SetPaused(true); + updatePauseOverlay(); } -function resumeGame() +/** + * Resume the game. + * + * @param explicit - true if the player explicitly wants to resume the game. + * If this argument isn't set, a multiplayer game won't be resumed and the pause overlay won't + * be closed in single player. + */ +function resumeGame(explicit = false) { - Engine.GetGUIObjectByName("pauseButtonText").caption = translate("Pause"); - Engine.GetGUIObjectByName("pauseOverlay").hidden = true; - Engine.SetPaused(false); + pauseGame(false, explicit); } +/** + * Called when the current player toggles a pause button. + */ function togglePause() { if (!Engine.GetGUIObjectByName("pauseButton").enabled) return; closeOpenDialogs(); - let pauseOverlay = Engine.GetGUIObjectByName("pauseOverlay"); + 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("pauseButtonText").caption = g_Paused ? translate("Resume") : translate("Pause"); + Engine.GetGUIObjectByName("resumeMessage").hidden = !g_Paused; - Engine.SetPaused(pauseOverlay.hidden); - Engine.GetGUIObjectByName("pauseButtonText").caption = pauseOverlay.hidden ? translate("Resume") : translate("Pause"); + Engine.GetGUIObjectByName("pausedByText").hidden = !g_IsNetworked; + Engine.GetGUIObjectByName("pausedByText").caption = sprintf(translate("Paused by %(players)s"), + { "players": g_PausingClients.map(guid => colorizePlayernameByGUID(guid)).join(translate(", ")) }); - pauseOverlay.hidden = !pauseOverlay.hidden; + 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() { // The developer overlay is disabled in ranked games if (Engine.HasXmppClient() && Engine.IsRankedGame()) 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."); Engine.PostNetworkCommand({ "type": "aichat", "message": message, "translateMessage": true, "translateParameters": [], "parameters": {} }); } function closeOpenDialogs() { closeMenu(); closeChat(); closeDiplomacy(); closeTrade(); } function formatTributeTooltip(playerID, resource, amount) { return sprintf(translate("Tribute %(resourceAmount)s %(resourceType)s to %(playerName)s. Shift-click to tribute %(greaterAmount)s."), { "resourceAmount": amount, "resourceType": getLocalizedResourceName(resource, "withinSentence"), "playerName": colorizePlayernameByID(playerID), "greaterAmount": amount < 500 ? 500 : amount + 500 }); } Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 18203) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 18204) @@ -1,900 +1,907 @@ /** * All known cheat commands. * @type {Object} */ const g_Cheats = getCheatsData(); /** * Number of seconds after which chatmessages will disappear. */ const g_ChatTimeout = 30; /** * Maximum number of lines to display simultaneously. */ const g_ChatLines = 20; /** * The strings to be displayed including sender and formating. */ var g_ChatMessages = []; /** * Holds the timer-IDs used for hiding the chat after g_ChatTimeout seconds. */ var g_ChatTimers = []; /** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => handleNetStatusMessage(msg), "netwarn": msg => addNetworkWarning(msg), "players": msg => handlePlayerAssignmentsMessage(msg), + "paused": msg => setClientPauseState(msg.guid, msg.pause), "rejoined": msg => addChatMessage({ "type": "rejoined", "guid": msg.guid }), "kicked": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been kicked"), { "username": msg.username }) }), "banned": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been banned"), { "username": msg.username }) }), "chat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }), "aichat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text, "translate": true }), "gamesetup": msg => "", // Needed for autostart "start": msg => "" }; var g_FormatChatMessage = { "system": msg => msg.text, "connect": msg => sprintf(translate("%(player)s is starting to rejoin the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "disconnect": msg => sprintf(translate("%(player)s has left the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "rejoined": msg => sprintf(translate("%(player)s has rejoined the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "clientlist": msg => getUsernameList(), "message": msg => formatChatCommand(msg), "defeat": msg => formatDefeatMessage(msg), "diplomacy": msg => formatDiplomacyMessage(msg), "tribute": msg => formatTributeMessage(msg), "attack": msg => formatAttackMessage(msg) }; /** * Show a label and grey overlay or hide both on connection change. */ var g_StatusMessageTypes = { "authenticated": msg => translate("Connection to the server has been authenticated."), "connected": msg => translate("Connected to the server."), "disconnected": msg => translate("Connection to the server has been lost.") + "\n" + // Translation: States the reason why the client disconnected from the server. sprintf(translate("Reason: %(reason)s."), { "reason": getDisconnectReason(msg.reason, true) }), "waiting_for_players": msg => translate("Waiting for other players to connect..."), "join_syncing": msg => translate("Synchronising gameplay with other players..."), "active": msg => "" }; /** * Chatmessage shown after commands like /me or /enemies. */ var g_ChatCommands = { "regular": { "context": translate("(%(context)s) %(userTag)s %(message)s"), "no-context": translate("%(userTag)s %(message)s") }, "me": { "context": translate("(%(context)s) * %(user)s %(message)s"), "no-context": translate("* %(user)s %(message)s") } }; var g_ChatAddresseeContext = { "/team": translate("Team"), "/allies": translate("Ally"), "/enemies": translate("Enemy"), "/observers": translate("Observer"), "/msg": translate("Private") }; /** * Returns true if the current player is an addressee, given the chat message type and sender. */ var g_IsChatAddressee = { "/team": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[Engine.GetPlayerID()].team != -1 && g_Players[Engine.GetPlayerID()].team == g_Players[senderID].team, "/allies": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[senderID].isMutualAlly[Engine.GetPlayerID()], "/enemies": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[senderID].isEnemy[Engine.GetPlayerID()], "/observers": senderID => g_IsObserver, "/msg": (senderID, addresseeGUID) => addresseeGUID == Engine.GetPlayerGUID() }; /** * Chatmessage shown on diplomacy change. */ var g_DiplomacyMessages = { "active": { "ally": translate("You are now allied with %(player)s."), "enemy": translate("You are now at war with %(player)s."), "neutral": translate("You are now neutral with %(player)s.") }, "passive": { "ally": translate("%(player)s is now allied with you."), "enemy": translate("%(player)s is now at war with you."), "neutral": translate("%(player)s is now neutral with you.") }, "observer": { "ally": translate("%(player)s is now allied with %(player2)s."), "enemy": translate("%(player)s is now at war with %(player2)s."), "neutral": translate("%(player)s is now neutral with %(player2)s.") } }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. */ var g_NotificationsTypes = { "chat": function(notification, player) { let message = { "type": "message", "guid": findGuidForPlayerID(player) || -1, "text": notification.message }; if (message.guid == -1) message.player = player; addChatMessage(message); }, "aichat": function(notification, player) { let message = { "guid": findGuidForPlayerID(player) || -1, "type": "message", "text": notification.message, "translate": true }; if (message.guid == -1) message.player = player; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; // special case for formatting of player names which are transmitted as _player_num for (let param in message.parameters) { if (!param.startsWith("_player_")) continue; message.parameters[param] = colorizePlayernameByID(message.parameters[param]); } } addChatMessage(message); }, "defeat": function(notification, player) { addChatMessage({ "type": "defeat", "guid": findGuidForPlayerID(player), "player": player, "resign": !!notification.resign }); updateDiplomacy(); updateChatAddressees(); }, "diplomacy": function(notification, player) { addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); updateDiplomacy(); }, "quit": function(notification, player) { Engine.Exit(); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "attack": function(notification, player) { if (player != g_ViewedPlayer) return; // Focus camera on attacks if (g_FollowPlayer) { setCameraFollow(notification.target); g_Selection.reset(); if (notification.target) g_Selection.addList([notification.target]); } if (Engine.ConfigDB_GetValue("user", "gui.session.attacknotificationmessage") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "dialog": function(notification, player) { if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, "resetselectionpannel": function(notification, player) { if (player != Engine.GetPlayerID()) return; g_Selection.rebuildSelection({}); }, "playercommand": function(notification, player) { // For observers, focus the camera on units commanded by the selected player if (!g_FollowPlayer || player != g_ViewedPlayer) return; let cmd = notification.cmd; // Ignore boring animals let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]); if (entState && entState.identity && entState.identity.classes && entState.identity.classes.indexOf("Animal") != -1) return; // Focus the building to construct if (cmd.type == "repair") { let targetState = GetEntityState(cmd.target); if (targetState) Engine.CameraMoveTo(targetState.position.x, targetState.position.z); } // Focus commanded entities, but don't lose previous focus when training units else if (cmd.type != "train" && cmd.type != "research" && entState) setCameraFollow(cmd.entities[0]); // Select units affected by that command let selection = []; if (cmd.entities) selection = cmd.entities; if (cmd.target) selection.push(cmd.target); // Allow gaia in selection when gathering g_Selection.reset(); g_Selection.addList(selection, false, cmd.type == "gather"); } }; /** * Loads all known cheat commands. * * @returns {Object} */ function getCheatsData() { let cheats = {}; for (let fileName of getJSONFileList("simulation/data/cheats/")) { let currentCheat = Engine.ReadJSONFile("simulation/data/cheats/"+fileName+".json"); if (!currentCheat) continue; if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1) warn("Cheat name '" + currentCheat.Name + "' is already present"); else cheats[currentCheat.Name] = currentCheat.Data; } return cheats; } /** * Reads userinput from the chat and sends a simulation command in case it is a known cheat. * Hence cheats won't be sent as chat over network. * * @param {string} text * @returns {boolean} - True if a cheat was executed. */ function executeCheat(text) { if (g_IsObserver || !g_Players[Engine.GetPlayerID()].cheatsEnabled) return false; // Find the cheat code that is a prefix of the user input let cheatCode = Object.keys(g_Cheats).find(cheatCode => text.indexOf(cheatCode) == 0); if (!cheatCode) return false; let cheat = g_Cheats[cheatCode]; let parameter = text.substr(cheatCode.length); if (cheat.isNumeric) parameter = +parameter; if (cheat.DefaultParameter && (isNaN(parameter) || parameter <= 0)) parameter = cheat.DefaultParameter; Engine.PostNetworkCommand({ "type": "cheat", "action": cheat.Action, "text": cheat.Type, "player": Engine.GetPlayerID(), "parameter": parameter, "templates": cheat.Templates, "selected": g_Selection.toList() }); return true; } function findGuidForPlayerID(playerID) { return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID); } /** * Processes all pending notifications sent from the GUIInterface simulation component. */ function handleNotifications() { let notifications = Engine.GuiInterfaceCall("GetNotifications"); for (let notification of notifications) { if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type]) { error("Invalid GUI notification: " + uneval(notification)); continue; } for (let player of notification.players) g_NotificationsTypes[notification.type](notification, player); } } /** * Updates playerdata cache and refresh diplomacy panel. */ function updateDiplomacy() { g_Players = getPlayerData(g_PlayerAssignments, g_Players); if (g_IsDiplomacyOpen) openDiplomacy(); } /** * Displays all active counters (messages showing the remaining time) for wonder-victory, ceasefire etc. */ function updateTimeNotifications() { let notifications = Engine.GuiInterfaceCall("GetTimeNotifications", g_ViewedPlayer); let notificationText = ""; for (let n of notifications) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); parameters.time = timeToString(n.endTime - g_SimState.timeElapsed); notificationText += sprintf(message, parameters) + "\n"; } Engine.GetGUIObjectByName("notificationText").caption = notificationText; } /** * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. * * @param {Object} msg */ function handleNetMessage(msg) { log("Net message: " + uneval(msg)); if (g_NetMessageTypes[msg.type]) g_NetMessageTypes[msg.type](msg); else error("Unrecognised net message type '" + msg.type + "'"); } /** * @param {Object} message */ function handleNetStatusMessage(message) { if (g_Disconnected) return; if (!g_StatusMessageTypes[message.status]) { error("Unrecognised netstatus type '" + message.status + "'"); return; } let label = Engine.GetGUIObjectByName("netStatus"); let statusMessage = g_StatusMessageTypes[message.status](message); label.caption = statusMessage; label.hidden = !statusMessage; if (message.status == "disconnected") { + // Hide the pause overlay, and pause animations. + Engine.GetGUIObjectByName("pauseOverlay").hidden = true; + Engine.SetPaused(true, false); + g_Disconnected = true; closeOpenDialogs(); } } function handlePlayerAssignmentsMessage(message) { // Find and report all leavings for (let guid in g_PlayerAssignments) { if (message.hosts[guid]) continue; + setClientPauseState(guid, false); + addChatMessage({ "type": "disconnect", "guid": guid }); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; } let joins = Object.keys(message.hosts).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.hosts; // Report all joinings joins.forEach(guid => { let playerID = g_PlayerAssignments[guid].player; if (g_Players[playerID]) { g_Players[playerID].guid = guid; g_Players[playerID].name = g_PlayerAssignments[guid].name; g_Players[playerID].offline = false; } addChatMessage({ "type": "connect", "guid": guid }); }); updateChatAddressees(); // Update lobby gamestatus if (g_IsController && Engine.HasXmppClient()) { let players = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name); Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", ")); } } function updateChatAddressees() { let addressees = [ { "label": translateWithContext("chat addressee", "Everyone"), "cmd": "" } ]; if (!g_IsObserver) { addressees.push({ "label": translateWithContext("chat addressee", "Allies"), "cmd": "/allies" }); addressees.push({ "label": translateWithContext("chat addressee", "Enemies"), "cmd": "/enemies" }); } addressees.push({ "label": translateWithContext("chat addressee", "Observers"), "cmd": "/observers" }); // Add playernames for private messages for (let guid of sortGUIDsByPlayerID()) { if (guid == Engine.GetPlayerGUID()) continue; let playerID = g_PlayerAssignments[guid].player; // Don't provide option for PM from observer to player if (g_IsObserver && !isPlayerObserver(playerID)) continue; let colorBox = isPlayerObserver(playerID) ? "" : colorizePlayernameHelper("■", playerID) + " "; addressees.push({ "cmd": "/msg " + g_PlayerAssignments[guid].name, "label": colorBox + g_PlayerAssignments[guid].name }); } let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); chatAddressee.list = addressees.map(adressee => adressee.label); chatAddressee.list_data = addressees.map(adressee => adressee.cmd); chatAddressee.selected = 0; } /** * Send text as chat. Don't look for commands. * * @param {string} text */ function submitChatDirectly(text) { if (!text.length) return; if (g_IsNetworked) Engine.SendNetworkChat(text); else addChatMessage({ "type": "message", "guid": "local", "text": text }); } /** * Loads the text from the GUI window, checks if it is a local command * or cheat and executes it. Otherwise sends it as chat. */ function submitChatInput() { let input = Engine.GetGUIObjectByName("chatInput"); let text = input.caption; input.blur(); // Remove focus input.caption = ""; // Clear chat input toggleChatWindow(); if (!text.length) return; if (executeNetworkCommand(text)) return; if (executeCheat(text)) return; let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); if (chatAddressee.selected > 0 && (text.indexOf("/") != 0 || text.indexOf("/me ") == 0)) text = chatAddressee.list_data[chatAddressee.selected] + " " + text; submitChatDirectly(text); } /** * Displays the prepared chatmessage. * * @param msg {Object} */ function addChatMessage(msg) { if (!g_FormatChatMessage[msg.type]) return; let formatted = g_FormatChatMessage[msg.type](msg); if (!formatted) return; g_ChatMessages.push(formatted); g_ChatTimers.push(setTimeout(removeOldChatMessage, g_ChatTimeout * 1000)); if (g_ChatMessages.length > g_ChatLines) removeOldChatMessage(); else Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } /** * Called when the timer has run out for the oldest chatmessage or when the message limit is reached. */ function removeOldChatMessage() { clearTimeout(g_ChatTimers[0]); g_ChatTimers.shift(); g_ChatMessages.shift(); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } /** * This function is used for AIs, whose names don't exist in g_PlayerAssignments. */ function colorizePlayernameByID(playerID) { let username = g_Players[playerID] && escapeText(g_Players[playerID].name); return colorizePlayernameHelper(username, playerID); } function colorizePlayernameByGUID(guid) { let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : ""; let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; return colorizePlayernameHelper(username, playerID); } function colorizePlayernameHelper(username, playerID) { let playerColor = playerID > -1 ? rgbToGuiColor(g_Players[playerID].color) : "white"; return '[color="' + playerColor + '"]' + (username || translate("Unknown Player")) + "[/color]"; } function formatDefeatMessage(msg) { return sprintf( msg.resign ? translate("%(player)s has resigned.") : translate("%(player)s has been defeated."), { "player": colorizePlayernameByID(msg.player) } ); } function formatDiplomacyMessage(msg) { let messageType; if (g_IsObserver) messageType = "observer"; else if (Engine.GetPlayerID() == msg.sourcePlayer) messageType = "active"; else if (Engine.GetPlayerID() == msg.targetPlayer) messageType = "passive"; else return ""; return sprintf(g_DiplomacyMessages[messageType][msg.status], { "player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer), "player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer) }); } function formatTributeMessage(msg) { // Check observer first, since we also want to see if the selected player in the developer-overlay has sent tributes let message = ""; if (g_IsObserver) message = translate("%(player)s has sent %(player2)s %(amounts)s."); else if (msg.targetPlayer == Engine.GetPlayerID()) message = translate("%(player)s has sent you %(amounts)s."); return sprintf(message, { "player": colorizePlayernameByID(msg.sourcePlayer), "player2": colorizePlayernameByID(msg.targetPlayer), "amounts": getLocalizedResourceAmounts(msg.amounts) }); } function formatAttackMessage(msg) { if (msg.player != g_ViewedPlayer) return ""; let message = msg.targetIsDomesticAnimal ? translate("Your livestock has been attacked by %(attacker)s!") : translate("You have been attacked by %(attacker)s!"); return sprintf(message, { "attacker": colorizePlayernameByID(msg.attacker) }); } function formatChatCommand(msg) { if (!msg.text) return ""; let isMe = msg.text.indexOf("/me ") == 0; if (!isMe && !parseChatAddressee(msg)) return ""; isMe = msg.text.indexOf("/me ") == 0; if (isMe) msg.text = msg.text.substr("/me ".length); // Translate or escape text if (!msg.text) return ""; if (msg.translate) { msg.text = translate(msg.text); if (msg.translateParameters) { let parameters = msg.parameters || {}; translateObjectKeys(parameters, msg.translateParameters); msg.text = sprintf(msg.text, parameters); } } else msg.text = escapeText(msg.text); // GUID for players, playerID for AIs let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player); return sprintf(g_ChatCommands[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"], { "message": msg.text, "context": msg.context || undefined, "user": coloredUsername, "userTag": sprintf(translate("<%(user)s>"), { "user": coloredUsername }) }); } /** * Checks if the current user is an addressee of the chatmessage sent by another player. * Sets the context of that message. * Returns true if the message should be displayed. * * @param {Object} msg */ function parseChatAddressee(msg) { if (msg.text[0] != '/') return true; // Split addressee command and message-text let cmd = msg.text.split(/\s/)[0]; msg.text = msg.text.substr(cmd.length + 1); if (cmd == "/ally") cmd = "/allies"; if (cmd == "/enemy") cmd = "/enemies"; if (cmd == "/observer") cmd = "/observers"; // GUID for players and observers, ID for bots let senderID = (g_PlayerAssignments[msg.guid] || msg).player; let isSender = msg.guid ? msg.guid == Engine.GetPlayerGUID() : senderID == Engine.GetPlayerID(); // Parse private message let isPM = cmd == "/msg"; let addresseeGUID; let addresseeIndex; if (isPM) { addresseeGUID = matchUsername(msg.text); let addressee = g_PlayerAssignments[addresseeGUID]; if (!addressee) { if (isSender) warn("Couldn't match username: " + msg.text); return false; } // Prohibit PM if addressee and sender are identical if (isSender && addresseeGUID == Engine.GetPlayerGUID()) return false; msg.text = msg.text.substr(addressee.name.length + 1); addresseeIndex = addressee.player; } // Set context string if (!g_ChatAddresseeContext[cmd]) { if (isSender) warn("Unknown chat command: " + cmd); return false; } msg.context = g_ChatAddresseeContext[cmd]; // For observers only permit public- and observer-chat and PM to observers if (isPlayerObserver(senderID) && (isPM && !isPlayerObserver(addresseeIndex) || !isPM && cmd != "/observers")) return false; return isSender || g_IsChatAddressee[cmd](senderID, addresseeGUID); } /** * Returns the guid of the user with the longest name that is a prefix of the given string. */ function matchUsername(text) { if (!text) return ""; let match = ""; let playerGUID = ""; for (let guid in g_PlayerAssignments) { let pName = g_PlayerAssignments[guid].name; if (text.indexOf(pName + " ") == 0 && pName.length > match.length) { match = pName; playerGUID = guid; } } return playerGUID; } /** * Custom dialog response handling, usable by trigger maps. */ function sendDialogAnswer(guiObject, dialogName) { Engine.GetGUIObjectByName(dialogName+"-dialog").hidden = true; Engine.PostNetworkCommand({ "type": "dialog-answer", "dialog": dialogName, "answer": guiObject.name.split("-").pop(), }); resumeGame(); } /** * Custom dialog opening, usable by trigger maps. */ function openDialog(dialogName, data, player) { let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog"); if (!dialog) { warn("messages.js: Unknow dialog with name " + dialogName); return; } dialog.hidden = false; for (let objName in data) { let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName); if (!obj) { warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog."); continue; } for (let key in data[objName]) { let n = data[objName][key]; if (typeof n == "object" && n.message) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); obj[key] = sprintf(message, parameters); } else obj[key] = n; } } pauseGame(); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 18203) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 18204) @@ -1,1253 +1,1263 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly || g_IsReplay)); /** * Colors to flash when pop limit reached. */ const g_DefaultPopulationColor = "white"; const g_PopulationAlertColor = "orange"; /** * A random file will be played. TODO: more variety */ const g_Ambient = [ "audio/ambient/dayscape/day_temperate_gen_03.ogg" ]; /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController; /** * True if this is a multiplayer game. */ var g_IsNetworked = 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; /** + * True if the current player has paused the game explicitly. + */ +var g_Paused = false; + +/** + * The list of GUIDs of players who have currently paused the game, if the game is networked. + */ +var g_PausingClients = []; + +/** * 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; /** * Unique ID for lobby reports. */ var g_MatchID; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var lastTickTime = new Date(); /** * Not constant as we add "gaia". */ var g_CivData = {}; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; var g_PlayerAssignments = { "local": { "name": singleplayerName(), "player": 1 } }; /** * Cache dev-mode settings that are frequently or widely used. */ var g_DevSettings = { "changePerspective": false, "controlAll": false }; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Blink the population counter if the player can't train more units. */ var g_IsTrainingBlocked = false; /** * Cache simulation state (updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TemplateDataWithoutLocalization = {}; var g_TechnologyData = {}; /** * Cache concatenated list of player states ("active", "defeated" or "won"). */ var g_CachedLastStates = ""; /** * Whether the current player has lost/won and reached the end of their game. * Used for reporting the gamestate and showing the game-end message only once. */ var g_GameEnded = false; /** * Top coordinate of the research list. * Changes depending on the number of displayed counters. */ var g_ResearchListTop = 4; /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Blink the hero selection if that entity has lost health since the last turn. */ var g_PreviousHeroHitPoints; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["Female", "Trader", "FishingBoat", "CitizenSoldier", "Healer"]; /** * Cache the idle worker status. */ var g_HasIdleWorker = false; function GetSimState() { if (!g_SimState) g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); return g_SimState; } function GetEntityState(entId) { if (!g_EntityStates[entId]) g_EntityStates[entId] = Engine.GuiInterfaceCall("GetEntityState", entId); return g_EntityStates[entId]; } function GetExtendedEntityState(entId) { let entState = GetEntityState(entId); if (!entState || entState.extended) return entState; let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId); for (let prop in extension) entState[prop] = extension[prop]; entState.extended = true; g_EntityStates[entId] = entState; return entState; } function GetTemplateData(templateName) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", templateName); translateObjectKeys(template, ["specific", "generic", "tooltip"]); g_TemplateData[templateName] = template; } return g_TemplateData[templateName]; } function GetTemplateDataWithoutLocalization(templateName) { if (!(templateName in g_TemplateDataWithoutLocalization)) { let template = Engine.GuiInterfaceCall("GetTemplateData", templateName); g_TemplateDataWithoutLocalization[templateName] = template; } return g_TemplateDataWithoutLocalization[templateName]; } function GetTechnologyData(technologyName) { if (!(technologyName in g_TechnologyData)) { let template = Engine.GuiInterfaceCall("GetTechnologyData", technologyName); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); g_TechnologyData[technologyName] = template; } return g_TechnologyData[technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } if (initData) { g_IsNetworked = initData.isNetworked; g_IsController = initData.isController; g_PlayerAssignments = initData.playerAssignments; g_MatchID = initData.attribs.matchID; g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; // Cache the player data // (This may be updated at runtime by handleNetMessage) g_Players = getPlayerData(g_PlayerAssignments); if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked; } else // Needed for autostart loading option { if (g_IsReplay) g_PlayerAssignments.local.player = -1; g_Players = getPlayerData(null); } g_CivData = loadCivData(); g_CivData.gaia = { "Code": "gaia", "Name": translate("Gaia") }; initializeMusic(); // before changing the perspective selectViewPlayer(g_ViewedPlayer); let gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.list = g_GameSpeeds.Title; gameSpeed.list_data = g_GameSpeeds.Speed; let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(Engine.GetSimRate()); gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default; gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); }; initMenuPosition(); // Populate player selection dropdown let playerNames = [translate("Observer")]; let playerIDs = [-1]; for (let player in g_Players) { playerIDs.push(player); playerNames.push(colorizePlayernameHelper("■", player) + " " + g_Players[player].name); } let viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer"); viewPlayerDropdown.list = playerNames; viewPlayerDropdown.list_data = playerIDs; viewPlayerDropdown.selected = Engine.GetPlayerID() + 1; // If in Atlas editor, disable the exit button if (Engine.IsAtlasRunning()) Engine.GetGUIObjectByName("menuExitButton").enabled = false; if (hotloadData) g_Selection.selected = hotloadData.selection; onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); // Report the performance after 5 seconds (when we're still near // the initial camera view) and a minute (when the profiler will // have settled down if framerates as very low), to give some // extremely rough indications of performance // // DISABLED: this information isn't currently useful for anything much, // and it generates a massive amount of data to transmit and store //setTimeout(function() { reportPerformance(5); }, 5000); //setTimeout(function() { reportPerformance(60); }, 60000); } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); playAmbient(); } function toggleChangePerspective(enabled) { g_DevSettings.changePerspective = enabled; selectViewPlayer(g_ViewedPlayer); } /** * Change perspective tool. * Shown to observers or when enabling the developers option. */ function selectViewPlayer(playerID) { if (playerID < -1 || playerID > g_Players.length - 1) return; g_IsObserver = isPlayerObserver(Engine.GetPlayerID()); let changeView = g_IsObserver || g_DevSettings.changePerspective; if (changeView) { g_ViewedPlayer = playerID; if (g_DevSettings.changePerspective) { Engine.SetPlayerID(g_ViewedPlayer); g_IsObserver = isPlayerObserver(g_ViewedPlayer); } } Engine.SetViewedPlayer(g_ViewedPlayer); updateTopPanel(); updateChatAddressees(); // Update GUI and clear player-dependent cache onSimulationUpdate(); let viewPlayer = Engine.GetGUIObjectByName("viewPlayer"); viewPlayer.hidden = !changeView; let alphaLabel = Engine.GetGUIObjectByName("alphaLabel"); alphaLabel.hidden = g_ViewedPlayer > 0 && !viewPlayer.hidden; alphaLabel.size = g_ViewedPlayer > 0 ? "50%+20 0 100%-226 100%" : "200 0 100%-475 100%"; Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver || g_ViewedPlayer < 1; if (g_IsDiplomacyOpen) openDiplomacy(); if (g_IsTradeOpen) openTrade(); } /** * 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) { return Engine.GetPlayerID() == playerID && !g_IsObserver || g_DevSettings.controlAll; } /** * Sets civ icon for the currently viewed player. * Hides most gui objects for observers. */ function updateTopPanel() { let isPlayer = g_ViewedPlayer > 0; if (isPlayer) { let civName = g_CivData[g_Players[g_ViewedPlayer].civ].Name; Engine.GetGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem; Engine.GetGUIObjectByName("civIconOverlay").tooltip = sprintf(translate("%(civ)s - Structure Tree"), { "civ": civName }); } // Hide stuff gaia/observers don't use. Engine.GetGUIObjectByName("food").hidden = !isPlayer; Engine.GetGUIObjectByName("wood").hidden = !isPlayer; Engine.GetGUIObjectByName("stone").hidden = !isPlayer; Engine.GetGUIObjectByName("metal").hidden = !isPlayer; Engine.GetGUIObjectByName("population").hidden = !isPlayer; Engine.GetGUIObjectByName("civIcon").hidden = !isPlayer; Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer; Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer; Engine.GetGUIObjectByName("observerText").hidden = isPlayer; // Disable stuff observers shouldn't use Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked; Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver; Engine.GetGUIObjectByName("summaryButton").enabled = g_IsObserver; } function reportPerformance(time) { let settings = Engine.GetMapSettings(); Engine.SubmitUserReport("profile", 3, JSON.stringify({ "time": time, "map": settings.Name, "seed": settings.Seed, // only defined for random maps "size": settings.Size, // only defined for random maps "profiler": Engine.GetProfilerState() })); } /** * Resign a player. * @param leaveGameAfterResign If player is quitting after resignation. */ function resignGame(leaveGameAfterResign) { if (g_IsObserver || g_Disconnected) return; Engine.PostNetworkCommand({ "type": "defeat-player", "playerId": Engine.GetPlayerID(), "resign": true }); updateTopPanel(); global.music.setState(global.music.states.DEFEAT); if (!leaveGameAfterResign) - resumeGame(); + resumeGame(true); } /** * Leave the game * @param willRejoin If player is going to be rejoining a networked game. */ function leaveGame(willRejoin) { let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); let mapSettings = Engine.GetMapSettings(); let gameResult; if (Engine.GetPlayerID() == -1) { gameResult = translate("You have left the game."); global.music.setState(global.music.states.VICTORY); } else { let playerState = extendedSimState.players[Engine.GetPlayerID()]; if (g_Disconnected) gameResult = translate("You have been disconnected."); else if (playerState.state == "won") gameResult = translate("You have won the battle!"); else if (playerState.state == "defeated") gameResult = translate("You have been defeated..."); else // "active" { global.music.setState(global.music.states.DEFEAT); if (willRejoin) gameResult = translate("You have left the game."); else { gameResult = translate("You have abandoned the game."); resignGame(true); } } } let summary = { "timeElapsed" : extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "players": g_Players, "mapSettings": Engine.GetMapSettings(), }; if (!g_IsReplay) Engine.SaveReplayMetadata(JSON.stringify(summary)); if (!g_HasRejoined) summary.replayDirectory = Engine.GetCurrentReplayDirectory(); summary.replaySelectionData = g_ReplaySelectionData; Engine.EndGame(); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); summary.gameResult = gameResult; summary.isReplay = g_IsReplay; Engine.SwitchGuiPage("page_summary.xml", summary); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected }; } // Return some data that will be stored in saved game files function getSavedGameData() { // TODO: any other gui state? return { "playerAssignments": g_PlayerAssignments, "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 = new Date(); let tickLength = new Date() - lastTickTime; lastTickTime = now; checkPlayerState(); while (true) { let message = Engine.PollNetworkClient(); if (!message) break; handleNetMessage(message); } updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; updateGUIObjects(); // Display rally points for selected buildings if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } updateTimers(); updateMenuPosition(tickLength); // When training is blocked, flash population (alternates color every 500msec) Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && Date.now() % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor; Engine.GuiInterfaceCall("ClearRenamedEntities"); } function checkPlayerState() { if (g_GameEnded || Engine.GetPlayerID() < 1) return; // Send a game report for each player in this game. let m_simState = GetSimState(); let playerState = m_simState.players[Engine.GetPlayerID()]; let tempStates = ""; for (let player of m_simState.players) tempStates += player.state + ","; if (g_CachedLastStates != tempStates) { g_CachedLastStates = tempStates; reportGame(); } if (playerState.state == "active") return; // Make sure nothing is open to avoid stacking. closeOpenDialogs(); // Make sure this doesn't run again. g_GameEnded = true; // Select observermode Engine.GetGUIObjectByName("viewPlayer").selected = playerState.state == "won" ? g_ViewedPlayer + 1 : 0; let btCaptions; let btCode; let message; let title; if (Engine.IsAtlasRunning()) { // If we're in Atlas, we can't leave the game btCaptions = [translate("OK")]; btCode = [null]; message = translate("Press OK to continue"); } else { btCaptions = [translate("No"), translate("Yes")]; btCode = [null, leaveGame]; message = translate("Do you want to quit?"); } if (playerState.state == "defeated") { title = translate("DEFEATED!"); global.music.setState(global.music.states.DEFEAT); } else if (playerState.state == "won") { title = translate("VICTORIOUS!"); global.music.setState(global.music.states.VICTORY); // TODO: Reveal map directly instead of this silly proxy. if (!Engine.GetGUIObjectByName("devCommandsRevealMap").checked) Engine.GetGUIObjectByName("devCommandsRevealMap").checked = true; } messageBox(400, 200, message, title, btCaptions, btCode); } function changeGameSpeed(speed) { if (!g_IsNetworked) Engine.SetSimRate(speed); } function hasIdleWorker() { return Engine.GuiInterfaceCall("HasIdleUnits", { "viewedPlayer": g_ViewedPlayer, "idleClasses": g_WorkerTypes, "excludeUnits": [] }); } function updateIdleWorkerButton() { g_HasIdleWorker = hasIdleWorker(); let idleWorkerButton = Engine.GetGUIObjectByName("idleOverlay"); let prefix = "stretched:session/"; if (!g_HasIdleWorker) idleWorkerButton.sprite = prefix + "minimap-idle-disabled.png"; else if (idleWorkerButton.sprite != prefix + "minimap-idle-highlight.png") idleWorkerButton.sprite = prefix + "minimap-idle.png"; } function onSimulationUpdate() { g_EntityStates = {}; g_TemplateData = {}; g_TechnologyData = {}; g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); if (!g_SimState) return; handleNotifications(); updateGUIObjects(); } function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateHero(); updateGroups(); updateDebug(); updatePlayerDisplay(); updateResearchDisplay(); updateSelectionDetails(); updateBuildingPlacementPreview(); updateTimeNotifications(); updateIdleWorkerButton(); if (g_ViewedPlayer > 0) { let playerState = GetSimState().players[g_ViewedPlayer]; g_DevSettings.controlAll = playerState && playerState.controlsAll; Engine.GetGUIObjectByName("devControlAll").checked = g_DevSettings.controlAll; } if (g_ViewedPlayer != -1 && !g_GameEnded) { // 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 onReplayFinished() { closeOpenDialogs(); pauseGame(); messageBox(400, 200, translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"), translateWithContext("replayFinished", "Confirmation"), [translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")], [resumeGame, leaveGame]); } /** * updates a status bar on the GUI * nameOfBar: name of the bar * points: points to show * maxPoints: max points * direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3; */ function updateGUIStatusBar(nameOfBar, points, maxPoints, direction) { // check, if optional direction parameter is valid. if (!direction || !(direction >= 0 && direction < 4)) direction = 0; // get the bar and update it let statusBar = Engine.GetGUIObjectByName(nameOfBar); if (!statusBar) return; let healthSize = statusBar.size; let value = 100*Math.max(0, Math.min(1, points / maxPoints)); // inverse bar if (direction == 2 || direction == 3) value = 100 - value; if (direction == 0) healthSize.rright = value; else if (direction == 1) healthSize.rbottom = value; else if (direction == 2) healthSize.rleft = value; else if (direction == 3) healthSize.rtop = value; statusBar.size = healthSize; } function updateHero() { let unitHeroPanel = Engine.GetGUIObjectByName("unitHeroPanel"); let heroButton = Engine.GetGUIObjectByName("unitHeroButton"); let playerState = GetSimState().players[g_ViewedPlayer]; if (!playerState || playerState.heroes.length <= 0) { g_PreviousHeroHitPoints = undefined; unitHeroPanel.hidden = true; return; } let heroImage = Engine.GetGUIObjectByName("unitHeroImage"); let heroState = GetExtendedEntityState(playerState.heroes[0]); let template = GetTemplateData(heroState.template); heroImage.sprite = "stretched:session/portraits/" + template.icon; let hero = playerState.heroes[0]; heroButton.onpress = function() { if (!Engine.HotkeyIsPressed("selection.add")) g_Selection.reset(); g_Selection.addList([hero]); }; heroButton.ondoublepress = function() { selectAndMoveTo(getEntityOrHolder(hero)); }; unitHeroPanel.hidden = false; // Setup tooltip let tooltip = "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]"; let healthLabel = "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]"; tooltip += "\n" + sprintf(translate("%(label)s %(current)s / %(max)s"), { "label": healthLabel, "current": Math.ceil(heroState.hitpoints), "max": Math.ceil(heroState.maxHitpoints) }); if (heroState.attack) tooltip += "\n" + getAttackTooltip(heroState); tooltip += "\n" + getArmorTooltip(heroState.armour); if (template.tooltip) tooltip += "\n" + template.tooltip; heroButton.tooltip = tooltip; // update heros health bar updateGUIStatusBar("heroHealthBar", heroState.hitpoints, heroState.maxHitpoints); let heroHP = { "hitpoints": heroState.hitpoints, "player": g_ViewedPlayer }; if (!g_PreviousHeroHitPoints) g_PreviousHeroHitPoints = heroHP; // if the health of the hero changed since the last update, trigger the animation if (g_PreviousHeroHitPoints.player == heroHP.player && g_PreviousHeroHitPoints.hitpoints > heroHP.hitpoints) startColorFade("heroHitOverlay", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit); g_PreviousHeroHitPoints = heroHP; } function updateGroups() { let guiName = "Group"; g_Groups.update(); for (let i = 0; i < 10; ++i) { let button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]"); let label = Engine.GetGUIObjectByName("unit"+guiName+"Label["+i+"]").caption = 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); setPanelObjectPosition(button, i, 1); } } function updateDebug() { let debug = Engine.GetGUIObjectByName("debug"); if (!Engine.GetGUIObjectByName("devDisplayState").checked) { debug.hidden = true; return; } debug.hidden = false; let conciseSimState = deepcopy(GetSimState()); conciseSimState.players = "<<>>"; let text = "simulation: " + uneval(conciseSimState); let selection = g_Selection.toList(); if (selection.length) { let entState = GetExtendedEntityState(selection[0]); if (entState) { let template = GetTemplateData(entState.template); text += "\n\nentity: {\n"; for (let k in entState) text += " "+k+":"+uneval(entState[k])+"\n"; text += "}\n\ntemplate: " + uneval(template); } } debug.caption = text.replace(/\[/g, "\\["); } function updatePlayerDisplay() { let playerState = GetSimState().players[g_ViewedPlayer]; if (!playerState) return; Engine.GetGUIObjectByName("resourceFood").caption = Math.floor(playerState.resourceCounts.food); Engine.GetGUIObjectByName("resourceWood").caption = Math.floor(playerState.resourceCounts.wood); Engine.GetGUIObjectByName("resourceStone").caption = Math.floor(playerState.resourceCounts.stone); Engine.GetGUIObjectByName("resourceMetal").caption = Math.floor(playerState.resourceCounts.metal); Engine.GetGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit; Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" + sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax }); g_IsTrainingBlocked = playerState.trainingBlocked; } function selectAndMoveTo(ent) { let entState = GetEntityState(ent); if (!entState || !entState.position) return; g_Selection.reset(); g_Selection.addList([ent]); let position = entState.position; Engine.CameraMoveTo(position.x, position.z); } function updateResearchDisplay() { let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer); // Set up initial positioning. let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right; for (let i = 0; i < 10; ++i) { let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]"); let size = button.size; size.top = g_ResearchListTop + (4 + buttonSideLength) * i; size.bottom = size.top + buttonSideLength; button.size = size; } let numButtons = 0; for (let tech in researchStarted) { // Show at most 10 in-progress techs. if (numButtons >= 10) break; let template = GetTechnologyData(tech); let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]"); button.hidden = false; button.tooltip = getEntityNames(template); button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher); let icon = "stretched:session/portraits/" + template.icon; Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon; // Scale the progress indicator. let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left)); Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size; ++numButtons; } // Hide unused buttons. for (let i = numButtons; i < 10; ++i) Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true; } /** * Toggles the display of status bars for all of the player's entities. */ function recalculateStatusBarDisplay() { let entities; if (g_ShowAllStatusBars) entities = g_IsObserver ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(Engine.GetPlayerID()); else { let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); // Remove selected entities from the 'all entities' array, to avoid disabling their status bars. entities = Engine.GuiInterfaceCall(g_IsObserver ? "GetNonGaiaEntities" : "GetPlayerEntities").filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars }); } // 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 (let ent in g_Selection.highlighted) highlighted.push(g_Selection.highlighted[ent]); if (g_ShowGuarding) { // flag the guarding entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[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 (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[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; } function playAmbient() { Engine.PlayAmbientSound(g_Ambient[Math.floor(Math.random() * g_Ambient.length)], true); } function getBuildString() { return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), { "buildDate": Engine.GetBuildTimestamp(0), revision: Engine.GetBuildTimestamp(2) }); } function showTimeWarpMessageBox() { messageBox( 500, 250, translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."), translate("Time warp mode") ); } /** * Send a report on the gamestatus to the lobby. */ function reportGame() { // Only 1v1 games are rated (and Gaia is part of g_Players) if (!Engine.HasXmppClient() || !Engine.IsRankedGame() || g_Players.length != 3) return; let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); let unitsClasses = [ "total", "Infantry", "Worker", "Female", "Cavalry", "Champion", "Hero", "Ship", "Trader" ]; let unitsCountersTypes = [ "unitsTrained", "unitsLost", "enemyUnitsKilled" ]; let buildingsClasses = [ "total", "CivCentre", "House", "Economic", "Outpost", "Military", "Fortress", "Wonder" ]; let buildingsCountersTypes = [ "buildingsConstructed", "buildingsLost", "enemyBuildingsDestroyed" ]; let resourcesTypes = [ "wood", "food", "stone", "metal" ]; let resourcesCounterTypes = [ "resourcesGathered", "resourcesUsed", "resourcesSold", "resourcesBought" ]; let playerStatistics = {}; // Unit Stats for (let unitCounterType of unitsCountersTypes) { if (!playerStatistics[unitCounterType]) playerStatistics[unitCounterType] = { }; for (let unitsClass of unitsClasses) playerStatistics[unitCounterType][unitsClass] = ""; } playerStatistics.unitsLostValue = ""; playerStatistics.unitsKilledValue = ""; // Building stats for (let buildingCounterType of buildingsCountersTypes) { if (!playerStatistics[buildingCounterType]) playerStatistics[buildingCounterType] = { }; for (let buildingsClass of buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] = ""; } playerStatistics.buildingsLostValue = ""; playerStatistics.enemyBuildingsDestroyedValue = ""; // Resources for (let resourcesCounterType of resourcesCounterTypes) { if (!playerStatistics[resourcesCounterType]) playerStatistics[resourcesCounterType] = { }; for (let resourcesType of resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] = ""; } playerStatistics.resourcesGathered.vegetarianFood = ""; playerStatistics.tradeIncome = ""; // Tribute playerStatistics.tributesSent = ""; playerStatistics.tributesReceived = ""; // Total playerStatistics.economyScore = ""; playerStatistics.militaryScore = ""; playerStatistics.totalScore = ""; // Various playerStatistics.treasuresCollected = ""; playerStatistics.lootCollected = ""; playerStatistics.feminisation = ""; playerStatistics.percentMapExplored = ""; let mapName = Engine.GetMapSettings().Name; let playerStates = ""; let playerCivs = ""; let teams = ""; let teamsLocked = true; // Serialize the statistics for each player into a comma-separated list. // Ignore gaia for (let i = 1; i < extendedSimState.players.length; ++i) { let player = extendedSimState.players[i]; playerStates += player.state + ","; playerCivs += player.civ + ","; teams += player.team + ","; teamsLocked = teamsLocked && player.teamsLocked; for (let resourcesCounterType of resourcesCounterTypes) for (let resourcesType of resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] += player.statistics[resourcesCounterType][resourcesType] + ","; playerStatistics.resourcesGathered.vegetarianFood += player.statistics.resourcesGathered.vegetarianFood + ","; for (let unitCounterType of unitsCountersTypes) for (let unitsClass of unitsClasses) playerStatistics[unitCounterType][unitsClass] += player.statistics[unitCounterType][unitsClass] + ","; for (let buildingCounterType of buildingsCountersTypes) for (let buildingsClass of buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] += player.statistics[buildingCounterType][buildingsClass] + ","; let total = 0; for (let type in player.statistics.resourcesGathered) total += player.statistics.resourcesGathered[type]; playerStatistics.economyScore += total + ","; playerStatistics.militaryScore += Math.round((player.statistics.enemyUnitsKilledValue + player.statistics.enemyBuildingsDestroyedValue) / 10) + ","; playerStatistics.totalScore += (total + Math.round((player.statistics.enemyUnitsKilledValue + player.statistics.enemyBuildingsDestroyedValue) / 10)) + ","; playerStatistics.tradeIncome += player.statistics.tradeIncome + ","; playerStatistics.tributesSent += player.statistics.tributesSent + ","; playerStatistics.tributesReceived += player.statistics.tributesReceived + ","; playerStatistics.percentMapExplored += player.statistics.percentMapExplored + ","; playerStatistics.treasuresCollected += player.statistics.treasuresCollected + ","; playerStatistics.lootCollected += player.statistics.lootCollected + ","; } // Send the report with serialized data let reportObject = {}; reportObject.timeElapsed = extendedSimState.timeElapsed; reportObject.playerStates = playerStates; reportObject.playerID = Engine.GetPlayerID(); reportObject.matchID = g_MatchID; reportObject.civs = playerCivs; reportObject.teams = teams; reportObject.teamsLocked = String(teamsLocked); reportObject.ceasefireActive = String(extendedSimState.ceasefireActive); reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining); reportObject.mapName = mapName; reportObject.economyScore = playerStatistics.economyScore; reportObject.militaryScore = playerStatistics.militaryScore; reportObject.totalScore = playerStatistics.totalScore; for (let rct of resourcesCounterTypes) { for (let rt of resourcesTypes) reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt]; // eg. rt = food rct.substr = Gathered rct = resourcesGathered } reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood; for (let type of unitsClasses) { // eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsTrained"] = playerStatistics.unitsTrained[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsLost"] = playerStatistics.unitsLost[type]; reportObject["enemy"+type+"UnitsKilled"] = playerStatistics.enemyUnitsKilled[type]; } for (let type of buildingsClasses) { reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsConstructed"] = playerStatistics.buildingsConstructed[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsLost"] = playerStatistics.buildingsLost[type]; reportObject["enemy"+type+"BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type]; } reportObject.tributesSent = playerStatistics.tributesSent; reportObject.tributesReceived = playerStatistics.tributesReceived; reportObject.percentMapExplored = playerStatistics.percentMapExplored; reportObject.treasuresCollected = playerStatistics.treasuresCollected; reportObject.lootCollected = playerStatistics.lootCollected; reportObject.tradeIncome = playerStatistics.tradeIncome; Engine.SendGameReport(reportObject); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 18203) +++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 18204) @@ -1,368 +1,363 @@