Rewrite session chat to use hierarchical object oriented design using class syntax, refs #5387.
Set global hotkeys in JS instead of empty XML objects from f192d4a2fa/D2260. Differential Revision: https://code.wildfiregames.com/D2355 This was SVN commit r23062.
This commit is contained in:
parent
b76239cbe7
commit
47f5c27eec
87
binaries/data/mods/public/gui/session/chat/Chat.js
Normal file
87
binaries/data/mods/public/gui/session/chat/Chat.js
Normal file
@ -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
|
||||
});
|
||||
}
|
||||
}
|
127
binaries/data/mods/public/gui/session/chat/ChatAddressees.js
Normal file
127
binaries/data/mods/public/gui/session/chat/ChatAddressees.js
Normal file
@ -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()
|
||||
}
|
||||
];
|
134
binaries/data/mods/public/gui/session/chat/ChatHistory.js
Normal file
134
binaries/data/mods/public/gui/session/chat/ChatHistory.js
Normal file
@ -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()
|
||||
}
|
||||
];
|
79
binaries/data/mods/public/gui/session/chat/ChatInput.js
Normal file
79
binaries/data/mods/public/gui/session/chat/ChatInput.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -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) });
|
||||
}
|
||||
};
|
@ -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")
|
||||
}
|
||||
};
|
@ -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)
|
||||
});
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
69
binaries/data/mods/public/gui/session/chat/ChatOverlay.js
Normal file
69
binaries/data/mods/public/gui/session/chat/ChatOverlay.js
Normal file
@ -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");
|
||||
}
|
||||
}
|
94
binaries/data/mods/public/gui/session/chat/ChatWindow.js
Normal file
94
binaries/data/mods/public/gui/session/chat/ChatWindow.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -27,12 +27,11 @@
|
||||
tooltip_style="sessionToolTipBold"
|
||||
>
|
||||
<translatableAttribute id="tooltip" context="chat input">Filter the chat history.</translatableAttribute>
|
||||
<action on="SelectionChange">updateChatHistory();</action>
|
||||
</object>
|
||||
|
||||
<object
|
||||
type="text"
|
||||
name="chatHistory"
|
||||
name="chatHistoryText"
|
||||
size="10 46 100%-10 100%"
|
||||
sprite="ModernDarkBoxGold"
|
||||
style="ChatPanel"
|
||||
@ -65,20 +64,11 @@
|
||||
name="chatInput"
|
||||
size="75 100%-76 100%-16 100%-52"
|
||||
style="ModernInput"
|
||||
>
|
||||
<action on="Press">submitChatInput();</action>
|
||||
<action on="Tab">
|
||||
let playernames = [];
|
||||
for (let player in g_PlayerAssignments)
|
||||
playernames.push(g_PlayerAssignments[player].name);
|
||||
autoCompleteText(this, playernames);
|
||||
</action>
|
||||
</object>
|
||||
/>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<object size="16 100%-40 30%+16 100%-12" type="button" style="StoneButton">
|
||||
<object name="closeChat" size="16 100%-40 30%+16 100%-12" type="button" style="StoneButton">
|
||||
<translatableAttribute id="caption">Cancel</translatableAttribute>
|
||||
<action on="Press">closeChat();</action>
|
||||
</object>
|
||||
|
||||
<!-- Extended Chat Checkbox -->
|
||||
@ -88,9 +78,7 @@
|
||||
checked="false"
|
||||
style="ModernTickBox"
|
||||
size="50%-40 100%-38 50%-20 100%-12"
|
||||
>
|
||||
<action on="Press">onToggleChatWindowExtended();</action>
|
||||
</object>
|
||||
/>
|
||||
|
||||
<!-- Extended Chat Label -->
|
||||
<object type="text" size="50%-20 100%-38 50%+50 100%-12" text_align="left" textcolor="white">
|
||||
@ -98,9 +86,8 @@
|
||||
</object>
|
||||
|
||||
<!-- Send Button -->
|
||||
<object size="70%-16 100%-40 100%-16 100%-12" type="button" style="StoneButton">
|
||||
<object name="sendChat" size="70%-16 100%-40 100%-16 100%-12" type="button" style="StoneButton">
|
||||
<translatableAttribute id="caption">Send</translatableAttribute>
|
||||
<action on="Press">submitChatInput();</action>
|
||||
</object>
|
||||
</object>
|
||||
|
@ -202,7 +202,7 @@ DeveloperOverlay.prototype.toggle = function()
|
||||
|
||||
// Only players can send the simulation chat command
|
||||
if (Engine.GetPlayerID() == -1)
|
||||
submitChatDirectly(message);
|
||||
g_Chat.submitChat(message);
|
||||
else
|
||||
Engine.PostNetworkCommand({
|
||||
"type": "aichat",
|
||||
|
@ -4,18 +4,6 @@
|
||||
<action on="Press">closeOpenDialogs();</action>
|
||||
</object>
|
||||
|
||||
<object hotkey="chat">
|
||||
<action on="Press">openChat();</action>
|
||||
</object>
|
||||
|
||||
<object hotkey="teamchat">
|
||||
<action on="Press">openChat(g_IsObserver ? "/observers" : "/allies");</action>
|
||||
</object>
|
||||
|
||||
<object hotkey="privatechat">
|
||||
<action on="Press">openChat(g_LastChatAddressee);</action>
|
||||
</object>
|
||||
|
||||
<object hotkey="session.gui.toggle">
|
||||
<action on="Press">toggleGUI();</action>
|
||||
</object>
|
||||
|
@ -136,8 +136,7 @@ function lobbyDialogButton()
|
||||
|
||||
function chatMenuButton()
|
||||
{
|
||||
closeOpenDialogs();
|
||||
openChat();
|
||||
g_Chat.openPage();
|
||||
}
|
||||
|
||||
function resignMenuButton()
|
||||
@ -248,29 +247,6 @@ function openOptions()
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
@ -295,74 +271,6 @@ function resizeDiplomacyDialog()
|
||||
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();
|
||||
@ -1250,10 +1158,11 @@ function openManual()
|
||||
function closeOpenDialogs()
|
||||
{
|
||||
closeMenu();
|
||||
closeChat();
|
||||
closeDiplomacy();
|
||||
closeTrade();
|
||||
closeObjectives();
|
||||
|
||||
g_Chat.closePage();
|
||||
}
|
||||
|
||||
function formatTributeTooltip(playerID, resourceCode, amount)
|
||||
|
@ -3,36 +3,6 @@
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -97,52 +67,6 @@ var g_NetMessageTypes = {
|
||||
"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.
|
||||
*/
|
||||
@ -156,141 +80,11 @@ var g_StatusMessageTypes = {
|
||||
"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
|
||||
@ -478,7 +272,7 @@ var g_NotificationsTypes =
|
||||
g_Selection.reset();
|
||||
g_Selection.addList(selection, false, cmd.type == "gather");
|
||||
},
|
||||
"play-tracks": function (notification, player)
|
||||
"play-tracks": function(notification, player)
|
||||
{
|
||||
if (notification.lock)
|
||||
{
|
||||
@ -765,7 +559,7 @@ function handlePlayerAssignmentsMessage(message)
|
||||
});
|
||||
|
||||
updateGUIObjects();
|
||||
updateChatAddressees();
|
||||
g_Chat.onUpdatePlayers();
|
||||
sendLobbyPlayerlistUpdate();
|
||||
}
|
||||
|
||||
@ -800,178 +594,14 @@ function onClientLeave(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();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1006,257 +636,6 @@ function colorizePlayernameParameters(parameters)
|
||||
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.
|
||||
*/
|
||||
|
@ -10,6 +10,8 @@ const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.Starting
|
||||
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
|
||||
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
|
||||
|
||||
var g_Chat;
|
||||
|
||||
var g_GameSpeeds;
|
||||
|
||||
/**
|
||||
@ -271,13 +273,19 @@ function init(initData, hotloadData)
|
||||
}
|
||||
|
||||
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();
|
||||
@ -293,7 +301,6 @@ function initGUIObjects()
|
||||
initBarterButtons();
|
||||
initPanelEntities();
|
||||
initViewedPlayerDropdown();
|
||||
initChatWindow();
|
||||
Engine.SetBoundingBoxDebugOverlay(false);
|
||||
updateEnabledRangeOverlayTypes();
|
||||
}
|
||||
@ -416,17 +423,6 @@ function updateDisplayedPlayerColors()
|
||||
*/
|
||||
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");
|
||||
@ -557,7 +553,7 @@ function selectViewPlayer(playerID)
|
||||
Engine.SetViewedPlayer(g_ViewedPlayer);
|
||||
updateDisplayedPlayerColors();
|
||||
updateTopPanel();
|
||||
updateChatAddressees();
|
||||
g_Chat.onUpdatePlayers();
|
||||
updateHotkeyTooltips();
|
||||
|
||||
// Update GUI and clear player-dependent cache
|
||||
@ -605,7 +601,7 @@ function controlsPlayer(playerID)
|
||||
function playersFinished(players, victoryString, won)
|
||||
{
|
||||
addChatMessage({
|
||||
"type": "defeat-victory",
|
||||
"type": "playerstate",
|
||||
"message": victoryString,
|
||||
"players": players
|
||||
});
|
||||
@ -616,7 +612,7 @@ function playersFinished(players, victoryString, won)
|
||||
sendLobbyPlayerlistUpdate();
|
||||
|
||||
updatePlayerData();
|
||||
updateChatAddressees();
|
||||
g_Chat.onUpdatePlayers();
|
||||
updateGameSpeedControl();
|
||||
|
||||
if (players.indexOf(g_ViewedPlayer) == -1)
|
||||
@ -762,7 +758,11 @@ function leaveGame(willRejoin)
|
||||
// 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()
|
||||
@ -837,7 +837,7 @@ function onWindowResized()
|
||||
// Update followPlayerLabel
|
||||
updateTopPanel();
|
||||
|
||||
resizeChatWindow();
|
||||
g_Chat.ChatWindow.resizeChatWindow();
|
||||
}
|
||||
|
||||
function changeGameSpeed(speed)
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
<script directory="gui/common/"/>
|
||||
<script directory="gui/session/"/>
|
||||
<script directory="gui/session/chat/"/>
|
||||
|
||||
<object name="session">
|
||||
|
||||
@ -94,8 +95,8 @@
|
||||
font="mono-stroke-10"
|
||||
/>
|
||||
|
||||
<include directory="gui/session/chat/"/>
|
||||
<include directory="gui/session/dialogs/"/>
|
||||
<include file="gui/session/chat_window.xml"/>
|
||||
<include file="gui/session/developer_overlay.xml"/>
|
||||
<include file="gui/session/diplomacy_window.xml"/>
|
||||
<include file="gui/session/objectives_window.xml"/>
|
||||
|
Loading…
Reference in New Issue
Block a user