Index: ps/trunk/binaries/data/mods/public/gui/session/chat_window.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat_window.xml (revision 23061)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat_window.xml (nonexistent)
@@ -1,107 +0,0 @@
-
-
-
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat_window.xml
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js (revision 23062)
@@ -0,0 +1,87 @@
+/**
+ * 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()
+ {
+ 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();
+ });
+
+ 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();
+ }
+
+ /**
+ * 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
+ });
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/Chat.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatAddressees.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatAddressees.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatAddressees.js (revision 23062)
@@ -0,0 +1,127 @@
+/**
+ * This class is concerned with building and propagating the chat addressee selection.
+ */
+class ChatAddressees
+{
+ constructor()
+ {
+ this.selectionChangeHandlers = [];
+
+ this.chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
+ this.chatAddressee.onSelectionChange = this.onSelectionChange.bind(this);
+ }
+
+ registerSelectionChangeHandler(handler)
+ {
+ this.selectionChangeHandlers.push(handler);
+ }
+
+ onSelectionChange()
+ {
+ let selection = this.getSelection();
+ for (let handler of this.selectionChangeHandlers)
+ handler(selection);
+ }
+
+ getSelection()
+ {
+ return this.chatAddressee.list_data[this.chatAddressee.selected] || "";
+ }
+
+ select(command)
+ {
+ this.chatAddressee.selected = this.chatAddressee.list_data.indexOf(command);
+ }
+
+ onUpdatePlayers()
+ {
+ // Remember previously selected item
+ let selectedName = this.getSelection();
+ selectedName = selectedName.startsWith("/msg ") && selectedName.substr("/msg ".length);
+
+ let addressees = this.AddresseeTypes.filter(
+ addresseeType => addresseeType.isSelectable()).map(
+ addresseeType => ({
+ "label": translateWithContext("chat addressee", addresseeType.label),
+ "cmd": addresseeType.command
+ }));
+
+ // Add playernames for private messages
+ let guids = sortGUIDsByPlayerID();
+ for (let guid of guids)
+ {
+ 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
+ });
+ }
+
+ // Select mock item if the selected addressee went offline
+ if (selectedName && guids.every(guid => g_PlayerAssignments[guid].name != selectedName))
+ addressees.push({
+ "cmd": "/msg " + selectedName,
+ "label": sprintf(translate("\\[OFFLINE] %(player)s"), { "player": selectedName })
+ });
+
+ let oldChatAddressee = this.getSelection();
+ this.chatAddressee.list = addressees.map(adressee => adressee.label);
+ this.chatAddressee.list_data = addressees.map(adressee => adressee.cmd);
+ this.chatAddressee.selected = Math.max(0, this.chatAddressee.list_data.indexOf(oldChatAddressee));
+ }
+}
+
+ChatAddressees.prototype.AddresseeTypes = [
+ {
+ "command": "",
+ "isSelectable": () => true,
+ "label": markForTranslationWithContext("chat addressee", "Everyone"),
+ "isAddressee": () => true
+ },
+ {
+ "command": "/allies",
+ "isSelectable": () => !g_IsObserver,
+ "label": markForTranslationWithContext("chat addressee", "Allies"),
+ "context": markForTranslationWithContext("chat message context", "Ally"),
+ "isAddressee":
+ senderID =>
+ g_Players[senderID] &&
+ g_Players[Engine.GetPlayerID()] &&
+ g_Players[senderID].isMutualAlly[Engine.GetPlayerID()],
+ },
+ {
+ "command": "/enemies",
+ "isSelectable": () => !g_IsObserver,
+ "label": markForTranslationWithContext("chat addressee", "Enemies"),
+ "context": markForTranslationWithContext("chat message context", "Enemy"),
+ "isAddressee":
+ senderID =>
+ g_Players[senderID] &&
+ g_Players[Engine.GetPlayerID()] &&
+ g_Players[senderID].isEnemy[Engine.GetPlayerID()],
+ },
+ {
+ "command": "/observers",
+ "isSelectable": () => true,
+ "label": markForTranslationWithContext("chat addressee", "Observers"),
+ "context": markForTranslationWithContext("chat message context", "Observer"),
+ "isAddressee": senderID => g_IsObserver
+ },
+ {
+ "command": "/msg",
+ "isSelectable": () => false,
+ "label": undefined,
+ "context": markForTranslationWithContext("chat message context", "Private"),
+ "isAddressee": (senderID, addresseeGUID) => addresseeGUID == Engine.GetPlayerGUID()
+ }
+];
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatAddressees.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatHistory.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatHistory.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatHistory.js (revision 23062)
@@ -0,0 +1,134 @@
+/**
+ * The objective of this class is to build a message type filter selection and
+ * to store and display the chat history according to that selection.
+ */
+class ChatHistory
+{
+ constructor()
+ {
+ /**
+ * All unparsed chat messages received since connect, including timestamp.
+ */
+ this.chatMessages = [];
+
+ this.selectionChangeHandlers = [];
+
+ this.chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
+ let filters = prepareForDropdown(this.Filters.filter(chatFilter => !chatFilter.hidden));
+ this.chatHistoryFilter.list = filters.text.map(text => translateWithContext("chat history filter", text));
+ this.chatHistoryFilter.list_data = filters.key;
+ this.chatHistoryFilter.selected = 0;
+ this.chatHistoryFilter.onSelectionChange = this.onSelectionChange.bind(this);
+
+ this.chatHistoryText = Engine.GetGUIObjectByName("chatHistoryText");
+ }
+
+ registerSelectionChangeHandler(handler)
+ {
+ this.selectionChangeHandlers.push(handler);
+ }
+
+ /**
+ * Called each time the history filter changes.
+ */
+ onSelectionChange()
+ {
+ this.displayChatHistory();
+
+ for (let handler of this.selectionChangeHandlers)
+ handler();
+ }
+
+ displayChatHistory()
+ {
+ let selected = this.chatHistoryFilter.list_data[this.chatHistoryFilter.selected];
+
+ this.chatHistoryText.caption =
+ this.chatMessages.filter(msg => msg.filter[selected]).map(msg =>
+ Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ?
+ sprintf(translate("%(time)s %(message)s"), {
+ "time": msg.timePrefix,
+ "message": msg.txt
+ }) :
+ msg.txt).join("\n");
+ }
+
+ onChatMessage(msg, formatted)
+ {
+ // Save to chat history
+ let historical = {
+ "txt": formatted,
+ "timePrefix": sprintf(translate("\\[%(time)s]"), {
+ "time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm"))
+ }),
+ "filter": {}
+ };
+
+ // Apply the filters now before diplomacies or playerstates change
+ let senderID = msg.guid && g_PlayerAssignments[msg.guid] ? g_PlayerAssignments[msg.guid].player : 0;
+ for (let filter of this.Filters)
+ historical.filter[filter.key] = filter.filter(msg, senderID);
+
+ this.chatMessages.push(historical);
+ }
+}
+
+/**
+ * Notice only messages will be filtered that are visible to the player in the first place.
+ */
+ChatHistory.prototype.Filters = [
+ {
+ "key": "all",
+ "text": markForTranslationWithContext("chat history filter", "Chat and notifications"),
+ "filter": (msg, senderID) => true
+ },
+ {
+ "key": "chat",
+ "text": markForTranslationWithContext("chat history filter", "Chat messages"),
+ "filter": (msg, senderID) => msg.type == "message"
+ },
+ {
+ "key": "player",
+ "text": markForTranslationWithContext("chat history filter", "Players chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ senderID > 0 && !isPlayerObserver(senderID)
+ },
+ {
+ "key": "ally",
+ "text": markForTranslationWithContext("chat history filter", "Ally chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ msg.cmd && msg.cmd == "/allies"
+ },
+ {
+ "key": "enemy",
+ "text": markForTranslationWithContext("chat history filter", "Enemy chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ msg.cmd && msg.cmd == "/enemies"
+ },
+ {
+ "key": "observer",
+ "text": markForTranslationWithContext("chat history filter", "Observer chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ msg.cmd && msg.cmd == "/observers"
+ },
+ {
+ "key": "private",
+ "text": markForTranslationWithContext("chat history filter", "Private chat"),
+ "filter": (msg, senderID) => !!msg.isVisiblePM
+ },
+ {
+ "key": "gamenotifications",
+ "text": markForTranslationWithContext("chat history filter", "Game notifications"),
+ "filter": (msg, senderID) => msg.type != "message" && msg.guid === undefined
+ },
+ {
+ "key": "chatnotifications",
+ "text": markForTranslationWithContext("chat history filter", "Network notifications"),
+ "filter": (msg, senderID) => msg.type != "message" && msg.guid !== undefined,
+ "hidden": !Engine.HasNetClient()
+ }
+];
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatHistory.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatInput.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatInput.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatInput.js (revision 23062)
@@ -0,0 +1,79 @@
+/**
+ * This class is concerned with setting up the text input field and the send button.
+ */
+class ChatInput
+{
+ constructor()
+ {
+ this.selectedCommand = "";
+ this.chatSubmitHandlers = [];
+ this.chatSubmittedHandlers = [];
+
+ this.chatInput = Engine.GetGUIObjectByName("chatInput");
+ this.chatInput.onPress = this.submitChatInput.bind(this);
+ this.chatInput.onTab = this.autoComplete.bind(this);
+ this.chatInput.tooltip = this.getHotkeyTooltip();
+
+ this.sendChat = Engine.GetGUIObjectByName("sendChat");
+ this.sendChat.onPress = this.submitChatInput.bind(this);
+ this.sendChat.tooltip = this.getHotkeyTooltip();
+ }
+
+ getHotkeyTooltip()
+ {
+ return translateWithContext("chat input", "Type the message to send.") + "\n" +
+ colorizeAutocompleteHotkey() +
+ colorizeHotkey("\n" + translate("Press %(hotkey)s to open the public chat."), "chat") +
+ colorizeHotkey(
+ "\n" + (g_IsObserver ?
+ translate("Press %(hotkey)s to open the observer chat.") :
+ translate("Press %(hotkey)s to open the ally chat.")),
+ "teamchat") +
+ colorizeHotkey("\n" + translate("Press %(hotkey)s to open the previously selected private chat."), "privatechat");
+ }
+
+ /**
+ * The functions registered using this function will be called sequentially
+ * when the user submits chat, until one of them returns true.
+ */
+ registerChatSubmitHandler(handler)
+ {
+ this.chatSubmitHandlers.push(handler);
+ }
+
+ /**
+ * The functions registered using this function will be called after the user submitted chat input.
+ */
+ registerChatSubmittedHandler(handler)
+ {
+ this.chatSubmittedHandlers.push(handler);
+ }
+
+ /**
+ * Called each time the addressee dropdown changes selection.
+ */
+ onSelectionChange(command)
+ {
+ this.selectedCommand = command;
+ }
+
+ autoComplete()
+ {
+ let playernames = [];
+ for (let player in g_PlayerAssignments)
+ playernames.push(g_PlayerAssignments[player].name);
+ autoCompleteText(this.chatInput, playernames);
+ }
+
+ submitChatInput()
+ {
+ let text = this.chatInput.caption;
+ if (!text.length)
+ return;
+
+ this.chatSubmitHandlers.some(handler => handler(text, this.selectedCommand));
+
+ for (let handler of this.chatSubmittedHandlers)
+ handler();
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatInput.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatNetwork.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatNetwork.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatNetwork.js (revision 23062)
@@ -0,0 +1,69 @@
+/**
+ * This class parses network events sent from the NetClient, such as players connecting or disconnecting from the game.
+ */
+class ChatMessageFormatNetwork
+{
+}
+
+ChatMessageFormatNetwork.clientlist = class
+{
+ parse()
+ {
+ return getUsernameList();
+ }
+};
+
+ChatMessageFormatNetwork.connect = class
+{
+ parse(msg)
+ {
+ return sprintf(
+ g_PlayerAssignments[msg.guid].player != -1 ?
+ // Translation: A player that left the game joins again
+ translate("%(player)s is starting to rejoin the game.") :
+ // Translation: A player joins the game for the first time
+ translate("%(player)s is starting to join the game."),
+ { "player": colorizePlayernameByGUID(msg.guid) });
+ }
+};
+
+ChatMessageFormatNetwork.disconnect = class
+{
+ parse(msg)
+ {
+ return sprintf(translate("%(player)s has left the game."), {
+ "player": colorizePlayernameByGUID(msg.guid)
+ });
+ }
+};
+
+ChatMessageFormatNetwork.kicked = class
+{
+ parse(msg)
+ {
+ return sprintf(
+ msg.banned ?
+ translate("%(username)s has been banned") :
+ translate("%(username)s has been kicked"),
+ {
+ "username": colorizePlayernameHelper(
+ msg.username,
+ g_Players.findIndex(p => p.name == msg.username)
+ )
+ });
+ }
+};
+
+ChatMessageFormatNetwork.rejoined = class
+{
+ parse(msg)
+ {
+ return sprintf(
+ g_PlayerAssignments[msg.guid].player != -1 ?
+ // Translation: A player that left the game joins again
+ translate("%(player)s has rejoined the game.") :
+ // Translation: A player joins the game for the first time
+ translate("%(player)s has joined the game."),
+ { "player": colorizePlayernameByGUID(msg.guid) });
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatNetwork.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js (revision 23062)
@@ -0,0 +1,166 @@
+/**
+ * This class interprets the given message as a chat text sent by a player to a selected addressee.
+ * It supports the /me command, translation and acoustic notification.
+ */
+class ChatMessageFormatPlayer
+{
+ constructor()
+ {
+ this.AddresseeTypes = [];
+ }
+
+ registerAddresseeTypes(types)
+ {
+ this.AddresseeTypes = this.AddresseeTypes.concat(types);
+ }
+
+ parse(msg)
+ {
+ if (!msg.text)
+ return "";
+
+ let isMe = msg.text.startsWith("/me ");
+ if (!isMe && !this.parseMessageAddressee(msg))
+ return "";
+
+ isMe = msg.text.startsWith("/me ");
+ 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);
+
+ let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name;
+ if (userName != g_PlayerAssignments[msg.guid].name &&
+ msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1)
+ soundNotification("nick");
+ }
+
+ // GUID for players, playerID for AIs
+ let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player);
+
+ return sprintf(translate(this.strings[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"]), {
+ "message": msg.text,
+ "context": msg.context ? translateWithContext("chat message context", msg.context) : "",
+ "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 and potentially addresseeGUID of that message.
+ * Returns true if the message should be displayed.
+ */
+ parseMessageAddressee(msg)
+ {
+ if (!msg.text.startsWith('/'))
+ return true;
+
+ // Split addressee command and message-text
+ msg.cmd = msg.text.split(/\s/)[0];
+ msg.text = msg.text.substr(msg.cmd.length + 1);
+
+ // GUID is "local" in singleplayer, some string in multiplayer.
+ // Chat messages sent by the simulation (AI) come with the playerID.
+ let senderID = msg.player ? msg.player : (g_PlayerAssignments[msg.guid] || msg).player;
+
+ let isSender = msg.guid ?
+ msg.guid == Engine.GetPlayerGUID() :
+ senderID == Engine.GetPlayerID();
+
+ // Parse private message
+ let isPM = msg.cmd == "/msg";
+ let addresseeGUID;
+ let addresseeIndex;
+ if (isPM)
+ {
+ addresseeGUID = this.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
+ let addresseeType = this.AddresseeTypes.find(type => type.command == msg.cmd);
+ if (!addresseeType)
+ {
+ if (isSender)
+ warn("Unknown chat command: " + msg.cmd);
+ return false;
+ }
+ msg.context = addresseeType.context;
+
+ // For observers only permit public- and observer-chat and PM to observers
+ if (isPlayerObserver(senderID) &&
+ (isPM && !isPlayerObserver(addresseeIndex) || !isPM && msg.cmd != "/observers"))
+ return false;
+
+ let visible = isSender || addresseeType.isAddressee(senderID, addresseeGUID);
+ msg.isVisiblePM = isPM && visible;
+
+ return visible;
+ }
+
+ /**
+ * Returns the guid of the user with the longest name that is a prefix of the given string.
+ */
+ 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;
+ }
+}
+
+/**
+ * Chatmessage shown after commands like /me or /enemies.
+ */
+ChatMessageFormatPlayer.prototype.strings = {
+ "regular": {
+ "context": markForTranslation("(%(context)s) %(userTag)s %(message)s"),
+ "no-context": markForTranslation("%(userTag)s %(message)s")
+ },
+ "me": {
+ "context": markForTranslation("(%(context)s) * %(user)s %(message)s"),
+ "no-context": markForTranslation("* %(user)s %(message)s")
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js (revision 23062)
@@ -0,0 +1,157 @@
+/**
+ * These classes construct a chat message from simulation events initiated from the GuiInterface PushNotification method.
+ */
+class ChatMessageFormatSimulation
+{
+}
+
+ChatMessageFormatSimulation.attack = class
+{
+ parse(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)
+ });
+ }
+};
+
+ChatMessageFormatSimulation.barter = class
+{
+ parse(msg)
+ {
+ if (!g_IsObserver || Engine.ConfigDB_GetValue("user", "gui.session.notifications.barter") != "true")
+ return "";
+
+ let amountsSold = {};
+ amountsSold[msg.resourceSold] = msg.amountsSold;
+
+ let amountsBought = {};
+ amountsBought[msg.resourceBought] = msg.amountsBought;
+
+ return sprintf(translate("%(player)s bartered %(amountsBought)s for %(amountsSold)s."), {
+ "player": colorizePlayernameByID(msg.player),
+ "amountsBought": getLocalizedResourceAmounts(amountsBought),
+ "amountsSold": getLocalizedResourceAmounts(amountsSold)
+ });
+ }
+};
+
+ChatMessageFormatSimulation.diplomacy = class
+{
+ parse(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(translate(this.strings[messageType][msg.status]), {
+ "player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer),
+ "player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer)
+ });
+ }
+};
+
+ChatMessageFormatSimulation.diplomacy.prototype.strings = {
+ "active": {
+ "ally": markForTranslation("You are now allied with %(player)s."),
+ "enemy": markForTranslation("You are now at war with %(player)s."),
+ "neutral": markForTranslation("You are now neutral with %(player)s.")
+ },
+ "passive": {
+ "ally": markForTranslation("%(player)s is now allied with you."),
+ "enemy": markForTranslation("%(player)s is now at war with you."),
+ "neutral": markForTranslation("%(player)s is now neutral with you.")
+ },
+ "observer": {
+ "ally": markForTranslation("%(player)s is now allied with %(player2)s."),
+ "enemy": markForTranslation("%(player)s is now at war with %(player2)s."),
+ "neutral": markForTranslation("%(player)s is now neutral with %(player2)s.")
+ }
+};
+
+ChatMessageFormatSimulation.phase = class
+{
+ parse(msg)
+ {
+ let notifyPhase = Engine.ConfigDB_GetValue("user", "gui.session.notifications.phase");
+ if (notifyPhase == "none" || msg.player != g_ViewedPlayer && !g_IsObserver && !g_Players[msg.player].isMutualAlly[g_ViewedPlayer])
+ return "";
+
+ let message = "";
+ if (notifyPhase == "all")
+ {
+ if (msg.phaseState == "started")
+ message = translate("%(player)s is advancing to the %(phaseName)s.");
+ else if (msg.phaseState == "aborted")
+ message = translate("The %(phaseName)s of %(player)s has been aborted.");
+ }
+ if (msg.phaseState == "completed")
+ message = translate("%(player)s has reached the %(phaseName)s.");
+
+ return sprintf(message, {
+ "player": colorizePlayernameByID(msg.player),
+ "phaseName": getEntityNames(GetTechnologyData(msg.phaseName, g_Players[msg.player].civ))
+ });
+ }
+};
+
+ChatMessageFormatSimulation.playerstate = class
+{
+ parse(msg)
+ {
+ if (!msg.message.pluralMessage)
+ return sprintf(translate(msg.message), {
+ "player": colorizePlayernameByID(msg.players[0])
+ });
+
+ let mPlayers = msg.players.map(playerID => colorizePlayernameByID(playerID));
+ let lastPlayer = mPlayers.pop();
+
+ return sprintf(translatePlural(msg.message.message, msg.message.pluralMessage, msg.message.pluralCount), {
+ // Translation: This comma is used for separating first to penultimate elements in an enumeration.
+ "players": mPlayers.join(translate(", ")),
+ "lastPlayer": lastPlayer
+ });
+ }
+};
+
+/**
+ * Optionally show all tributes sent in observer mode and tributes sent between allied players.
+ * Otherwise, only show tributes sent directly to us, and tributes that we send.
+ */
+ChatMessageFormatSimulation.tribute = class
+{
+ parse(msg)
+ {
+ let message = "";
+ if (msg.targetPlayer == Engine.GetPlayerID())
+ message = translate("%(player)s has sent you %(amounts)s.");
+ else if (msg.sourcePlayer == Engine.GetPlayerID())
+ message = translate("You have sent %(player2)s %(amounts)s.");
+ else if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.tribute") == "true" &&
+ (g_IsObserver || g_GameAttributes.settings.LockTeams &&
+ g_Players[msg.sourcePlayer].isMutualAlly[Engine.GetPlayerID()] &&
+ g_Players[msg.targetPlayer].isMutualAlly[Engine.GetPlayerID()]))
+ message = translate("%(player)s has sent %(player2)s %(amounts)s.");
+
+ return sprintf(message, {
+ "player": colorizePlayernameByID(msg.sourcePlayer),
+ "player2": colorizePlayernameByID(msg.targetPlayer),
+ "amounts": getLocalizedResourceAmounts(msg.amounts)
+ });
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageHandler.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageHandler.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageHandler.js (revision 23062)
@@ -0,0 +1,86 @@
+/**
+ * The purpose of this class is to run a given chat message through parsers until
+ * one of them succeeds and then call all callback handlers on the result.
+ */
+class ChatMessageHandler
+{
+ constructor()
+ {
+ /**
+ * Each property is an array of messageformat class instances.
+ * The classes must have a parse function that receives a
+ * msg object and translates into a string.
+ */
+ this.messageFormats = {};
+
+ /**
+ * Functions that are called each time a message was parsed.
+ */
+ this.messageHandlers = [];
+
+ this.registerMessageFormat("system", new ChatMessageHandler.System());
+ }
+
+ /**
+ * @param type - a string denoting the messagetype used by addChatMessage calls.
+ * @param handler - a class instance with a parse function.
+ */
+ registerMessageFormat(type, handler)
+ {
+ if (!this.messageFormats[type])
+ this.messageFormats[type] = [];
+
+ this.messageFormats[type].push(handler);
+ }
+
+ /**
+ * Receives a class where each enumerable owned property is a chat format
+ * class identified by the property name.
+ */
+ registerMessageFormatClass(formatClass)
+ {
+ for (let type in formatClass)
+ this.registerMessageFormat(type, new formatClass[type]());
+ }
+
+ registerMessageHandler(handler)
+ {
+ this.messageHandlers.push(handler);
+ }
+
+ handleMessage(msg)
+ {
+ let formatted = this.parseMessage(msg);
+ if (!formatted)
+ return;
+
+ for (let handler of this.messageHandlers)
+ handler(msg, formatted);
+ }
+
+ parseMessage(msg)
+ {
+ if (!this.messageFormats[msg.type])
+ {
+ error("Unknown chat message type: " + uneval(msg));
+ return undefined;
+ }
+
+ for (let messageFormat of this.messageFormats[msg.type])
+ {
+ let txt = messageFormat.parse(msg);
+ if (txt)
+ return txt;
+ }
+
+ return undefined;
+ }
+}
+
+ChatMessageHandler.System = class
+{
+ parse(msg)
+ {
+ return msg.txt;
+ }
+};
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatMessageHandler.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatOverlay.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatOverlay.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatOverlay.js (revision 23062)
@@ -0,0 +1,69 @@
+/**
+ * This class is concerned with displaying the most recent chat messages on a screen overlay for some seconds.
+ */
+class ChatOverlay
+{
+ constructor()
+ {
+ /**
+ * Maximum number of lines to display simultaneously.
+ */
+ this.chatLines = 20;
+
+ /**
+ * Number of seconds after which chatmessages will disappear.
+ */
+ this.chatTimeout = 30;
+
+ /**
+ * Holds the timer-IDs used for hiding the chat after chatTimeout seconds.
+ */
+ this.chatTimers = [];
+
+ /**
+ * The currently displayed strings, limited by the given timeframe and limit above.
+ */
+ this.chatMessages = [];
+
+ this.chatText = Engine.GetGUIObjectByName("chatText");
+ }
+
+ /**
+ * Displays this message in the chat overlay and sets up the timer to remove it after a while.
+ */
+ onChatMessage(msg, chatMessage)
+ {
+ this.chatMessages.push(chatMessage);
+ this.chatTimers.push(setTimeout(this.removeOldChatMessage.bind(this), this.chatTimeout * 1000));
+
+ if (this.chatMessages.length > this.chatLines)
+ this.removeOldChatMessage();
+ else
+ this.chatText.caption = this.chatMessages.join("\n");
+ }
+
+ /**
+ * Empty all messages currently displayed in the chat overlay.
+ */
+ clearChatMessages()
+ {
+ this.chatMessages = [];
+ this.chatText.caption = "";
+
+ for (let timer of this.chatTimers)
+ clearTimeout(timer);
+
+ this.chatTimers = [];
+ }
+
+ /**
+ * Called when the timer has run out for the oldest chatmessage or when the message limit is reached.
+ */
+ removeOldChatMessage()
+ {
+ clearTimeout(this.chatTimers[0]);
+ this.chatTimers.shift();
+ this.chatMessages.shift();
+ this.chatText.caption = this.chatMessages.join("\n");
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatOverlay.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatWindow.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/ChatWindow.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/ChatWindow.js (revision 23062)
@@ -0,0 +1,94 @@
+/**
+ * This class is concerned with opening, closing the chat page, and
+ * resizing it depending on whether the chat history is shown.
+ */
+class ChatWindow
+{
+ constructor()
+ {
+ this.chatInput = Engine.GetGUIObjectByName("chatInput");
+ this.closeChat = Engine.GetGUIObjectByName("closeChat");
+
+ this.extendedChat = Engine.GetGUIObjectByName("extendedChat");
+ this.chatHistoryText = Engine.GetGUIObjectByName("chatHistoryText");
+ this.chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage");
+
+ this.chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel");
+ this.chatDialogPanelSmallSize = Engine.GetGUIObjectByName("chatDialogPanelSmall").size;
+ this.chatDialogPanelLargeSize = Engine.GetGUIObjectByName("chatDialogPanelLarge").size;
+
+ // Adjust the width so that the chat history is in the golden ratio
+ this.aspectRatio = (1 + Math.sqrt(5)) / 2;
+
+ this.initPage();
+ }
+
+ initPage()
+ {
+ this.closeChat.onPress = this.closePage.bind(this);
+
+ this.extendedChat.onPress = () => {
+ Engine.ConfigDB_CreateAndWriteValueToFile("user", "chat.session.extended", String(this.isExtended()), "config/user.cfg");
+ this.resizeChatWindow();
+ this.chatInput.focus();
+ };
+
+ this.extendedChat.checked = Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true";
+
+ this.resizeChatWindow();
+ }
+
+ /**
+ * Called if the addressee or history filter selection changed.
+ */
+ onSelectionChange()
+ {
+ if (this.isOpen())
+ this.chatInput.focus();
+ }
+
+ isOpen()
+ {
+ return !this.chatDialogPanel.hidden;
+ }
+
+ isExtended()
+ {
+ return this.extendedChat.checked;
+ }
+
+ openPage(command)
+ {
+ this.chatInput.focus();
+ this.chatDialogPanel.hidden = false;
+ }
+
+ closePage()
+ {
+ this.chatInput.caption = "";
+ this.chatInput.blur();
+ this.chatDialogPanel.hidden = true;
+ }
+
+ resizeChatWindow()
+ {
+ // Hide/show the panel
+ this.chatHistoryPage.hidden = !this.isExtended();
+
+ // Resize the window
+ if (this.isExtended())
+ {
+ this.chatDialogPanel.size = this.chatDialogPanelLargeSize;
+
+ let chatHistoryTextSize = this.chatHistoryText.getComputedSize();
+ let width = this.aspectRatio * (chatHistoryTextSize.bottom - chatHistoryTextSize.top);
+
+ let size = this.chatDialogPanel.size;
+ size.left = -width / 2 - this.chatHistoryText.size.left;
+ size.right = width / 2 + this.chatHistoryText.size.left;
+ this.chatDialogPanel.size = size;
+ }
+ else
+ this.chatDialogPanel.size = this.chatDialogPanelSmallSize;
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/ChatWindow.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/chat/chat_window.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/chat/chat_window.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/chat/chat_window.xml (revision 23062)
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Filter:
+
+
+
+ Filter the chat history.
+
+
+
+
+
+
+
+
+
+
+ To:
+
+
+ Select chat addressee.
+
+
+
+
+ Text:
+
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+ History
+
+
+
+
+ Send
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/chat/chat_window.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.js (revision 23061)
+++ ps/trunk/binaries/data/mods/public/gui/session/developer_overlay.js (revision 23062)
@@ -1,270 +1,270 @@
function DeveloperOverlay()
{
this.commandHeight = 20;
this.displayState = false;
this.timeWarp = false;
this.changePerspective = false;
this.controlAll = false;
Engine.GetGUIObjectByName("devCommandsOverlay").onPress = this.toggle;
this.layout();
}
DeveloperOverlay.prototype.getCommands = function() {
return [
{
"label": translate("Control all units"),
"onPress": checked => {
Engine.PostNetworkCommand({
"type": "control-all",
"flag": checked
});
},
"checked": () => this.controlAll,
},
{
"label": translate("Change perspective"),
"onPress": checked => {
this.setChangePerspective(checked);
},
},
{
"label": translate("Display selection state"),
"onPress": checked => {
this.displayState = checked;
},
},
{
"label": translate("Pathfinder overlay"),
"onPress": checked => {
Engine.GuiInterfaceCall("SetPathfinderDebugOverlay", checked);
},
},
{
"label": translate("Obstruction overlay"),
"onPress": checked => {
Engine.GuiInterfaceCall("SetObstructionDebugOverlay", checked);
},
},
{
"label": translate("Unit motion overlay"),
"onPress": checked => {
g_Selection.SetMotionDebugOverlay(checked);
},
},
{
"label": translate("Range overlay"),
"onPress": checked => {
Engine.GuiInterfaceCall("SetRangeDebugOverlay", checked);
},
},
{
"label": translate("Bounding box overlay"),
"onPress": checked => {
Engine.SetBoundingBoxDebugOverlay(checked);
},
},
{
"label": translate("Restrict camera"),
"onPress": checked => {
Engine.GameView_SetConstrainCameraEnabled(checked);
},
"checked": () => Engine.GameView_GetConstrainCameraEnabled(),
},
{
"label": translate("Reveal map"),
"onPress": checked => {
Engine.PostNetworkCommand({
"type": "reveal-map",
"enable": checked
});
},
"checked": () => Engine.GuiInterfaceCall("IsMapRevealed"),
},
{
"label": translate("Enable time warp"),
"onPress": checked => {
this.timeWarp = checked;
if (checked)
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"));
Engine.EnableTimeWarpRecording(checked ? 10 : 0);
},
},
{
"label": translate("Promote selected units"),
"onPress": checked => {
Engine.PostNetworkCommand({
"type": "promote",
"entities": g_Selection.toList()
});
},
},
{
"label": translate("Hierarchical pathfinder overlay"),
"onPress": checked => {
Engine.GuiInterfaceCall("SetPathfinderHierDebugOverlay", checked);
},
},
{
"label": translate("Enable culling"),
"onPress": checked => {
Engine.GameView_SetCullingEnabled(checked);
},
"checked": () => Engine.GameView_GetCullingEnabled(),
},
{
"label": translate("Lock cull camera"),
"onPress": checked => {
Engine.GameView_SetLockCullCameraEnabled(checked);
},
"checked": () => Engine.GameView_GetLockCullCameraEnabled(),
},
{
"label": translate("Display camera frustum"),
"onPress": checked => {
Engine.Renderer_SetDisplayFrustumEnabled(checked);
},
"checked": () => Engine.Renderer_GetDisplayFrustumEnabled(),
},
];
};
DeveloperOverlay.prototype.layout = function()
{
for (let body of Engine.GetGUIObjectByName("dev_commands").children)
body.hidden = true;
let overlayHeight = 0;
let commands = this.getCommands();
for (let i = 0; i < commands.length; ++i)
{
let command = commands[i];
let body = Engine.GetGUIObjectByName("dev_command[" + i + "]");
let bodySize = body.size;
bodySize.top = i * this.commandHeight;
bodySize.bottom = bodySize.top + this.commandHeight;
body.size = bodySize;
body.hidden = false;
let label = Engine.GetGUIObjectByName("dev_command_label[" + i + "]");
label.caption = command.label;
let checkbox = Engine.GetGUIObjectByName("dev_command_checkbox[" + i + "]");
checkbox.onPress = function() {
command.onPress(checkbox.checked);
if (command.checked)
checkbox.checked = command.checked();
};
overlayHeight += this.commandHeight;
}
let overlay = Engine.GetGUIObjectByName("devCommandsOverlay");
let overlaySize = overlay.size;
overlaySize.bottom = overlaySize.top + overlayHeight;
overlay.size = overlaySize;
this.updateValues();
};
DeveloperOverlay.prototype.updateValues = function()
{
let commands = this.getCommands();
for (let i = 0; i < commands.length; ++i)
{
let command = commands[i];
let body = Engine.GetGUIObjectByName("dev_command[" + i + "]");
if (body.hidden)
continue;
let checkbox = Engine.GetGUIObjectByName("dev_command_checkbox[" + i + "]");
if (command.checked)
checkbox.checked = command.checked();
}
};
DeveloperOverlay.prototype.toggle = function()
{
if (!g_GameAttributes.settings.CheatsEnabled && !g_IsReplay)
return;
let overlay = Engine.GetGUIObjectByName("devCommandsOverlay");
overlay.hidden = !overlay.hidden;
let message = overlay.hidden ?
markForTranslation("The Developer Overlay was closed.") :
markForTranslation("The Developer Overlay was opened.");
// Only players can send the simulation chat command
if (Engine.GetPlayerID() == -1)
- submitChatDirectly(message);
+ g_Chat.submitChat(message);
else
Engine.PostNetworkCommand({
"type": "aichat",
"message": message,
"translateMessage": true,
"translateParameters": [],
"parameters": {}
});
};
DeveloperOverlay.prototype.update = function()
{
this.updateValues();
let debug = Engine.GetGUIObjectByName("debugEntityState");
if (!this.displayState)
{
debug.hidden = true;
return;
}
debug.hidden = false;
let conciseSimState = clone(GetSimState());
conciseSimState.players = "<<>>";
let text = "simulation: " + uneval(conciseSimState);
let selection = g_Selection.toList();
if (selection.length)
{
let entState = GetEntityState(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, "\\[");
};
DeveloperOverlay.prototype.isTimeWarpEnabled = function() {
return this.timeWarp;
};
DeveloperOverlay.prototype.isChangePerspective = function() {
return this.changePerspective;
};
DeveloperOverlay.prototype.setChangePerspective = function(value) {
this.changePerspective = value;
selectViewPlayer(g_ViewedPlayer);
};
DeveloperOverlay.prototype.isControlAll = function() {
return this.controlAll;
};
DeveloperOverlay.prototype.setControlAll = function(value) {
this.controlAll = value;
};
Index: ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 23061)
+++ ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 23062)
@@ -1,130 +1,118 @@
closeOpenDialogs();
-
- openChat();
-
-
-
- openChat(g_IsObserver ? "/observers" : "/allies");
-
-
-
- openChat(g_LastChatAddressee);
-
-
toggleGUI();
toggleMenu();
toggleTutorial();
openGameSummary();
openStrucTree("page_civinfo.xml");
openStrucTree("page_structree.xml");
toggleConfigBool("silhouettes");
var newSetting = !Engine.Renderer_GetShowSkyEnabled();
Engine.Renderer_SetShowSkyEnabled(newSetting);
togglePause();
Engine.QuickSave(getSavedGameData());
Engine.QuickLoad();
performCommand(g_Selection.toList().map(ent => GetEntityState(ent)), "delete");
unloadAll();
stopUnits(g_Selection.toList());
backToWork();
updateSelectionDetails();
updateSelectionDetails();
updateSelectionDetails();
updateBarterButtons();
updateSelectionDetails();
updateBarterButtons();
findIdleUnit(g_MilitaryTypes);
findIdleUnit(["!Domestic"]);
clearSelection();
toggleRangeOverlay("Attack");
toggleRangeOverlay("Auras");
toggleRangeOverlay("Heal");
g_ShowAllStatusBars = !g_ShowAllStatusBars;
recalculateStatusBarDisplay();
Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 23061)
+++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 23062)
@@ -1,1267 +1,1176 @@
// Menu / panel border size
var MARGIN = 4;
// Includes the main menu button
const NUM_BUTTONS = 10;
// Regular menu buttons
var 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;
// Number of pixels per millisecond to move
var MENU_SPEED = 1.2;
// Trade menu: step for probability changes
var STEP = 5;
// Shown in the trade dialog.
var g_IdleTraderTextColor = "orange";
/**
* Store civilization code and page (structree or history) opened in civilization info.
*/
var g_CivInfo = {
"civ": "",
"page": "page_structree.xml"
};
/**
* The barter constants should match with the simulation
* Quantity of goods to sell per click.
*/
const g_BarterResourceSellQuantity = 100;
/**
* Multiplier to be applied when holding the massbarter hotkey.
*/
const g_BarterMultiplier = 5;
/**
* Barter actions, as mapped to the names of GUI Buttons.
*/
const g_BarterActions = ["Buy", "Sell"];
/**
* Currently selected resource type to sell in the barter GUI.
*/
var g_BarterSell;
var g_IsMenuOpen = false;
var g_IsDiplomacyOpen = false;
var g_IsTradeOpen = false;
var g_IsObjectivesOpen = false;
/**
* Used to disable a specific bribe button for the time we are waiting for the result of the bribe after it was clicked.
* It contains an array per viewedPlayer. This array is a list of the players that were bribed.
*/
var g_BribeButtonsWaiting = {};
/**
* Remember last viewed summary panel and charts.
*/
var g_SummarySelectedData;
// 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() {};
function initMenu()
{
Engine.GetGUIObjectByName("menu").size = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM;
// TODO: Atlas should pass g_GameAttributes.settings
for (let button of ["menuExitButton", "summaryButton", "objectivesButton", "diplomacyButton"])
Engine.GetGUIObjectByName(button).enabled = !Engine.IsAtlasRunning();
}
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 lobbyDialogButton()
{
if (!Engine.HasXmppClient())
return;
closeOpenDialogs();
Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
}
function chatMenuButton()
{
- closeOpenDialogs();
- openChat();
+ g_Chat.openPage();
}
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_loadgame.xml",
{ "savedGameData": getSavedGameData() },
resumeGame);
}
function openOptions()
{
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage(
"page_options.xml",
{},
callbackFunctionNames => {
for (let functionName of callbackFunctionNames)
if (global[functionName])
global[functionName]();
resumeGame();
});
}
-function openChat(command = "")
-{
- if (g_Disconnected)
- return;
-
- closeOpenDialogs();
-
- let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
- chatAddressee.selected = chatAddressee.list_data.indexOf(command);
-
- Engine.GetGUIObjectByName("chatInput").focus();
- Engine.GetGUIObjectByName("chatDialogPanel").hidden = false;
-
- updateChatHistory();
-}
-
-function closeChat()
-{
- Engine.GetGUIObjectByName("chatInput").caption = "";
- Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus
- Engine.GetGUIObjectByName("chatDialogPanel").hidden = true;
-}
-
function resizeDiplomacyDialog()
{
let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel");
let size = dialog.size;
let resTribCodesLength = g_ResourceData.GetTributableCodes().length;
if (resTribCodesLength)
{
let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size;
let widthOffset = resTribCodesLength * (tribSize.right - tribSize.left) / 2;
size.left -= widthOffset;
size.right += widthOffset;
}
else
Engine.GetGUIObjectByName("diplomacyHeaderTribute").hidden = true;
let firstRow = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
let heightOffset = (g_Players.length - 1) * (firstRow.bottom - firstRow.top) / 2;
size.top -= heightOffset;
size.bottom += heightOffset;
dialog.size = size;
}
-function initChatWindow()
-{
- let filters = prepareForDropdown(g_ChatHistoryFilters.filter(chatFilter => !chatFilter.hidden));
- let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
- chatHistoryFilter.list = filters.text;
- chatHistoryFilter.list_data = filters.key;
- chatHistoryFilter.selected = 0;
-
- Engine.GetGUIObjectByName("extendedChat").checked =
- Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true";
-
- resizeChatWindow();
-}
-
-function resizeChatWindow()
-{
- // Hide/show the panel
- let chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage");
- let extended = Engine.GetGUIObjectByName("extendedChat").checked;
- chatHistoryPage.hidden = !extended;
-
- // Resize the window
- let chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel");
- if (extended)
- {
- chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelLarge").size;
- // Adjust the width so that the chat history is in the golden ratio
- let chatHistory = Engine.GetGUIObjectByName("chatHistory");
- let height = chatHistory.getComputedSize().bottom - chatHistory.getComputedSize().top;
- let width = (1 + Math.sqrt(5)) / 2 * height;
- let size = chatDialogPanel.size;
- size.left = -width / 2 - chatHistory.size.left;
- size.right = width / 2 + chatHistory.size.left;
- chatDialogPanel.size = size;
- }
- else
- chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelSmall").size;
-}
-
-function updateChatHistory()
-{
- if (Engine.GetGUIObjectByName("chatDialogPanel").hidden ||
- !Engine.GetGUIObjectByName("extendedChat").checked)
- return;
-
- let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
- let selected = chatHistoryFilter.list_data[chatHistoryFilter.selected];
-
- Engine.GetGUIObjectByName("chatHistory").caption =
- g_ChatHistory.filter(msg => msg.filter[selected]).map(msg =>
- Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ?
- sprintf(translate("%(time)s %(message)s"), {
- "time": msg.timePrefix,
- "message": msg.txt
- }) :
- msg.txt
- ).join("\n");
-}
-
-function onToggleChatWindowExtended()
-{
- Engine.ConfigDB_CreateAndWriteValueToFile("user", "chat.session.extended", String(Engine.GetGUIObjectByName("extendedChat").checked), "config/user.cfg");
-
- resizeChatWindow();
-
- Engine.GetGUIObjectByName("chatInput").focus();
-}
-
function openDiplomacy()
{
closeOpenDialogs();
if (g_ViewedPlayer < 1)
return;
g_IsDiplomacyOpen = true;
updateDiplomacy(true);
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false;
}
function closeDiplomacy()
{
g_IsDiplomacyOpen = false;
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = true;
}
function toggleDiplomacy()
{
let open = g_IsDiplomacyOpen;
closeOpenDialogs();
if (!open)
openDiplomacy();
}
function updateDiplomacy(opening = false)
{
if (g_ViewedPlayer < 1 || !g_IsDiplomacyOpen)
return;
let simState = GetSimState();
let isCeasefireActive = simState.ceasefireActive;
let hasSharedLos = GetSimState().players[g_ViewedPlayer].hasSharedLos;
// 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);
// Tribute buttons do not need to be updated onTick, and should not because of massTributing
if (opening)
diplomacyFormatTributeButtons(i, myself || playerInactive);
diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]);
diplomacyFormatSpyRequestButton(i, myself || playerInactive || g_Players[i].isMutualAlly[g_ViewedPlayer] && hasSharedLos);
}
let diplomacyCeasefireCounter = Engine.GetGUIObjectByName("diplomacyCeasefireCounter");
diplomacyCeasefireCounter.caption = sprintf(
translateWithContext("ceasefire", "Remaining ceasefire time: %(time)s."),
{ "time": timeToString(simState.ceasefireTimeRemaining) }
);
diplomacyCeasefireCounter.hidden = !isCeasefireActive;
}
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;
row.hidden = false;
row.sprite = "color:" + rgbToGuiColor(g_DisplayedPlayerColors[i], 32);
setOutcomeIcon(g_Players[i].state, "diplomacyPlayerOutcome[" + (i - 1) + "]");
let diplomacyPlayerName = Engine.GetGUIObjectByName("diplomacyPlayerName[" + (i - 1) + "]");
diplomacyPlayerName.caption = colorizePlayernameByID(i);
diplomacyPlayerName.tooltip = translateAISettings(g_GameAttributes.settings.PlayerData[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;
let isCurrentStance = g_Players[g_ViewedPlayer]["is" + stance][i];
button.caption = isCurrentStance ? translate("x") : "";
button.enabled = controlsPlayer(g_ViewedPlayer) && !isCurrentStance;
button.onPress = (function(player, stance) { return function() {
Engine.PostNetworkCommand({
"type": "diplomacy",
"player": i,
"to": stance.toLowerCase()
});
}; })(i, stance);
}
}
function diplomacyFormatTributeButtons(i, hidden)
{
let resTribCodes = g_ResourceData.GetTributableCodes();
let r = 0;
for (let resCode of resTribCodes)
{
let button = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]");
if (!button)
{
warn("Current GUI limits prevent displaying more than " + r + " tribute buttons!");
break;
}
Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]_image").sprite = "stretched:session/icons/resources/" + resCode + ".png";
button.hidden = hidden;
setPanelObjectPosition(button, r, r + 1, 0);
++r;
if (hidden)
continue;
button.enabled = controlsPlayer(g_ViewedPlayer);
button.tooltip = formatTributeTooltip(i, resCode, 100);
button.onPress = (function(i, resCode, 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 isMassTributePressed = Engine.HotkeyIsPressed("session.masstribute");
if (isMassTributePressed)
{
inputState = INPUT_MASSTRIBUTING;
multiplier += multiplier == 1 ? 4 : 5;
}
let amounts = {};
for (let res of resTribCodes)
amounts[res] = 0;
amounts[resCode] = 100 * multiplier;
button.tooltip = formatTributeTooltip(i, resCode, amounts[resCode]);
// 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, resCode, 100);
};
if (!isMassTributePressed)
g_FlushTributing();
};
})(i, resCode, 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, "player": i });
}; })(i);
}
function diplomacyFormatSpyRequestButton(i, hidden)
{
let button = Engine.GetGUIObjectByName("diplomacySpyRequest[" + (i - 1) + "]");
let template = GetTemplateData("special/spy");
button.hidden = hidden || !template || !!GetSimState().players[g_ViewedPlayer].disabledTemplates["special/spy"];
if (button.hidden)
return;
button.enabled = controlsPlayer(g_ViewedPlayer) &&
!(g_BribeButtonsWaiting[g_ViewedPlayer] && g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) != -1);
let modifier = "";
let tooltips = [translate("Bribe a random unit from this player and share its vision during a limited period.")];
if (!button.enabled)
modifier = "color:0 0 0 127:grayscale:";
else
{
if (template.requiredTechnology)
{
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": g_ViewedPlayer
});
if (!technologyEnabled)
{
modifier = "color:0 0 0 127:grayscale:";
button.enabled = false;
tooltips.push(getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[g_ViewedPlayer].civ));
}
}
if (template.cost)
{
let modifiedTemplate = clone(template);
for (let res in template.cost)
modifiedTemplate.cost[res] = Math.floor(GetSimState().players[i].spyCostMultiplier * template.cost[res]);
tooltips.push(getEntityCostTooltip(modifiedTemplate));
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": modifiedTemplate.cost,
"player": g_ViewedPlayer
});
let costRatio = Engine.GetTemplate("special/spy").VisionSharing.FailureCostRatio;
if (costRatio > 0)
{
tooltips.push(translate("A failed bribe will cost you:"));
for (let res in modifiedTemplate.cost)
modifiedTemplate.cost[res] = Math.floor(costRatio * modifiedTemplate.cost[res]);
tooltips.push(getEntityCostTooltip(modifiedTemplate));
}
if (neededResources)
{
if (button.enabled)
modifier = resourcesToAlphaMask(neededResources) + ":";
button.enabled = false;
tooltips.push(getNeededResourcesTooltip(neededResources));
}
}
}
let icon = Engine.GetGUIObjectByName("diplomacySpyRequestImage[" + (i - 1) + "]");
icon.sprite = modifier + "stretched:session/icons/bribes.png";
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (function(i, button) { return function() {
Engine.PostNetworkCommand({ "type": "spy-request", "source": g_ViewedPlayer, "player": i });
if (!g_BribeButtonsWaiting[g_ViewedPlayer])
g_BribeButtonsWaiting[g_ViewedPlayer] = [];
// Don't push i twice
if (g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) == -1)
g_BribeButtonsWaiting[g_ViewedPlayer].push(i);
diplomacyFormatSpyRequestButton(i, false);
}; })(i, button);
}
function resizeTradeDialog()
{
let dialog = Engine.GetGUIObjectByName("tradeDialogPanel");
let size = dialog.size;
let width = size.right - size.left;
let resTradCodesLength = g_ResourceData.GetTradableCodes().length;
Engine.GetGUIObjectByName("tradeDialogPanelTrade").hidden = !resTradCodesLength;
let resBarterCodesLength = g_ResourceData.GetBarterableCodes().length;
Engine.GetGUIObjectByName("tradeDialogPanelBarter").hidden = !resBarterCodesLength;
let tradeSize = Engine.GetGUIObjectByName("tradeResource[0]").size;
let length = Math.max(resTradCodesLength, resBarterCodesLength);
width += length * (tradeSize.right - tradeSize.left);
size.left = -width / 2;
size.right = width / 2;
dialog.size = size;
}
function openTrade()
{
closeOpenDialogs();
if (g_ViewedPlayer < 1)
return;
g_IsTradeOpen = true;
let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer);
let button = {};
let resTradeCodes = g_ResourceData.GetTradableCodes();
let resBarterCodes = g_ResourceData.GetBarterableCodes();
let currTradeSelection = resTradeCodes[0];
let updateTradeButtons = function()
{
for (let res in button)
{
button[res].label.caption = proba[res] + "%";
button[res].sel.hidden = !controlsPlayer(g_ViewedPlayer) || res != currTradeSelection;
button[res].up.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 100 || proba[currTradeSelection] == 0;
button[res].dn.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 0 || proba[currTradeSelection] == 100;
}
};
hideRemaining("tradeResources", resTradeCodes.length);
Engine.GetGUIObjectByName("tradeHelp").hidden = false;
for (let i = 0; i < resBarterCodes.length; ++i)
{
let resBarterCode = resBarterCodes[i];
let barterResource = Engine.GetGUIObjectByName("barterResource[" + i + "]");
if (!barterResource)
{
warn("Current GUI limits prevent displaying more than " + i + " resources in the barter dialog!");
break;
}
barterOpenCommon(resBarterCode, i, "barter");
setPanelObjectPosition(barterResource, i, i + 1);
}
for (let i = 0; i < resTradeCodes.length; ++i)
{
let resTradeCode = resTradeCodes[i];
let tradeResource = Engine.GetGUIObjectByName("tradeResource[" + i + "]");
if (!tradeResource)
{
warn("Current GUI limits prevent displaying more than " + i + " resources in the trading goods selection dialog!");
break;
}
setPanelObjectPosition(tradeResource, i, i + 1);
let icon = Engine.GetGUIObjectByName("tradeResourceIcon[" + i + "]");
icon.sprite = "stretched:session/icons/resources/" + resTradeCode + ".png";
let buttonUp = Engine.GetGUIObjectByName("tradeArrowUp[" + i + "]");
let buttonDn = Engine.GetGUIObjectByName("tradeArrowDn[" + i + "]");
button[resTradeCode] = {
"up": buttonUp,
"dn": buttonDn,
"label": Engine.GetGUIObjectByName("tradeResourceText[" + i + "]"),
"sel": Engine.GetGUIObjectByName("tradeResourceSelection[" + i + "]")
};
proba[resTradeCode] = proba[resTradeCode] || 0;
let buttonResource = Engine.GetGUIObjectByName("tradeResourceButton[" + i + "]");
buttonResource.enabled = controlsPlayer(g_ViewedPlayer);
buttonResource.onPress = (resource => {
return () => {
if (Engine.HotkeyIsPressed("session.fulltradeswap"))
{
for (let res of resTradeCodes)
proba[res] = 0;
proba[resource] = 100;
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
}
currTradeSelection = resource;
updateTradeButtons();
};
})(resTradeCode);
buttonUp.enabled = controlsPlayer(g_ViewedPlayer);
buttonUp.onPress = (resource => {
return () => {
proba[resource] += Math.min(STEP, proba[currTradeSelection]);
proba[currTradeSelection] -= Math.min(STEP, proba[currTradeSelection]);
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
updateTradeButtons();
};
})(resTradeCode);
buttonDn.enabled = controlsPlayer(g_ViewedPlayer);
buttonDn.onPress = (resource => {
return () => {
proba[currTradeSelection] += Math.min(STEP, proba[resource]);
proba[resource] -= Math.min(STEP, proba[resource]);
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
updateTradeButtons();
};
})(resTradeCode);
}
updateTradeButtons();
updateTraderTexts();
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false;
}
function updateTraderTexts()
{
let traderNumber = Engine.GuiInterfaceCall("GetTraderNumber", g_ViewedPlayer);
Engine.GetGUIObjectByName("traderCountText").caption = getIdleLandTradersText(traderNumber) + "\n\n" + getIdleShipTradersText(traderNumber);
}
function initBarterButtons()
{
let resBartCodes = g_ResourceData.GetBarterableCodes();
g_BarterSell = resBartCodes.length ? resBartCodes[0] : undefined;
}
/**
* Code common to both the Barter Panel and the Trade/Barter Dialog, that
* only needs to be run when the panel or dialog is opened by the player.
*
* @param {string} resourceCode
* @param {number} idx - Element index within its set
* @param {string} prefix - Common prefix of the gui elements to be worked upon
*/
function barterOpenCommon(resourceCode, idx, prefix)
{
let barterButton = {};
for (let action of g_BarterActions)
barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]");
let resource = resourceNameWithinSentence(resourceCode);
barterButton.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource });
barterButton.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource });
barterButton.Sell.onPress = function() {
g_BarterSell = resourceCode;
updateSelectionDetails();
updateBarterButtons();
};
}
/**
* Code common to both the Barter Panel and the Trade/Barter Dialog, that
* needs to be run on simulation update and when relevant hotkeys
* (i.e. massbarter) are pressed.
*
* @param {string} resourceCode
* @param {number} idx - Element index within its set
* @param {string} prefix - Common prefix of the gui elements to be worked upon
* @param {number} player
*/
function barterUpdateCommon(resourceCode, idx, prefix, player)
{
let barterButton = {};
let barterIcon = {};
let barterAmount = {};
for (let action of g_BarterActions)
{
barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]");
barterIcon[action] = Engine.GetGUIObjectByName(prefix + action + "Icon[" + idx + "]");
barterAmount[action] = Engine.GetGUIObjectByName(prefix + action + "Amount[" + idx + "]");
}
let selectionIcon = Engine.GetGUIObjectByName(prefix + "SellSelection[" + idx + "]");
let amountToSell = g_BarterResourceSellQuantity;
if (Engine.HotkeyIsPressed("session.massbarter"))
amountToSell *= g_BarterMultiplier;
let isSelected = resourceCode == g_BarterSell;
let grayscale = isSelected ? "color:0 0 0 100:grayscale:" : "";
// Select color of the sell button
let neededRes = {};
neededRes[resourceCode] = amountToSell;
let canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": player
}) ? "color:255 0 0 80:" : "";
// Select color of the buy button
neededRes = {};
neededRes[g_BarterSell] = amountToSell;
let canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": player
}) ? "color:255 0 0 80:" : "";
barterIcon.Sell.sprite = canSellCurrent + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png";
barterIcon.Buy.sprite = canBuyAny + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png";
barterAmount.Sell.caption = "-" + amountToSell;
let prices = GetSimState().players[player].barterPrices;
barterAmount.Buy.caption = "+" + Math.round(prices.sell[g_BarterSell] / prices.buy[resourceCode] * amountToSell);
barterButton.Buy.onPress = function() {
Engine.PostNetworkCommand({
"type": "barter",
"sell": g_BarterSell,
"buy": resourceCode,
"amount": amountToSell
});
};
barterButton.Buy.hidden = isSelected;
barterButton.Buy.enabled = controlsPlayer(player);
barterButton.Sell.hidden = false;
selectionIcon.hidden = !isSelected;
}
function updateBarterButtons()
{
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState)
return;
let canBarter = playerState.canBarter;
Engine.GetGUIObjectByName("barterNoMarketsMessage").hidden = canBarter;
Engine.GetGUIObjectByName("barterResources").hidden = !canBarter;
Engine.GetGUIObjectByName("barterHelp").hidden = !canBarter;
if (canBarter)
g_ResourceData.GetBarterableCodes().forEach((resCode, i) => {
barterUpdateCommon(resCode, i, "barter", g_ViewedPlayer);
});
}
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": coloredText(inactiveString, g_IdleTraderTextColor)
});
}
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": coloredText(inactiveString, g_IdleTraderTextColor)
});
}
function closeTrade()
{
g_IsTradeOpen = false;
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = true;
}
function toggleTrade()
{
let open = g_IsTradeOpen;
closeOpenDialogs();
if (!open)
openTrade();
}
function toggleTutorial()
{
let tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel");
tutorialPanel.hidden = !tutorialPanel.hidden ||
!Engine.GetGUIObjectByName("tutorialText").caption;
}
function updateGameSpeedControl()
{
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
let player = g_Players[Engine.GetPlayerID()];
g_GameSpeeds = getGameSpeedChoices(!player || player.state != "active");
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
let simRate = Engine.GetSimRate();
let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(+simRate.toFixed(2));
if (gameSpeedIdx == -1)
warn("Unknown gamespeed:" + simRate);
gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default;
gameSpeed.onSelectionChange = function() {
changeGameSpeed(+this.list_data[this.selected]);
};
}
function toggleGameSpeed()
{
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.hidden = !gameSpeed.hidden;
}
function toggleObjectives()
{
let open = g_IsObjectivesOpen;
closeOpenDialogs();
if (!open)
openObjectives();
}
function openObjectives()
{
g_IsObjectivesOpen = true;
let player = g_Players[Engine.GetPlayerID()];
let playerState = player && player.state;
let isActive = !playerState || playerState == "active";
Engine.GetGUIObjectByName("gameDescriptionText").caption = getGameDescription();
let objectivesPlayerstate = Engine.GetGUIObjectByName("objectivesPlayerstate");
objectivesPlayerstate.hidden = isActive;
objectivesPlayerstate.caption = g_PlayerStateMessages[playerState] || "";
let gameDescription = Engine.GetGUIObjectByName("gameDescription");
let gameDescriptionSize = gameDescription.size;
gameDescriptionSize.top = Engine.GetGUIObjectByName(
isActive ? "objectivesTitle" : "objectivesPlayerstate").size.bottom;
gameDescription.size = gameDescriptionSize;
Engine.GetGUIObjectByName("objectivesPanel").hidden = false;
}
function closeObjectives()
{
g_IsObjectivesOpen = false;
Engine.GetGUIObjectByName("objectivesPanel").hidden = true;
}
/**
* Allows players to see their own summary.
* If they have shared ally vision researched, they are able to see the summary of there allies too.
*/
function openGameSummary()
{
closeOpenDialogs();
pauseGame();
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
Engine.PushGuiPage(
"page_summary.xml",
{
"sim": {
"mapSettings": g_GameAttributes.settings,
"playerStates": extendedSimState.players.filter((state, player) =>
g_IsObserver || player == 0 || player == g_ViewedPlayer ||
extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]),
"timeElapsed": extendedSimState.timeElapsed
},
"gui": {
"dialog": true,
"isInGame": true
},
"selectedData": g_SummarySelectedData
},
resumeGameAndSaveSummarySelectedData);
}
function openStrucTree(page)
{
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage(
page,
{
"civ": g_CivInfo.civ || g_Players[g_ViewedPlayer].civ
// TODO add info about researched techs and unlocked entities
},
storeCivInfoPage);
}
function storeCivInfoPage(data)
{
if (data.nextPage)
Engine.PushGuiPage(
data.nextPage,
{ "civ": data.civ },
storeCivInfoPage);
else
{
g_CivInfo = data;
resumeGame();
}
}
/**
* 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(pause = true, explicit = false)
{
// The NetServer only supports pausing after all clients finished loading the game.
if (g_IsNetworked && (!explicit || !g_IsNetworkedActive))
return;
if (explicit)
g_Paused = pause;
Engine.SetPaused(g_Paused || pause, !!explicit);
if (g_IsNetworked)
{
setClientPauseState(Engine.GetPlayerGUID(), g_Paused);
return;
}
updatePauseOverlay();
}
function resumeGame(explicit = false)
{
pauseGame(false, explicit);
}
function resumeGameAndSaveSummarySelectedData(data)
{
g_SummarySelectedData = data.summarySelectedData;
resumeGame(data.explicitResume);
}
/**
* Called when the current player toggles a pause button.
*/
function togglePause()
{
if (!Engine.GetGUIObjectByName("pauseButton").enabled)
return;
closeOpenDialogs();
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("pauseButton").caption = g_Paused ? translate("Resume") : translate("Pause");
Engine.GetGUIObjectByName("resumeMessage").hidden = !g_Paused;
Engine.GetGUIObjectByName("pausedByText").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("pausedByText").caption = sprintf(translate("Paused by %(players)s"),
{ "players": g_PausingClients.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of players", ", ")) });
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", {}, resumeGame);
}
function closeOpenDialogs()
{
closeMenu();
- closeChat();
closeDiplomacy();
closeTrade();
closeObjectives();
+
+ g_Chat.closePage();
}
function formatTributeTooltip(playerID, resourceCode, amount)
{
return sprintf(translate("Tribute %(resourceAmount)s %(resourceType)s to %(playerName)s. Shift-click to tribute %(greaterAmount)s."), {
"resourceAmount": amount,
"resourceType": resourceNameWithinSentence(resourceCode),
"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 23061)
+++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 23062)
@@ -1,1317 +1,696 @@
/**
* All known cheat commands.
*/
const g_Cheats = getCheatsData();
/**
- * Number of seconds after which chatmessages will disappear.
- */
-var g_ChatTimeout = 30;
-
-/**
- * Maximum number of lines to display simultaneously.
- */
-var g_ChatLines = 20;
-
-/**
- * The currently displayed strings, limited by the given timeframe and limit above.
- */
-var g_ChatMessages = [];
-
-/**
- * All unparsed chat messages received since connect, including timestamp.
- */
-var g_ChatHistory = [];
-
-/**
- * Holds the timer-IDs used for hiding the chat after g_ChatTimeout seconds.
- */
-var g_ChatTimers = [];
-
-/**
- * Command to send to the previously selected private chat partner.
- */
-var g_LastChatAddressee = "";
-
-/**
* All tutorial messages received so far.
*/
var g_TutorialMessages = [];
/**
* GUI tags applied to the most recent tutorial message.
*/
var g_TutorialNewMessageTags = { "color": "yellow" };
/**
* 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 => {
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 => {}
};
-var g_FormatChatMessage = {
- "system": msg => msg.text,
- "connect": msg =>
- sprintf(
- g_PlayerAssignments[msg.guid].player != -1 ?
- // Translation: A player that left the game joins again
- translate("%(player)s is starting to rejoin the game.") :
- // Translation: A player joins the game for the first time
- translate("%(player)s is starting to join the game."),
- { "player": colorizePlayernameByGUID(msg.guid) }
- ),
- "disconnect": msg =>
- sprintf(translate("%(player)s has left the game."), {
- "player": colorizePlayernameByGUID(msg.guid)
- }),
- "rejoined": msg =>
- sprintf(
- g_PlayerAssignments[msg.guid].player != -1 ?
- // Translation: A player that left the game joins again
- translate("%(player)s has rejoined the game.") :
- // Translation: A player joins the game for the first time
- translate("%(player)s has joined the game."),
- { "player": colorizePlayernameByGUID(msg.guid) }
- ),
- "kicked": msg =>
- sprintf(
- msg.banned ?
- translate("%(username)s has been banned") :
- translate("%(username)s has been kicked"),
- {
- "username": colorizePlayernameHelper(
- msg.username,
- g_Players.findIndex(p => p.name == msg.username)
- )
- }
- ),
- "clientlist": msg => getUsernameList(),
- "message": msg => formatChatCommand(msg),
- "defeat-victory": msg => formatDefeatVictoryMessage(msg.message, msg.players),
- "diplomacy": msg => formatDiplomacyMessage(msg),
- "tribute": msg => formatTributeMessage(msg),
- "barter": msg => formatBarterMessage(msg),
- "attack": msg => formatAttackMessage(msg),
- "phase": msg => formatPhaseMessage(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 => ""
};
-/**
- * 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()
-};
-
-/**
- * Notice only messages will be filtered that are visible to the player in the first place.
- */
-var g_ChatHistoryFilters = [
- {
- "key": "all",
- "text": translateWithContext("chat history filter", "Chat and notifications"),
- "filter": (msg, senderID) => true
- },
- {
- "key": "chat",
- "text": translateWithContext("chat history filter", "Chat messages"),
- "filter": (msg, senderID) => msg.type == "message"
- },
- {
- "key": "player",
- "text": translateWithContext("chat history filter", "Players chat"),
- "filter": (msg, senderID) =>
- msg.type == "message" &&
- senderID > 0 && !isPlayerObserver(senderID)
- },
- {
- "key": "ally",
- "text": translateWithContext("chat history filter", "Ally chat"),
- "filter": (msg, senderID) =>
- msg.type == "message" &&
- msg.cmd && msg.cmd == "/allies"
- },
- {
- "key": "enemy",
- "text": translateWithContext("chat history filter", "Enemy chat"),
- "filter": (msg, senderID) =>
- msg.type == "message" &&
- msg.cmd && msg.cmd == "/enemies"
- },
- {
- "key": "observer",
- "text": translateWithContext("chat history filter", "Observer chat"),
- "filter": (msg, senderID) =>
- msg.type == "message" &&
- msg.cmd && msg.cmd == "/observers"
- },
- {
- "key": "private",
- "text": translateWithContext("chat history filter", "Private chat"),
- "filter": (msg, senderID) => !!msg.isVisiblePM
- },
- {
- "key": "gamenotifications",
- "text": translateWithContext("chat history filter", "Game notifications"),
- "filter": (msg, senderID) => msg.type != "message" && msg.guid === undefined
- },
- {
- "key": "chatnotifications",
- "text": translateWithContext("chat history filter", "Network notifications"),
- "filter": (msg, senderID) => msg.type != "message" && msg.guid !== undefined,
- "hidden": !Engine.HasNetClient()
- }
-];
-
var g_PlayerStateMessages = {
"won": translate("You have won!"),
"defeated": translate("You have been defeated!")
};
/**
- * 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.
* 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();
if (g_DiplomacyColorsToggle)
updateDisplayedPlayerColors();
addChatMessage({
"type": "diplomacy",
"sourcePlayer": player,
"targetPlayer": notification.targetPlayer,
"status": notification.status
});
},
"ceasefire-ended": function(notification, player)
{
updatePlayerData();
if (g_DiplomacyColorsToggle)
updateDisplayedPlayerColors();
},
"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)
{
if (g_BribeButtonsWaiting[player])
g_BribeButtonsWaiting[player] = g_BribeButtonsWaiting[player].filter(p => p != notification.target);
if (notification.entity && g_ViewedPlayer == player)
{
closeDiplomacy();
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)
+ "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);
}
};
/**
* 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);
}
}
/**
* 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 = leaveGame;
}
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);
});
updateGUIObjects();
- updateChatAddressees();
+ 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)
{
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 updateChatAddressees()
-{
- // Remember previously selected item
- let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
- let selectedName = chatAddressee.list_data[chatAddressee.selected] || "";
- selectedName = selectedName.substr(0, 4) == "/msg" && selectedName.substr(5);
-
- 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
- let guids = sortGUIDsByPlayerID();
- for (let guid of guids)
- {
- 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
- });
- }
-
- // Select mock item if the selected addressee went offline
- if (selectedName && guids.every(guid => g_PlayerAssignments[guid].name != selectedName))
- addressees.push({
- "cmd": "/msg " + selectedName,
- "label": sprintf(translate("\\[OFFLINE] %(player)s"), { "player": selectedName })
- });
-
- let oldChatAddressee = chatAddressee.list_data[chatAddressee.selected];
- chatAddressee.list = addressees.map(adressee => adressee.label);
- chatAddressee.list_data = addressees.map(adressee => adressee.cmd);
- chatAddressee.selected = Math.max(0, chatAddressee.list_data.indexOf(oldChatAddressee));
-}
-
-/**
- * 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 text = Engine.GetGUIObjectByName("chatInput").caption;
-
- closeChat();
-
- 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;
-
- let selectedChat = chatAddressee.list_data[chatAddressee.selected];
- if (selectedChat.startsWith("/msg"))
- g_LastChatAddressee = selectedChat;
-
- 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;
-
- // Update chat overlay
- 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");
-
- // Save to chat history
- let historical = {
- "txt": formatted,
- "timePrefix": sprintf(translate("\\[%(time)s]"), {
- "time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), translate("HH:mm"))
- }),
- "filter": {}
- };
-
- // Apply the filters now before diplomacies or playerstates change
- let senderID = msg.guid && g_PlayerAssignments[msg.guid] ? g_PlayerAssignments[msg.guid].player : 0;
- for (let filter of g_ChatHistoryFilters)
- historical.filter[filter.key] = filter.filter(msg, senderID);
-
- g_ChatHistory.push(historical);
- updateChatHistory();
+ g_Chat.ChatMessageHandler.handleMessage(msg);
}
function clearChatMessages()
{
- g_ChatMessages.length = 0;
- Engine.GetGUIObjectByName("chatText").caption = "";
-
- for (let timer of g_ChatTimers)
- clearTimeout(timer);
-
- g_ChatTimers.length = 0;
-}
-
-/**
- * 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");
+ 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 ? rgbToGuiColor(g_DisplayedPlayerColors[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]);
}
-function formatDefeatVictoryMessage(message, players)
-{
- if (!message.pluralMessage)
- return sprintf(translate(message), {
- "player": colorizePlayernameByID(players[0])
- });
-
- let mPlayers = players.map(playerID => colorizePlayernameByID(playerID));
- let lastPlayer = mPlayers.pop();
-
- return sprintf(translatePlural(message.message, message.pluralMessage, message.pluralCount), {
- // Translation: This comma is used for separating first to penultimate elements in an enumeration.
- "players": mPlayers.join(translate(", ")),
- "lastPlayer": lastPlayer
- });
-}
-
-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)
- });
-}
-
-/**
- * Optionally show all tributes sent in observer mode and tributes sent between allied players.
- * Otherwise, only show tributes sent directly to us, and tributes that we send.
- */
-function formatTributeMessage(msg)
-{
- let message = "";
- if (msg.targetPlayer == Engine.GetPlayerID())
- message = translate("%(player)s has sent you %(amounts)s.");
- else if (msg.sourcePlayer == Engine.GetPlayerID())
- message = translate("You have sent %(player2)s %(amounts)s.");
- else if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.tribute") == "true" &&
- (g_IsObserver || g_GameAttributes.settings.LockTeams &&
- g_Players[msg.sourcePlayer].isMutualAlly[Engine.GetPlayerID()] &&
- g_Players[msg.targetPlayer].isMutualAlly[Engine.GetPlayerID()]))
- message = translate("%(player)s has sent %(player2)s %(amounts)s.");
-
- return sprintf(message, {
- "player": colorizePlayernameByID(msg.sourcePlayer),
- "player2": colorizePlayernameByID(msg.targetPlayer),
- "amounts": getLocalizedResourceAmounts(msg.amounts)
- });
-}
-
-function formatBarterMessage(msg)
-{
- if (!g_IsObserver || Engine.ConfigDB_GetValue("user", "gui.session.notifications.barter") != "true")
- return "";
-
- let amountsSold = {};
- amountsSold[msg.resourceSold] = msg.amountsSold;
-
- let amountsBought = {};
- amountsBought[msg.resourceBought] = msg.amountsBought;
-
- return sprintf(translate("%(player)s bartered %(amountsBought)s for %(amountsSold)s."), {
- "player": colorizePlayernameByID(msg.player),
- "amountsBought": getLocalizedResourceAmounts(amountsBought),
- "amountsSold": getLocalizedResourceAmounts(amountsSold)
- });
-}
-
-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 formatPhaseMessage(msg)
-{
- let notifyPhase = Engine.ConfigDB_GetValue("user", "gui.session.notifications.phase");
- if (notifyPhase == "none" || msg.player != g_ViewedPlayer && !g_IsObserver && !g_Players[msg.player].isMutualAlly[g_ViewedPlayer])
- return "";
-
- let message = "";
- if (notifyPhase == "all")
- {
- if (msg.phaseState == "started")
- message = translate("%(player)s is advancing to the %(phaseName)s.");
- else if (msg.phaseState == "aborted")
- message = translate("The %(phaseName)s of %(player)s has been aborted.");
- }
- if (msg.phaseState == "completed")
- message = translate("%(player)s has reached the %(phaseName)s.");
-
- return sprintf(message, {
- "player": colorizePlayernameByID(msg.player),
- "phaseName": getEntityNames(GetTechnologyData(msg.phaseName, g_Players[msg.player].civ))
- });
-}
-
-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);
-
- let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name;
- if (userName != g_PlayerAssignments[msg.guid].name &&
- msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1)
- soundNotification("nick");
- }
-
- // 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 and potentially addresseeGUID 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
- msg.cmd = msg.text.split(/\s/)[0];
- msg.text = msg.text.substr(msg.cmd.length + 1);
-
- // GUID is "local" in singleplayer, some string in multiplayer.
- // Chat messages sent by the simulation (AI) come with the playerID.
- let senderID = msg.player ? msg.player : (g_PlayerAssignments[msg.guid] || msg).player;
-
- let isSender = msg.guid ?
- msg.guid == Engine.GetPlayerGUID() :
- senderID == Engine.GetPlayerID();
-
- // Parse private message
- let isPM = msg.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[msg.cmd])
- {
- if (isSender)
- warn("Unknown chat command: " + msg.cmd);
- return false;
- }
- msg.context = g_ChatAddresseeContext[msg.cmd];
-
- // For observers only permit public- and observer-chat and PM to observers
- if (isPlayerObserver(senderID) &&
- (isPM && !isPlayerObserver(addresseeIndex) || !isPM && msg.cmd != "/observers"))
- return false;
- let visible = isSender || g_IsChatAddressee[msg.cmd](senderID, addresseeGUID);
- msg.isVisiblePM = isPM && visible;
-
- return visible;
-}
-
-/**
- * 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 23061)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23062)
@@ -1,1720 +1,1720 @@
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_GameSpeeds;
/**
* Whether to display diplomacy colors (where players see self/ally/neutral/enemy each in different colors and
* observers see each team in a different color) or regular player colors.
*/
var g_DiplomacyColorsToggle = false;
/**
* The array of displayed player colors (either the diplomacy color or regular color for each player).
*/
var g_DisplayedPlayerColors;
/**
* Colors to flash when pop limit reached.
*/
var g_DefaultPopulationColor = "white";
var g_PopulationAlertColor = "orange";
/**
* Seen in the tooltip of the top panel.
*/
var g_ResourceTitleFont = "sans-bold-16";
/**
* 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;
/**
* 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;
/**
* 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;
var g_DeveloperOverlay;
/**
* 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;
/**
* Blink the population counter if the player can't train more units.
*/
var g_IsTrainingBlocked = 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();
/**
* 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_DeveloperOverlay = new DeveloperOverlay();
+ g_Chat = new Chat();
+
LoadModificationTemplates();
updatePlayerData();
initializeMusic(); // before changing the perspective
initGUIObjects();
if (hotloadData)
+ {
g_Selection.selected = hotloadData.selection;
+ g_PlayerAssignments = hotloadData.playerAssignments;
+ g_Players = hotloadData.player;
+ }
sendLobbyPlayerlistUpdate();
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
}
function initGUIObjects()
{
initMenu();
updateGameSpeedControl();
resizeDiplomacyDialog();
resizeTradeDialog();
initBarterButtons();
initPanelEntities();
initViewedPlayerDropdown();
- initChatWindow();
Engine.SetBoundingBoxDebugOverlay(false);
updateEnabledRangeOverlayTypes();
}
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;
}
function updateDiplomacyColorsButton()
{
g_DiplomacyColorsToggle = !g_DiplomacyColorsToggle;
let diplomacyColorsButton = Engine.GetGUIObjectByName("diplomacyColorsButton");
diplomacyColorsButton.sprite = g_DiplomacyColorsToggle ?
"stretched:session/minimap-diplomacy-on.png" :
"stretched:session/minimap-diplomacy-off.png";
diplomacyColorsButton.sprite_over = g_DiplomacyColorsToggle ?
"stretched:session/minimap-diplomacy-on-highlight.png" :
"stretched:session/minimap-diplomacy-off-highlight.png";
Engine.GetGUIObjectByName("diplomacyColorsWindowButtonIcon").sprite = g_DiplomacyColorsToggle ?
"stretched:session/icons/diplomacy-on.png" :
"stretched:session/icons/diplomacy.png";
updateDisplayedPlayerColors();
}
/**
* Updates the displayed colors of players in the simulation and GUI.
*/
function updateDisplayedPlayerColors()
{
if (g_DiplomacyColorsToggle)
{
let getDiplomacyColor = stance =>
guiToRgbColor(Engine.ConfigDB_GetValue("user", "gui.session.diplomacycolors." + stance)) ||
guiToRgbColor(Engine.ConfigDB_GetValue("default", "gui.session.diplomacycolors." + stance));
let teamRepresentatives = {};
for (let i = 1; i < g_Players.length; ++i)
if (g_ViewedPlayer <= 0)
{
// Observers and gaia see team colors
let team = g_Players[i].team;
g_DisplayedPlayerColors[i] = g_Players[teamRepresentatives[team] || i].color;
if (team != -1 && !teamRepresentatives[team])
teamRepresentatives[team] = i;
}
else
// Players see colors depending on diplomacy
g_DisplayedPlayerColors[i] =
g_ViewedPlayer == i ? getDiplomacyColor("self") :
g_Players[g_ViewedPlayer].isAlly[i] ? getDiplomacyColor("ally") :
g_Players[g_ViewedPlayer].isNeutral[i] ? getDiplomacyColor("neutral") :
getDiplomacyColor("enemy");
g_DisplayedPlayerColors[0] = g_Players[0].color;
}
else
g_DisplayedPlayerColors = g_Players.map(player => player.color);
Engine.GuiInterfaceCall("UpdateDisplayedPlayerColors", {
"displayedPlayerColors": g_DisplayedPlayerColors,
"displayDiplomacyColors": g_DiplomacyColorsToggle,
"showAllStatusBars": g_ShowAllStatusBars,
"selected": g_Selection.toList()
});
updateGUIObjects();
}
/**
* Depends on the current player (g_IsObserver).
*/
function updateHotkeyTooltips()
{
- Engine.GetGUIObjectByName("chatInput").tooltip =
- translateWithContext("chat input", "Type the message to send.") + "\n" +
- colorizeAutocompleteHotkey() +
- colorizeHotkey("\n" + translate("Press %(hotkey)s to open the public chat."), "chat") +
- colorizeHotkey(
- "\n" + (g_IsObserver ?
- translate("Press %(hotkey)s to open the observer chat.") :
- translate("Press %(hotkey)s to open the ally chat.")),
- "teamchat") +
- colorizeHotkey("\n" + translate("Press %(hotkey)s to open the previously selected private chat."), "privatechat");
-
Engine.GetGUIObjectByName("idleWorkerButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "selection.idleworker") +
translate("Find idle worker");
Engine.GetGUIObjectByName("diplomacyColorsButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "session.diplomacycolors") +
translate("Toggle Diplomacy Colors");
Engine.GetGUIObjectByName("diplomacyColorsWindowButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "session.diplomacycolors") +
translate("Toggle Diplomacy Colors");
Engine.GetGUIObjectByName("diplomacyButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "session.gui.diplomacy.toggle") +
translate("Diplomacy");
Engine.GetGUIObjectByName("tradeButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "session.gui.barter.toggle") +
translate("Barter & Trade");
Engine.GetGUIObjectByName("tradeHelp").tooltip = colorizeHotkey(
translate("Select one type of goods you want to modify by clicking on it, and then use the arrows of the other types to modify their shares. You can also press %(hotkey)s while selecting one type of goods to bring its share to 100%%."),
"session.fulltradeswap");
Engine.GetGUIObjectByName("barterHelp").tooltip = sprintf(
translate("Start by selecting the resource you wish to sell from the upper row. For each time the lower buttons are pressed, %(quantity)s of the upper resource will be sold for the displayed quantity of the lower. Press and hold %(hotkey)s to temporarily multiply the traded amount by %(multiplier)s."), {
"quantity": g_BarterResourceSellQuantity,
"hotkey": colorizeHotkey("%(hotkey)s", "session.massbarter"),
"multiplier": g_BarterMultiplier
});
Engine.GetGUIObjectByName("objectivesButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "session.gui.objectives.toggle") +
translate("Objectives");
}
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 initViewedPlayerDropdown()
{
g_DisplayedPlayerColors = g_Players.map(player => player.color);
updateViewedPlayerDropdown();
// Select "observer" in the view player dropdown when rejoining as a defeated player
let player = g_Players[Engine.GetPlayerID()];
Engine.GetGUIObjectByName("viewPlayer").selected = player && player.state == "defeated" ? 0 : Engine.GetPlayerID() + 1;
}
function updateViewedPlayerDropdown()
{
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.list_data = [-1].concat(g_Players.map((player, i) => i));
viewPlayer.list = [translate("Observer")].concat(g_Players.map(
(player, i) => colorizePlayernameHelper("■", i) + " " + player.name
));
}
/**
* Change perspective tool.
* Shown to observers or when enabling the developers option.
*/
function selectViewPlayer(playerID)
{
if (playerID < -1 || playerID > g_Players.length - 1)
return;
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay(true);
g_IsObserver = isPlayerObserver(Engine.GetPlayerID());
if (g_IsObserver || g_DeveloperOverlay.isChangePerspective())
{
if (g_ViewedPlayer != playerID)
clearSelection();
g_ViewedPlayer = playerID;
}
if (g_DeveloperOverlay.isChangePerspective())
{
Engine.SetPlayerID(g_ViewedPlayer);
g_IsObserver = isPlayerObserver(g_ViewedPlayer);
}
Engine.SetViewedPlayer(g_ViewedPlayer);
updateDisplayedPlayerColors();
updateTopPanel();
- updateChatAddressees();
+ g_Chat.onUpdatePlayers();
updateHotkeyTooltips();
// Update GUI and clear player-dependent cache
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
onSimulationUpdate();
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)
{
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": "defeat-victory",
+ "type": "playerstate",
"message": victoryString,
"players": players
});
if (players.indexOf(Engine.GetPlayerID()) != -1)
reportGame();
sendLobbyPlayerlistUpdate();
updatePlayerData();
- updateChatAddressees();
+ g_Chat.onUpdatePlayers();
updateGameSpeedControl();
if (players.indexOf(g_ViewedPlayer) == -1)
return;
// Select "observer" item on loss. On win enable observermode without changing perspective
Engine.GetGUIObjectByName("viewPlayer").selected = won ? g_ViewedPlayer + 1 : 0;
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";
}
/**
* Sets civ icon for the currently viewed player.
* Hides most gui objects for observers.
*/
function updateTopPanel()
{
let isPlayer = g_ViewedPlayer > 0;
let civIcon = Engine.GetGUIObjectByName("civIcon");
civIcon.hidden = !isPlayer;
if (isPlayer)
{
civIcon.sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem;
Engine.GetGUIObjectByName("civIconOverlay").tooltip =
sprintf(
translate("%(civ)s\n%(hotkey_civinfo)s / %(hotkey_structree)s: View History / Structure Tree\nLast opened will be reopened on click."), {
"civ": setStringTags(g_CivData[g_Players[g_ViewedPlayer].civ].Name, { "font": "sans-bold-stroke-14" }),
"hotkey_civinfo": colorizeHotkey("%(hotkey)s", "civinfo"),
"hotkey_structree": colorizeHotkey("%(hotkey)s", "structree")
});
}
// Following gaia can be interesting on scripted maps
Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver || g_ViewedPlayer == -1;
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.hidden = !g_IsObserver && !g_DeveloperOverlay.isChangePerspective();
let followPlayerLabel = Engine.GetGUIObjectByName("followPlayerLabel");
followPlayerLabel.hidden = Engine.GetTextWidth(followPlayerLabel.font, followPlayerLabel.caption + " ") +
followPlayerLabel.getComputedSize().left > viewPlayer.getComputedSize().left;
let resCodes = g_ResourceData.GetCodes();
let r = 0;
for (let res of resCodes)
{
if (!Engine.GetGUIObjectByName("resource[" + r + "]"))
{
warn("Current GUI limits prevent displaying more than " + r + " resources in the top panel!");
break;
}
Engine.GetGUIObjectByName("resource[" + r + "]_icon").sprite = "stretched:session/icons/resources/" + res + ".png";
Engine.GetGUIObjectByName("resource[" + r + "]").hidden = !isPlayer;
++r;
}
horizontallySpaceObjects("resourceCounts", 5);
hideRemaining("resourceCounts", r);
let resPop = Engine.GetGUIObjectByName("population");
let resPopSize = resPop.size;
resPopSize.left = Engine.GetGUIObjectByName("resource[" + (r - 1) + "]").size.right;
resPop.size = resPopSize;
Engine.GetGUIObjectByName("population").hidden = !isPlayer;
Engine.GetGUIObjectByName("diplomacyButton").hidden = !isPlayer;
Engine.GetGUIObjectByName("tradeButton").hidden = !isPlayer ||
(!g_ResourceData.GetTradableCodes().length && !g_ResourceData.GetBarterableCodes().length);
Engine.GetGUIObjectByName("observerText").hidden = isPlayer;
let alphaLabel = Engine.GetGUIObjectByName("alphaLabel");
alphaLabel.hidden = isPlayer && !viewPlayer.hidden;
alphaLabel.size = isPlayer ? "50%+44 0 100%-283 100%" : "155 0 85%-279 100%";
Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked || g_IsController;
Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver;
Engine.GetGUIObjectByName("lobbyButton").enabled = Engine.HasXmppClient();
}
/**
* Resign a player.
* @param leaveGameAfterResign If player is quitting after resignation.
*/
function resignGame(leaveGameAfterResign)
{
if (g_IsObserver || g_Disconnected)
return;
Engine.PostNetworkCommand({
"type": "resign"
});
if (!leaveGameAfterResign)
resumeGame(true);
}
/**
* Leave the game
* @param willRejoin If player is going to be rejoining a networked game.
*/
function leaveGame(willRejoin)
{
if (!willRejoin && !g_IsObserver)
resignGame(true);
// 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 };
+ 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]));
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();
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates color every 500msec)
Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && now % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor;
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function onWindowResized()
{
// Update followPlayerLabel
updateTopPanel();
- resizeChatWindow();
+ g_Chat.ChatWindow.resizeChatWindow();
}
function changeGameSpeed(speed)
{
if (!g_IsNetworked)
Engine.SetSimRate(speed);
}
function updateIdleWorkerButton()
{
Engine.GetGUIObjectByName("idleWorkerButton").enabled = Engine.GuiInterfaceCall("HasIdleUnits", {
"viewedPlayer": g_ViewedPlayer,
"idleClasses": g_WorkerTypes,
"excludeUnits": []
});
}
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());
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();
// 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, leaveGame] : [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");
}
function updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updatePanelEntities();
displayPanelEntities();
updateGroups();
updatePlayerDisplay();
updateResearchDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
updateIdleWorkerButton();
if (g_IsTradeOpen)
{
updateTraderTexts();
updateBarterButtons();
}
if (g_ViewedPlayer > 0)
{
let playerState = GetSimState().players[g_ViewedPlayer];
g_DeveloperOverlay.setControlAll(playerState && playerState.controlsAll);
}
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]);
}
updateViewedPlayerDropdown();
updateDiplomacy();
g_DeveloperOverlay.update();
}
function saveResPopTooltipSort()
{
Engine.ConfigDB_CreateAndWriteValueToFile("user", "gui.session.respoptooltipsort", String((+Engine.ConfigDB_GetValue("user", "gui.session.respoptooltipsort") + 2) % 3 - 1), "config/user.cfg");
}
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 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 = g_PanelEntities.sort((panelEntA, panelEntB) => panelEntIndex(panelEntA.ent) - panelEntIndex(panelEntB.ent));
}
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);
}
}
/**
* Create ally player stat tooltip.
* @param {string} resource - Resource type, on which values will be sorted.
* @param {object} playerStates - Playerstates from players whos stats are viewed in the tooltip.
* @param {number} sort - 0 no order, -1 descending, 1 ascending order.
* @returns {string} Tooltip string.
*/
function getAllyStatTooltip(resource, playerStates, sort)
{
let tooltip = [];
for (let player in playerStates)
tooltip.push({
"playername": colorizePlayernameHelper("■", player) + " " + g_Players[player].name,
"statValue": resource == "pop" ?
sprintf(translate("%(popCount)s/%(popLimit)s/%(popMax)s"), playerStates[player]) :
Math.round(playerStates[player].resourceCounts[resource]),
"orderValue": resource == "pop" ? playerStates[player].popCount :
Math.round(playerStates[player].resourceCounts[resource])
});
if (sort)
tooltip.sort((a, b) => sort * (b.orderValue - a.orderValue));
return "\n" + tooltip.map(stat => sprintf(translate("%(playername)s: %(statValue)s"), stat)).join("\n");
}
function updatePlayerDisplay()
{
let allPlayerStates = GetSimState().players;
let viewedPlayerState = allPlayerStates[g_ViewedPlayer];
let viewablePlayerStates = {};
for (let player in allPlayerStates)
if (player != 0 &&
player != g_ViewedPlayer &&
g_Players[player].state != "defeated" &&
(g_IsObserver ||
viewedPlayerState.hasSharedLos &&
g_Players[player].isMutualAlly[g_ViewedPlayer]))
viewablePlayerStates[player] = allPlayerStates[player];
if (!viewedPlayerState)
return;
let tooltipSort = +Engine.ConfigDB_GetValue("user", "gui.session.respoptooltipsort");
let orderHotkeyTooltip = Object.keys(viewablePlayerStates).length <= 1 ? "" :
"\n" + sprintf(translate("%(order)s: %(hotkey)s to change order."), {
"hotkey": setStringTags("\\[Click]", g_HotkeyTags),
"order": tooltipSort == 0 ? translate("Unordered") : tooltipSort == 1 ? translate("Descending") : translate("Ascending")
});
let resCodes = g_ResourceData.GetCodes();
for (let r = 0; r < resCodes.length; ++r)
{
let resourceObj = Engine.GetGUIObjectByName("resource[" + r + "]");
if (!resourceObj)
break;
let res = resCodes[r];
let tooltip = '[font="' + g_ResourceTitleFont + '"]' +
resourceNameFirstWord(res) + '[/font]';
let descr = g_ResourceData.GetResource(res).description;
if (descr)
tooltip += "\n" + translate(descr);
tooltip += orderHotkeyTooltip + getAllyStatTooltip(res, viewablePlayerStates, tooltipSort);
resourceObj.tooltip = tooltip;
Engine.GetGUIObjectByName("resource[" + r + "]_count").caption = Math.floor(viewedPlayerState.resourceCounts[res]);
}
Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), viewedPlayerState);
Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" +
sprintf(translate("Maximum population: %(popCap)s"), { "popCap": viewedPlayerState.popMax }) +
orderHotkeyTooltip +
getAllyStatTooltip("pop", viewablePlayerStates, tooltipSort);
g_IsTrainingBlocked = viewedPlayerState.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, 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"
});
}
/**
* 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 23061)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 23062)
@@ -1,159 +1,160 @@
+
onTick();
onWindowResized();
restoreSavedGameData(arguments[0]);
onSimulationUpdate();
onReplayFinished();
onReplayOutOfSync(arguments[0], arguments[1], arguments[2]);
Engine.ConfigDB_CreateValue("user", "gui.session.timeelapsedcounter", String(Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") != "true"));
Engine.ConfigDB_CreateValue("user", "gui.session.ceasefirecounter", String(Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") != "true"));
Exit
leaveGame();
Game Paused
Click to Resume Game
togglePause();
+
-