Index: ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js (revision 23086) +++ ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js (revision 23087) @@ -1,95 +1,97 @@ /** * This class is only concerned with owning the helper classes and linking them. * The class is not dealing with specific GUI objects and doesn't provide own handlers. */ class Chat { constructor(playerViewControl) { this.ChatWindow = new ChatWindow(); this.ChatOverlay = new ChatOverlay(); this.ChatHistory = new ChatHistory(); this.ChatHistory.registerSelectionChangeHandler(this.ChatWindow.onSelectionChange.bind(this.ChatWindow)); this.ChatInput = new ChatInput(); this.ChatInput.registerChatSubmitHandler(executeNetworkCommand); this.ChatInput.registerChatSubmitHandler(executeCheat); this.ChatInput.registerChatSubmitHandler(this.submitChat.bind(this)); this.ChatInput.registerChatSubmittedHandler(this.closePage.bind(this)); this.ChatAddressees = new ChatAddressees(); this.ChatAddressees.registerSelectionChangeHandler(this.ChatInput.onSelectionChange.bind(this.ChatInput)); this.ChatAddressees.registerSelectionChangeHandler(this.ChatWindow.onSelectionChange.bind(this.ChatWindow)); this.ChatMessageHandler = new ChatMessageHandler(); this.ChatMessageHandler.registerMessageFormatClass(ChatMessageFormatNetwork); this.ChatMessageHandler.registerMessageFormatClass(ChatMessageFormatSimulation); this.ChatMessageFormatPlayer = new ChatMessageFormatPlayer(); this.ChatMessageFormatPlayer.registerAddresseeTypes(this.ChatAddressees.AddresseeTypes); this.ChatMessageHandler.registerMessageFormat("message", this.ChatMessageFormatPlayer); this.ChatMessageHandler.registerMessageHandler(this.ChatOverlay.onChatMessage.bind(this.ChatOverlay)); this.ChatMessageHandler.registerMessageHandler(this.ChatHistory.onChatMessage.bind(this.ChatHistory)); this.ChatMessageHandler.registerMessageHandler(() => { if (this.ChatWindow.isOpen() && this.ChatWindow.isExtended()) this.ChatHistory.displayChatHistory(); }); - registerPlayersFinishedHandler(this.onUpdatePlayers.bind(this)); - playerViewControl.registerViewedPlayerChangeHandler(this.onUpdatePlayers.bind(this)); + let updater = this.onUpdatePlayers.bind(this); + registerPlayersFinishedHandler(updater); + registerPlayerAssignmentsChangeHandler(updater); + playerViewControl.registerViewedPlayerChangeHandler(updater); Engine.SetGlobalHotkey("chat", this.openPage.bind(this)); Engine.SetGlobalHotkey("privatechat", this.openPage.bind(this)); Engine.SetGlobalHotkey("teamchat", () => { this.openPage(g_IsObserver ? "/observers" : "/allies"); }); } /** * Called by the owner whenever g_PlayerAssignments or g_Players changed. */ onUpdatePlayers() { this.ChatAddressees.onUpdatePlayers(); } openPage(command = "") { if (g_Disconnected) return; closeOpenDialogs(); this.ChatAddressees.select(command); this.ChatHistory.displayChatHistory(); this.ChatWindow.openPage(command); } closePage() { this.ChatWindow.closePage(); } getOpenHotkeyTooltip() { return this.ChatInput.getOpenHotkeyTooltip(); } /** * Send the given chat message. */ submitChat(text, command = "") { if (command.startsWith("/msg ")) Engine.SetGlobalHotkey("privatechat", () => { this.openPage(command); }); let msg = command ? command + " " + text : text; if (Engine.HasNetClient()) Engine.SendNetworkChat(msg); else this.ChatMessageHandler.handleMessage({ "type": "message", "guid": "local", "text": msg }); } } Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyGamelistReporter.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyGamelistReporter.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyGamelistReporter.js (revision 23087) @@ -0,0 +1,90 @@ +/** + * Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby. + * This report excludes the matchsettings, since they do not change during the match. + * + * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data, + * but the payload size is minimized by only extracting properties relevant for display. + */ +class LobbyGamelistReporter +{ + constructor() + { + if (!LobbyGamelistReporter.Available()) + throw new Error("Lobby gamelist service not available"); + + let updater = this.sendGamelistUpdate.bind(this); + registerPlayersInitHandler(updater); + registerPlayersFinishedHandler(updater); + registerPlayerAssignmentsChangeHandler(updater); + } + + sendGamelistUpdate() + { + Engine.SendChangeStateGame( + this.countConnectedPlayers(), + playerDataToStringifiedTeamList([...this.getPlayers(), ...this.getObservers()])); + } + + getPlayers() + { + let players = []; + + // Skip gaia + for (let playerID = 1; playerID < g_GameAttributes.settings.PlayerData.length; ++playerID) + { + let pData = g_GameAttributes.settings.PlayerData[playerID]; + + let player = { + "Name": pData.Name, + "Civ": pData.Civ + }; + + if (g_GameAttributes.settings.LockTeams) + player.Team = pData.Team; + + if (pData.AI) + { + player.AI = pData.AI; + player.AIDiff = pData.AIDiff; + player.AIBehavior = pData.AIBehavior; + } + + if (g_Players[playerID].offline) + player.Offline = true; + + // Whether the player has won or was defeated + let state = g_Players[playerID].state; + if (state != "active") + player.State = state; + + players.push(player); + } + return players; + } + + getObservers() + { + let observers = []; + for (let guid in g_PlayerAssignments) + if (g_PlayerAssignments[guid].player == -1) + observers.push({ + "Name": g_PlayerAssignments[guid].name, + "Team": "observer" + }); + return observers; + } + + countConnectedPlayers() + { + let connectedPlayers = 0; + for (let guid in g_PlayerAssignments) + if (g_PlayerAssignments[guid].player != -1) + ++connectedPlayers; + return connectedPlayers; + } +} + +LobbyGamelistReporter.Available = function() +{ + return Engine.HasXmppClient() && g_IsController; +}; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyGamelistReporter.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js (revision 23087) @@ -0,0 +1,23 @@ +/** + * This class reports the buildings built, lost and destroyed of some selected structure classes. + */ +LobbyRatingReport.prototype.Buildings = class +{ + insertValues(report, playerStates) + { + let lower = txt => txt.substr(0, 1).toLowerCase() + txt.substr(1); + let time = playerStates[0].sequences.time.length - 1; + + for (let buildingClass in playerStates[0].sequences.buildingsConstructed) + report[lower(buildingClass) + "BuildingsConstructed"] = playerStates.map(playerState => + playerState.sequences.buildingsConstructed[buildingClass][time]).join(",") + ","; + + for (let buildingClass in playerStates[0].sequences.buildingsLost) + report[lower(buildingClass) + "BuildingsLost"] = playerStates.map(playerState => + playerState.sequences.buildingsLost[buildingClass][time]).join(",") + ","; + + for (let buildingClass in playerStates[0].sequences.enemyBuildingsDestroyed) + report["enemy" + buildingClass + "BuildingsDestroyed"] = playerStates.map(playerState => + playerState.sequences.enemyBuildingsDestroyed[buildingClass][time]).join(",") + ","; + } +}; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Misc.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Misc.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Misc.js (revision 23087) @@ -0,0 +1,21 @@ +/** + * This class counts trade, tributes, loot and map exploration. + */ +LobbyRatingReport.prototype.Misc = class +{ + insertValues(report, playerStates) + { + for (let category of this.MiscCategories) + report[category] = playerStates.map(playerState => + playerState.sequences[category][playerState.sequences.time.length - 1]).join(",") + ","; + } +}; + +LobbyRatingReport.prototype.Misc.prototype.MiscCategories = [ + "tradeIncome", + "tributesSent", + "tributesReceived", + "treasuresCollected", + "lootCollected", + "percentMapExplored" +]; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Misc.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js (revision 23087) @@ -0,0 +1,15 @@ +/** + * This class reports the chosen settings and victory state of the participating players. + */ +LobbyRatingReport.prototype.Players = class +{ + insertValues(report, playerStates) + { + Object.assign(report, { + "playerStates": playerStates.map(playerState => playerState.state).join(",") + ",", + "civs": playerStates.map(playerState => playerState.civ).join(",") + ",", + "teams": playerStates.map(playerState => playerState.team).join(",") + ",", + "teamsLocked": String(playerStates.every(playerState => playerState.teamsLocked)) + }); + } +}; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Players.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Resources.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Resources.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Resources.js (revision 23087) @@ -0,0 +1,25 @@ +/** + * This class reports the amount of resources that each player has obtained or used. + */ +LobbyRatingReport.prototype.Resources = class +{ + insertValues(report, playerStates) + { + let time = playerStates[0].sequences.time.length - 1; + + for (let action of this.Actions) + for (let resCode of g_ResourceData.GetCodes()) + report[resCode + action] = playerStates.map(playerState => + playerState.sequences["resources" + action][resCode][time]).join(",") + ","; + + report.vegetarianFoodGathered = playerStates.map( + playerState => playerState.sequences.resourcesGathered.vegetarianFood[time]).join(",") + ","; + } +}; + +LobbyRatingReport.prototype.Resources.prototype.Actions = [ + "Gathered", + "Used", + "Sold", + "Bought" +]; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Resources.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Score.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Score.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Score.js (revision 23087) @@ -0,0 +1,57 @@ +/** + * This class computes the economic and military score points of each player. + */ +LobbyRatingReport.prototype.Score = class +{ + insertValues(report, playerStates) + { + Object.assign(report, { + "economyScore": playerStates.map(this.economyScore.bind(this)).join(",") + ",", + "militaryScore": playerStates.map(this.militaryScore.bind(this)).join(",") + ",", + "totalScore": playerStates.map(this.totalScore.bind(this)).join(",") + ",", + }); + } + + /** + * Keep this in sync with summary screen score computation! + */ + economyScore(playerState) + { + let total = 0; + let time = playerState.sequences.time.length - 1; + + // Notice that this avoids the vegetarianFood property of resourcesGathered + for (let resCode of g_ResourceData.GetCodes()) + total += playerState.sequences.resourcesGathered[resCode][time]; + + total += playerState.sequences.tradeIncome[time]; + + return Math.round(total / 10); + } + + militaryScore(playerState) + { + let time = playerState.sequences.time.length - 1; + + let totalDestruction = + playerState.sequences.enemyUnitsKilledValue[time] + + playerState.sequences.enemyBuildingsDestroyedValue[time] + + playerState.sequences.unitsCapturedValue[time] + + playerState.sequences.buildingsCapturedValue[time]; + + return Math.round(totalDestruction / 10); + } + + explorationScore(playerState) + { + let time = playerState.sequences.time.length - 1; + return playerState.sequences.percentMapExplored[time] * 10; + } + + totalScore(playerState) + { + return this.economyScore(playerState) + + this.militaryScore(playerState) + + this.explorationScore(playerState); + } +}; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Score.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Units.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Units.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Units.js (revision 23087) @@ -0,0 +1,23 @@ +/** + * This class reports the units trained, lost and killed of some selected unit classes. + */ +LobbyRatingReport.prototype.Units = class +{ + insertValues(report, playerStates) + { + let lower = txt => txt.substr(0, 1).toLowerCase() + txt.substr(1); + let time = playerStates[0].sequences.time.length - 1; + + for (let unitClass in playerStates[0].sequences.unitsTrained) + report[lower(unitClass) + "UnitsTrained"] = playerStates.map(playerState => + playerState.sequences.unitsTrained[unitClass][time]).join(",") + ","; + + for (let unitClass in playerStates[0].sequences.unitsLost) + report[lower(unitClass) + "UnitsLost"] = playerStates.map(playerState => + playerState.sequences.unitsLost[unitClass][time]).join(",") + ","; + + for (let unitClass in playerStates[0].sequences.enemyUnitsKilled) + report["enemy" + unitClass + "UnitsKilled"] = playerStates.map(playerState => + playerState.sequences.enemyUnitsKilled[unitClass][time]).join(",") + ","; + } +}; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Units.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js (revision 23087) @@ -0,0 +1,53 @@ +/** + * This is a container for classes that extend the report object. + * Keep in sync with the lobby bot code, the StatisticsTracker. + */ +class LobbyRatingReport +{ +} + +/** + * This class reports the state of the current game to the lobby bot when the current player has been defeated or won. + */ +class LobbyRatingReporter +{ + constructor() + { + if (!LobbyRatingReporter.Available()) + throw new Error("Lobby rating service is not available"); + + registerPlayersFinishedHandler(this.onPlayersFinished.bind(this)); + } + + onPlayersFinished(players) + { + // Observers don't send the state, players send it only once per match + if (players.indexOf(Engine.GetPlayerID()) != -1) + return; + + let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); + + let report = { + "playerID": Engine.GetPlayerID(), + "matchID": g_GameAttributes.matchID, + "mapName": g_GameAttributes.settings.Name, + "timeElapsed": extendedSimState.timeElapsed, + }; + + // Remove gaia + let playerStates = clone(extendedSimState.players).slice(1); + + for (let name in LobbyRatingReport.prototype) + new LobbyRatingReport.prototype[name]().insertValues(report, playerStates); + + Engine.SendGameReport(report); + } +} + +/** + * Only 1v1 games are rated, account for gaia. + */ +LobbyRatingReporter.Available = function() +{ + return Engine.HasXmppClient() && Engine.IsRankedGame() && g_GameAttributes.settings.PlayerData.length == 3; +}; Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReporter.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyService.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyService.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyService.js (revision 23087) @@ -0,0 +1,18 @@ +/** + * @file The lobby scripting code is kept separate from the rest of the session to + * ease distribution of the game without any lobby code. + */ + +/** + * The host sends a gamelist update everytime a client joins or leaves the match. + */ +var g_LobbyGamelistReporter = + LobbyGamelistReporter.Available() && + new LobbyGamelistReporter(); + +/** + * The participants of a rated 1v1 match send a rating report when the winner was decided. + */ +var g_LobbyRatingReporter = + LobbyRatingReporter.Available() && + new LobbyRatingReporter(); Property changes on: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyService.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 23086) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 23087) @@ -1,699 +1,711 @@ /** * All known cheat commands. */ const g_Cheats = getCheatsData(); /** * All tutorial messages received so far. */ var g_TutorialMessages = []; /** * GUI tags applied to the most recent tutorial message. */ var g_TutorialNewMessageTags = { "color": "yellow" }; /** + * These handlers are called everytime a client joins or disconnects. + */ +var g_PlayerAssignmentsChangeHandlers = new Set(); + +/** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => { handleNetStatusMessage(msg); }, "netwarn": msg => { addNetworkWarning(msg); }, "out-of-sync": msg => { onNetworkOutOfSync(msg); }, "players": msg => { handlePlayerAssignmentsMessage(msg); }, "paused": msg => { g_PauseControl.setClientPauseState(msg.guid, msg.pause); }, "clients-loading": msg => { handleClientsLoadingMessage(msg.guids); }, "rejoined": msg => { addChatMessage({ "type": "rejoined", "guid": msg.guid }); }, "kicked": msg => { addChatMessage({ "type": "kicked", "username": msg.username, "banned": msg.banned }); }, "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 => {} }; /** * 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" + getDisconnectReason(msg.reason, true), "waiting_for_players": msg => translate("Waiting for players to connect:"), "join_syncing": msg => translate("Synchronizing gameplay with other players…"), "active": msg => "" }; var g_PlayerStateMessages = { "won": translate("You have won!"), "defeated": translate("You have been defeated!") }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. * Don't open new pages (message boxes) here! Otherwise further notifications * handled in the same turn can't access the GUI objects anymore. */ 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 = { "type": "message", "text": notification.message, "guid": findGuidForPlayerID(player) || -1, "player": player, "translate": true }; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; colorizePlayernameParameters(notification.parameters); } addChatMessage(message); }, "defeat": function(notification, player) { playersFinished(notification.allies, notification.message, false); }, "won": function(notification, player) { playersFinished(notification.allies, notification.message, true); }, "diplomacy": function(notification, player) { updatePlayerData(); g_DiplomacyColors.onDiplomacyChange(); addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); }, "ceasefire-ended": function(notification, player) { updatePlayerData(); g_DiplomacyColors.OnCeasefireEnded(); }, "tutorial": function(notification, player) { updateTutorial(notification); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "barter": function(notification, player) { addChatMessage({ "type": "barter", "player": player, "amountsSold": notification.amountsSold, "amountsBought": notification.amountsBought, "resourceSold": notification.resourceSold, "resourceBought": notification.resourceBought }); }, "spy-response": function(notification, player) { g_DiplomacyDialog.onSpyResponse(notification, player); if (notification.entity && g_ViewedPlayer == player) { g_DiplomacyDialog.close(); setCameraFollow(notification.entity); } }, "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.notifications.attack") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "phase": function(notification, player) { addChatMessage({ "type": "phase", "player": player, "phaseName": notification.phaseName, "phaseState": notification.phaseState }); }, "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 rallypoint commands of trained animals let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]); if (g_ViewedPlayer != 0 && 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); } else if (cmd.type == "delete-entities" && notification.position) Engine.CameraMoveTo(notification.position.x, notification.position.y); // 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]); if (["walk", "attack-walk", "patrol"].indexOf(cmd.type) != -1) DrawTargetMarker(cmd); // 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"); }, "play-tracks": function(notification, player) { if (notification.lock) { global.music.storeTracks(notification.tracks.map(track => ({ "Type": "custom", "File": track }))); global.music.setState(global.music.states.CUSTOM); } global.music.setLocked(notification.lock); } }; +function registerPlayerAssignmentsChangeHandler(handler) +{ + g_PlayerAssignmentsChangeHandlers.add(handler); +} + /** * Loads all known cheat commands. */ function getCheatsData() { let cheats = {}; for (let fileName of Engine.ListDirectoryFiles("simulation/data/cheats/", "*.json", false)) { let currentCheat = Engine.ReadJSONFile(fileName); if (cheats[currentCheat.Name]) warn("Cheat name '" + currentCheat.Name + "' is already present"); else cheats[currentCheat.Name] = currentCheat.Data; } return deepfreeze(cheats); } /** * Reads userinput from the chat and sends a simulation command in case it is a known cheat. * * @returns {boolean} - True if a cheat was executed. */ function executeCheat(text) { if (!controlsPlayer(Engine.GetPlayerID()) || !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(code => text.indexOf(code) == 0); if (!cheatCode) return false; let cheat = g_Cheats[cheatCode]; let parameter = text.substr(cheatCode.length + 1); if (cheat.isNumeric) parameter = +parameter; if (cheat.DefaultParameter && !parameter) 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() { for (let notification of Engine.GuiInterfaceCall("GetNotifications")) { 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); } } function toggleTutorial() { let tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel"); tutorialPanel.hidden = !tutorialPanel.hidden || !Engine.GetGUIObjectByName("tutorialText").caption; } /** * Updates the tutorial panel when a new goal. */ function updateTutorial(notification) { // Show the tutorial panel if not yet done Engine.GetGUIObjectByName("tutorialPanel").hidden = false; if (notification.warning) { Engine.GetGUIObjectByName("tutorialWarning").caption = coloredText(translate(notification.warning), "orange"); return; } let notificationText = notification.instructions.reduce((instructions, item) => instructions + (typeof item == "string" ? translate(item) : colorizeHotkey(translate(item.text), item.hotkey)), ""); Engine.GetGUIObjectByName("tutorialText").caption = g_TutorialMessages.concat(setStringTags(notificationText, g_TutorialNewMessageTags)).join("\n"); g_TutorialMessages.push(notificationText); if (notification.readyButton) { Engine.GetGUIObjectByName("tutorialReady").hidden = false; if (notification.leave) { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click to quit this tutorial."); Engine.GetGUIObjectByName("tutorialReady").caption = translate("Quit"); Engine.GetGUIObjectByName("tutorialReady").onPress = endGame; } else Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click when ready."); } else { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Follow the instructions."); Engine.GetGUIObjectByName("tutorialReady").hidden = true; } } /** * 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 - GetSimState().timeElapsed); colorizePlayernameParameters(parameters); notificationText += sprintf(message, parameters) + "\n"; } Engine.GetGUIObjectByName("notificationText").caption = notificationText; } /** * Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. */ function handleNetMessages() { while (true) { let msg = Engine.PollNetworkClient(); if (!msg) return; 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; } g_IsNetworkedActive = message.status == "active"; let netStatus = Engine.GetGUIObjectByName("netStatus"); let statusMessage = g_StatusMessageTypes[message.status](message); netStatus.caption = statusMessage; netStatus.hidden = !statusMessage; let loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText"); loadingClientsText.hidden = message.status != "waiting_for_players"; if (message.status == "disconnected") { // Hide the pause overlay, and pause animations. Engine.GetGUIObjectByName("pauseOverlay").hidden = true; Engine.SetPaused(true, false); g_Disconnected = true; updateCinemaPath(); closeOpenDialogs(); } } function handleClientsLoadingMessage(guids) { let loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText"); loadingClientsText.caption = guids.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of client loading messages", ", ")); } function onNetworkOutOfSync(msg) { let txt = [ sprintf(translate("Out-Of-Sync error on turn %(turn)s."), { "turn": msg.turn }), sprintf(translateWithContext("Out-Of-Sync", "Players: %(players)s"), { "players": msg.players.join(translateWithContext("Separator for a list of players", ", ")) }), msg.hash == msg.expectedHash ? translateWithContext("Out-Of-Sync", "Your game state is identical to the hosts game state.") : translateWithContext("Out-Of-Sync", "Your game state differs from the hosts game state."), "" ]; if (msg.turn > 1 && g_GameAttributes.settings.PlayerData.some(pData => pData && pData.AI)) txt.push(translateWithContext("Out-Of-Sync", "Rejoining Multiplayer games with AIs is not supported yet!")); else txt.push( translateWithContext("Out-Of-Sync", "Ensure all players use the same mods."), translateWithContext("Out-Of-Sync", 'Click on "Report a Bug" in the main menu to help fix this.'), sprintf(translateWithContext("Out-Of-Sync", "Replay saved to %(filepath)s"), { "filepath": escapeText(msg.path_replay) }), sprintf(translateWithContext("Out-Of-Sync", "Dumping current state to %(filepath)s"), { "filepath": escapeText(msg.path_oos_dump) }) ); messageBox( 600, 280, txt.join("\n"), translate("Out of Sync") ); } function onReplayOutOfSync(turn, hash, expectedHash) { messageBox( 500, 140, sprintf(translate("Out-Of-Sync error on turn %(turn)s."), { "turn": turn }) + "\n" + // Translation: This is shown if replay is out of sync translateWithContext("Out-Of-Sync", "The current game state is different from the original game state."), translate("Out of Sync") ); } function handlePlayerAssignmentsMessage(message) { for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) onClientLeave(guid); let joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.newAssignments; joins.forEach(guid => { onClientJoin(guid); }); + for (let handler of g_PlayerAssignmentsChangeHandlers) + handler(); + + // TODO: use subscription instead updateGUIObjects(); - g_Chat.onUpdatePlayers(); - sendLobbyPlayerlistUpdate(); } function onClientJoin(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 }); } function onClientLeave(guid) { g_PauseControl.setClientPauseState(guid, false); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; addChatMessage({ "type": "disconnect", "guid": guid }); } function addChatMessage(msg) { g_Chat.ChatMessageHandler.handleMessage(msg); } function clearChatMessages() { g_Chat.ChatOverlay.clearChatMessages(); } /** * 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 ? g_DiplomacyColors.getPlayerColor(playerID) : "white"; return coloredText(username || translate("Unknown Player"), playerColor); } /** * Insert the colorized playername to chat messages sent by the AI and time notifications. */ function colorizePlayernameParameters(parameters) { for (let param in parameters) if (param.startsWith("_player_")) parameters[param] = colorizePlayernameByID(parameters[param]); } /** * 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; } } g_PauseControl.implicitPause(); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23086) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23087) @@ -1,1419 +1,1156 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_CivData = loadCivData(false, true); const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; var g_Chat; var g_DeveloperOverlay; var g_DiplomacyColors; var g_DiplomacyDialog; var g_GameSpeedControl; var g_Menu; var g_MiniMapPanel; var g_ObjectivesDialog; var g_PauseControl; var g_PauseOverlay; var g_PlayerViewControl; var g_TradeDialog; var g_TopPanel; /** * A random file will be played. TODO: more variety */ var g_Ambient = ["audio/ambient/dayscape/day_temperate_gen_03.ogg"]; /** * Map, player and match settings set in gamesetup. */ const g_GameAttributes = 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.HasNetServer(); /** * 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; /** * Shows a message box asking the user to leave if "won" or "defeated". */ var g_ConfirmExit = 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 Singleplayer. */ 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 = []; +var g_HotkeyChangeHandlers = new Set(); /** * 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 = []; /** * Display data of the current players entities shown in the top panel. */ var g_PanelEntities = []; /** * 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]; } function GetTemplateData(templateName) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", templateName); 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])) { let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), 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); } 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_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay); g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates); g_Chat = new Chat(g_PlayerViewControl); g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection); g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors); g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl); g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes); g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl); g_PauseControl = new PauseControl(); g_PauseOverlay = new PauseOverlay(g_PauseControl); g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); g_TradeDialog = new TradeDialog(g_PlayerViewControl); g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl); initSelectionPanels(); LoadModificationTemplates(); updatePlayerData(); initializeMusic(); // before changing the perspective initPanelEntities(); Engine.SetBoundingBoxDebugOverlay(false); updateEnabledRangeOverlayTypes(); 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; } - sendLobbyPlayerlistUpdate(); - // 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.push(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, "teamsLocked": playerState.teamsLocked, "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; } /** * Called when the user changed the diplomacy colors in the options. * TODO: Remove this proxy and make the options page agnostic of the session page. */ function updateDisplayedPlayerColors() { g_DiplomacyColors.updateDisplayedPlayerColors(); } function initPanelEntities() { Engine.GetGUIObjectByName("panelEntityPanel").children.forEach((button, slot) => { button.onPress = function() { let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot); if (!panelEnt) return; if (!Engine.HotkeyIsPressed("selection.add")) g_Selection.reset(); g_Selection.addList([panelEnt.ent]); }; button.onDoublePress = function() { let panelEnt = g_PanelEntities.find(ent => ent.slot !== undefined && ent.slot == slot); if (panelEnt) selectAndMoveTo(getEntityOrHolder(panelEnt.ent)); }; }); } /** * Returns the entity itself except when garrisoned where it returns its garrisonHolder */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length && entState.unitAI.orders[0].type == "Garrison") return getEntityOrHolder(entState.unitAI.orders[0].data.target); 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); playAmbient(); } 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 }); - if (players.indexOf(Engine.GetPlayerID()) != -1) - reportGame(); - - sendLobbyPlayerlistUpdate(); - 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 ); g_ConfirmExit = won ? "won" : "defeated"; } function resumeGame() { g_PauseControl.implicitResume(); } function closeOpenDialogs() { g_Menu.close(); g_Chat.closePage(); g_DiplomacyDialog.close(); g_ObjectivesDialog.close(); g_TradeDialog.close(); } function endGame() { // 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(); Engine.SwitchGuiPage("page_summary.xml", { "sim": simData, "gui": { "dialog": false, "assignedPlayer": playerID, "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }); } // 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.toList().filter(entId => !g_EntityStates[entId])); for (let handler of g_EntitySelectionChangeHandlers) handler(); updateGUIObjects(); // Display rally points for selected buildings if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); updateTimers(); Engine.GuiInterfaceCall("ClearRenamedEntities"); } 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; if (!GetSimState()) return; GetMultipleEntityStates(g_Selection.toList()); for (let handler of g_SimulationUpdateHandlers) handler(); // TODO: Move to handlers updateCinemaPath(); handleNotifications(); updateGUIObjects(); if (g_ConfirmExit) confirmExit(); } /** * Don't show the message box before all playerstate changes are processed. */ function confirmExit() { if (g_IsNetworked && !g_IsNetworkedActive) return; closeOpenDialogs(); g_PauseControl.implicitPause(); // Don't ask for exit if other humans are still playing let askExit = !Engine.HasNetServer() || g_Players.every((player, i) => i == 0 || player.state != "active" || g_GameAttributes.settings.PlayerData[i].AI != ""); let subject = g_PlayerStateMessages[g_ConfirmExit]; if (askExit) subject += "\n" + translate("Do you want to quit?"); messageBox( 400, 200, subject, g_ConfirmExit == "won" ? translate("VICTORIOUS!") : translate("DEFEATED!"), askExit ? [translate("No"), translate("Yes")] : [translate("OK")], askExit ? [resumeGame, endGame] : [resumeGame]); g_ConfirmExit = false; } function toggleGUI() { g_ShowGUI = !g_ShowGUI; updateCinemaPath(); } function updateCinemaPath() { let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath; Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true"); } // TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updatePanelEntities(); displayPanelEntities(); updateGroups(); updateResearchDisplay(); updateSelectionDetails(); updateBuildingPlacementPreview(); updateTimeNotifications(); 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 onReplayFinished() { closeOpenDialogs(); g_PauseControl.implicitPause(); messageBox(400, 200, translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"), translateWithContext("replayFinished", "Confirmation"), [translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")], [resumeGame, endGame]); } /** * 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 updatePanelEntities() { let panelEnts = g_ViewedPlayer == -1 ? GetSimState().players.reduce((ents, pState) => ents.concat(pState.panelEntities), []) : GetSimState().players[g_ViewedPlayer].panelEntities; g_PanelEntities = g_PanelEntities.filter(panelEnt => panelEnts.find(ent => ent == panelEnt.ent)); for (let ent of panelEnts) { let panelEntState = GetEntityState(ent); let template = GetTemplateData(panelEntState.template); let panelEnt = g_PanelEntities.find(pEnt => ent == pEnt.ent); if (!panelEnt) { panelEnt = { "ent": ent, "tooltip": undefined, "sprite": "stretched:session/portraits/" + template.icon, "maxHitpoints": undefined, "currentHitpoints": panelEntState.hitpoints, "previousHitpoints": undefined }; g_PanelEntities.push(panelEnt); } panelEnt.tooltip = createPanelEntityTooltip(panelEntState, template); panelEnt.previousHitpoints = panelEnt.currentHitpoints; panelEnt.currentHitpoints = panelEntState.hitpoints; panelEnt.maxHitpoints = panelEntState.maxHitpoints; } let panelEntIndex = ent => g_PanelEntityOrder.findIndex(entClass => GetEntityState(ent).identity.classes.indexOf(entClass) != -1); g_PanelEntities.sort((panelEntA, panelEntB) => panelEntIndex(panelEntA.ent) - panelEntIndex(panelEntB.ent) ).splice(Engine.GetGUIObjectByName("panelEntityPanel").children.length); } function createPanelEntityTooltip(panelEntState, template) { let getPanelEntNameTooltip = panelEntState => "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]"; return [ getPanelEntNameTooltip, getCurrentHealthTooltip, getAttackTooltip, getArmorTooltip, getEntityTooltip, getAurasTooltip ].map(tooltip => tooltip(panelEntState)).filter(tip => tip).join("\n"); } function displayPanelEntities() { let buttons = Engine.GetGUIObjectByName("panelEntityPanel").children; buttons.forEach((button, slot) => { if (button.hidden || g_PanelEntities.some(ent => ent.slot !== undefined && ent.slot == slot)) return; button.hidden = true; stopColorFade("panelEntityHitOverlay[" + slot + "]"); }); // The slot identifies the button, displayIndex determines its position. for (let displayIndex = 0; displayIndex < Math.min(g_PanelEntities.length, buttons.length); ++displayIndex) { let panelEnt = g_PanelEntities[displayIndex]; // Find the first unused slot if new, otherwise reuse previous. let slot = panelEnt.slot === undefined ? buttons.findIndex(button => button.hidden) : panelEnt.slot; let panelEntButton = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]"); panelEntButton.tooltip = panelEnt.tooltip; updateGUIStatusBar("panelEntityHealthBar[" + slot + "]", panelEnt.currentHitpoints, panelEnt.maxHitpoints); if (panelEnt.slot === undefined) { let panelEntImage = Engine.GetGUIObjectByName("panelEntityImage[" + slot + "]"); panelEntImage.sprite = panelEnt.sprite; panelEntButton.hidden = false; panelEnt.slot = slot; } // If the health of the panelEnt changed since the last update, trigger the animation. if (panelEnt.previousHitpoints > panelEnt.currentHitpoints) startColorFade("panelEntityHitOverlay[" + slot + "]", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit); // TODO: Instead of instant position changes, animate button movement. setPanelObjectPosition(panelEntButton, displayIndex, buttons.length); } } 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; 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); } } 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, g_Players[g_ViewedPlayer].civ); 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; Engine.GetGUIObjectByName("researchStartedTimeRemaining[" + numButtons + "]").caption = Engine.FormatMillisecondsIntoDateStringGMT(researchStarted[tech].timeRemaining, translateWithContext("countdown format", "m:ss")); ++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. * * @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 (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_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); } /** * 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_CreateAndWriteValueToFile("user", configName, String(enabled), "config/user.cfg"); return enabled; } /** * Toggles the display of range overlays of selected entities for the given range type. * @param {string} type - for example "Auras" */ function toggleRangeOverlay(type) { let enabled = toggleConfigBool("gui.session." + type.toLowerCase() + "range"); Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", { "type": type, "enabled": enabled }); let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); Engine.GuiInterfaceCall("SetRangeOverlays", { "entities": selected, "enabled": enabled }); } function updateEnabledRangeOverlayTypes() { for (let type of ["Attack", "Auras", "Heal"]) Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", { "type": type, "enabled": Engine.ConfigDB_GetValue("user", "gui.session." + type.toLowerCase() + "range") == "true" }); } // 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(pickRandom(g_Ambient), true); } /** * Adds the ingame time and ceasefire counter to the global FPS and * realtime counters shown in the top right corner. */ function appendSessionCounters(counters) { let simState = GetSimState(); if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true") { let currentSpeed = Engine.GetSimRate(); if (currentSpeed != 1.0) // Translation: The "x" means "times", with the mathematical meaning of multiplication. counters.push(sprintf(translate("%(time)s (%(speed)sx)"), { "time": timeToString(simState.timeElapsed), "speed": Engine.FormatDecimalNumberIntoString(currentSpeed) })); else counters.push(timeToString(simState.timeElapsed)); } if (simState.ceasefireActive && Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true") counters.push(timeToString(simState.ceasefireTimeRemaining)); g_ResearchListTop = 4 + 14 * counters.length; } - -/** - * Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby. - * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data. - */ -function sendLobbyPlayerlistUpdate() -{ - if (!g_IsController || !Engine.HasXmppClient()) - return; - - // Extract the relevant player data and minimize packet load - let minPlayerData = []; - for (let playerID in g_GameAttributes.settings.PlayerData) - { - if (+playerID == 0) - continue; - - let pData = g_GameAttributes.settings.PlayerData[playerID]; - - let minPData = { "Name": pData.Name, "Civ": pData.Civ }; - - if (g_GameAttributes.settings.LockTeams) - minPData.Team = pData.Team; - - if (pData.AI) - { - minPData.AI = pData.AI; - minPData.AIDiff = pData.AIDiff; - minPData.AIBehavior = pData.AIBehavior; - } - - if (g_Players[playerID].offline) - minPData.Offline = true; - - // Whether the player has won or was defeated - let state = g_Players[playerID].state; - if (state != "active") - minPData.State = state; - - minPlayerData.push(minPData); - } - - // Add observers - let connectedPlayers = 0; - for (let guid in g_PlayerAssignments) - { - let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player]; - - if (pData) - ++connectedPlayers; - else - minPlayerData.push({ - "Name": g_PlayerAssignments[guid].name, - "Team": "observer" - }); - } - - Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData)); -} - -/** - * Send a report on the gamestatus to the lobby. - * Keep in sync with source/tools/XpartaMuPP/LobbyRanking.py - */ -function reportGame() -{ - // Only 1v1 games are rated (and Gaia is part of g_Players) - if (!Engine.HasXmppClient() || !Engine.IsRankedGame() || - g_Players.length != 3 || Engine.GetPlayerID() == -1) - return; - - let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); - - let unitsClasses = [ - "total", - "Infantry", - "Worker", - "FemaleCitizen", - "Cavalry", - "Champion", - "Hero", - "Siege", - "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 misc = [ - "tradeIncome", - "tributesSent", - "tributesReceived", - "treasuresCollected", - "lootCollected", - "percentMapExplored" - ]; - - 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 = ""; - - for (let type of misc) - playerStatistics[type] = ""; - - // Total - playerStatistics.economyScore = ""; - playerStatistics.militaryScore = ""; - playerStatistics.totalScore = ""; - - let mapName = g_GameAttributes.settings.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]; - let maxIndex = player.sequences.time.length - 1; - - 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.sequences[resourcesCounterType][resourcesType][maxIndex] + ","; - playerStatistics.resourcesGathered.vegetarianFood += player.sequences.resourcesGathered.vegetarianFood[maxIndex] + ","; - - for (let unitCounterType of unitsCountersTypes) - for (let unitsClass of unitsClasses) - playerStatistics[unitCounterType][unitsClass] += player.sequences[unitCounterType][unitsClass][maxIndex] + ","; - - for (let buildingCounterType of buildingsCountersTypes) - for (let buildingsClass of buildingsClasses) - playerStatistics[buildingCounterType][buildingsClass] += player.sequences[buildingCounterType][buildingsClass][maxIndex] + ","; - let total = 0; - for (let type in player.sequences.resourcesGathered) - total += player.sequences.resourcesGathered[type][maxIndex]; - - playerStatistics.economyScore += total + ","; - playerStatistics.militaryScore += Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] + - player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10) + ","; - playerStatistics.totalScore += (total + Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] + - player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10)) + ","; - - for (let type of misc) - playerStatistics[type] += player.sequences[type][maxIndex] + ","; - } - - // Send the report with serialized data - let reportObject = {}; - reportObject.timeElapsed = extendedSimState.timeElapsed; - reportObject.playerStates = playerStates; - reportObject.playerID = Engine.GetPlayerID(); - reportObject.matchID = g_GameAttributes.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]; - } - for (let type of misc) - reportObject[type] = playerStatistics[type]; - - 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 23086) +++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 23087) @@ -1,145 +1,147 @@