Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 17703) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 17704) @@ -1,788 +1,787 @@ /** * All known cheat commands. * @type {Object} */ const g_Cheats = getCheatsData(); /** * Number of seconds after which chatmessages will disappear. */ const g_ChatTimeout = 30; /** * Maximum number of lines to display simultaneously. */ const g_ChatLines = 20; /** * The strings to be displayed including sender and formating. */ var g_ChatMessages = []; /** * Holds the timer-IDs used for hiding the chat after g_ChatTimeout seconds. */ var g_ChatTimers = []; /** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => handleNetStatusMessage(msg), "players": msg => handlePlayerAssignmentsMessage(msg), "rejoined": msg => addChatMessage({ "type": "rejoined", "guid": msg.guid }), "kicked": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been kicked"), { "username": msg.username }) }), "banned": msg => addChatMessage({ "type": "system", "text": sprintf(translate("%(username)s has been banned"), { "username": msg.username }) }), "chat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }), "aichat": msg => addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text, "translate": true }), "gamesetup": msg => "", // Needed for autostart "start": msg => "" }; var g_FormatChatMessage = { "system": msg => msg.text, "connect": msg => sprintf(translate("%(player)s is starting to rejoin the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "disconnect": msg => sprintf(translate("%(player)s has left the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "rejoined": msg => sprintf(translate("%(player)s has rejoined the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "clientlist": msg => getUsernameList(), "message": msg => formatChatCommand(msg), "defeat": msg => formatDefeatMessage(msg), "diplomacy": msg => formatDiplomacyMessage(msg), "tribute": msg => formatTributeMessage(msg), "attack": msg => formatAttackMessage(msg) }; /** * Show a label and grey overlay or hide both on connection change. */ var g_StatusMessageTypes = { "authenticated": msg => translate("Connection to the server has been authenticated."), "connected": msg => translate("Connected to the server."), "disconnected": msg => translate("Connection to the server has been lost.") + "\n" + // Translation: States the reason why the client disconnected from the server. sprintf(translate("Reason: %(reason)s."), { "reason": getDisconnectReason(msg.reason) }) + "\n" + translate("The game has ended."), "waiting_for_players": msg => translate("Waiting for other players to connect..."), "join_syncing": msg => translate("Synchronising gameplay with other players..."), "active": msg => "" }; /** * Chatmessage shown after commands like /me or /enemies. */ var g_ChatCommands = { "regular": { "context": translate("(%(context)s) %(userTag)s %(message)s"), "no-context": translate("%(userTag)s %(message)s") }, "me": { "context": translate("(%(context)s) * %(user)s %(message)s"), "no-context": translate("* %(user)s %(message)s") } }; var g_ChatAddresseeContext = { "/team": translate("Team"), "/allies": translate("Ally"), "/enemies": translate("Enemy"), "/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[senderID].isMutualAlly[Engine.GetPlayerID()], "/enemies": senderID => g_Players[senderID] && g_Players[senderID].isEnemy[Engine.GetPlayerID()], "/msg": (senderID, addresseeGUID) => g_Players[Engine.GetPlayerID()] && g_PlayerAssignments[addresseeGUID] && g_PlayerAssignments[addresseeGUID].name == g_Players[Engine.GetPlayerID()].name }; /** * Chatmessage shown on diplomacy change. */ var g_DiplomacyMessages = { "active": { "ally": translate("You are now allied with %(player)s."), "enemy": translate("You are now at war with %(player)s."), "neutral": translate("You are now neutral with %(player)s.") }, "passive": { "ally": translate("%(player)s is now allied with you."), "enemy": translate("%(player)s is now at war with you."), "neutral": translate("%(player)s is now neutral with you.") }, "observer": { "ally": translate("%(player)s is now allied with %(player2)s."), "enemy": translate("%(player)s is now at war with %(player2)s."), "neutral": translate("%(player)s is now neutral with %(player2)s.") } }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. */ var g_NotificationsTypes = { "chat": function(notification, player) { let message = { "type": "message", "guid": findGuidForPlayerID(player) || -1, "text": notification.message }; if (message.guid == -1) message.player = player; addChatMessage(message); }, "aichat": function(notification, player) { let message = { "guid": findGuidForPlayerID(player) || -1, "type": "message", "text": notification.message, "translate": true }; if (message.guid == -1) message.player = player; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; // special case for formatting of player names which are transmitted as _player_num for (let param in message.parameters) { if (!param.startsWith("_player_")) continue; message.parameters[param] = colorizePlayernameByID(message.parameters[param]); } } addChatMessage(message); }, "defeat": function(notification, player) { addChatMessage({ "type": "defeat", "guid": findGuidForPlayerID(player), "player": player }); updateDiplomacy(); }, "diplomacy": function(notification, player) { addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); updateDiplomacy(); }, "quit": function(notification, player) { Engine.Exit(); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "attack": function(notification, player) { if (player != Engine.GetPlayerID()) return; if (Engine.ConfigDB_GetValue("user", "gui.session.attacknotificationmessage") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "dialog": function(notification, player) { if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, "resetselectionpannel": function(notification, player) { if (player != Engine.GetPlayerID()) return; g_Selection.rebuildSelection({}); } }; /** * Loads all known cheat commands. * * @returns {Object} */ function getCheatsData() { let cheats = {}; for (let fileName of getJSONFileList("simulation/data/cheats/")) { let currentCheat = Engine.ReadJSONFile("simulation/data/cheats/"+fileName+".json"); if (!currentCheat) continue; if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1) warn("Cheat name '" + currentCheat.Name + "' is already present"); else cheats[currentCheat.Name] = currentCheat.Data; } return cheats; } /** * Reads userinput from the chat and sends a simulation command in case it is a known cheat. * Hence cheats won't be sent as chat over network. * * @param {string} text * @returns {boolean} - True if a cheat was executed. */ function executeCheat(text) { if (Engine.GetPlayerID() == -1 || !g_Players[Engine.GetPlayerID()].cheatsEnabled) return false; // Find the cheat code that is a prefix of the user input let cheatCode = Object.keys(g_Cheats).find(cheatCode => text.indexOf(cheatCode) == 0); if (!cheatCode) return false; let cheat = g_Cheats[cheatCode]; let parameter = text.substr(cheatCode.length); if (cheat.isNumeric) parameter = +parameter; if (cheat.DefaultParameter && (isNaN(parameter) || parameter <= 0)) parameter = cheat.DefaultParameter; Engine.PostNetworkCommand({ "type": "cheat", "action": cheat.Action, "text": cheat.Type, "player": Engine.GetPlayerID(), "parameter": parameter, "templates": cheat.Templates, "selected": g_Selection.toList() }); return true; } function findGuidForPlayerID(playerID) { return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID); } /** * Processes all pending simulation messages. */ function handleNotifications() { let notifications = Engine.GuiInterfaceCall("GetNotifications"); for (let notification of notifications) { if (!notification.type) { error("Notification without type found.\n"+uneval(notification)); continue; } if (!notification.players) { error("Notification without players found.\n"+uneval(notification)); continue; } if (!g_NotificationsTypes[notification.type]) { error("Unknown notification type '" + notification.type + "' found."); continue; } for (let player of notification.players) g_NotificationsTypes[notification.type](notification, player); } } /** * Updates playerdata cache and refresh diplomacy panel. */ function updateDiplomacy() { g_Players = getPlayerData(g_PlayerAssignments); if (g_IsDiplomacyOpen) openDiplomacy(); } /** * Displays all active counters (messages showing the remaining time) for wonder-victory, ceasefire etc. */ function updateTimeNotifications() { let notifications = Engine.GuiInterfaceCall("GetTimeNotifications"); let notificationText = ""; for (let n of notifications) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); parameters.time = timeToString(n.endTime - g_SimState.timeElapsed); notificationText += sprintf(message, parameters) + "\n"; } Engine.GetGUIObjectByName("notificationText").caption = notificationText; } /** * Processes a CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. * * @param {Object} msg */ function handleNetMessage(msg) { log("Net message: " + uneval(msg)); if (g_NetMessageTypes[msg.type]) g_NetMessageTypes[msg.type](msg); else error("Unrecognised net message type '" + msg.type + "'"); } /** * @param {Object} message */ function handleNetStatusMessage(message) { if (g_Disconnected) return; if (!g_StatusMessageTypes[message.status]) { error("Unrecognised netstatus type '" + message.status + "'"); return; } let label = Engine.GetGUIObjectByName("netStatus"); let statusMessage = g_StatusMessageTypes[message.status](message); label.caption = statusMessage; label.hidden = !statusMessage; if (message.status == "disconnected") { g_Disconnected = true; closeOpenDialogs(); } } function handlePlayerAssignmentsMessage(message) { // Find and report all leavings for (let guid in g_PlayerAssignments) { if (message.hosts[guid]) continue; addChatMessage({ "type": "disconnect", "guid": guid }); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; } let joins = Object.keys(message.hosts).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.hosts; // Report all joinings joins.forEach(guid => { let playerID = g_PlayerAssignments[guid].player; if (g_Players[playerID]) { g_Players[playerID].guid = guid; g_Players[playerID].name = g_PlayerAssignments[guid].name; g_Players[playerID].offline = false; } addChatMessage({ "type": "connect", "guid": guid }); }); // Update lobby gamestatus if (g_IsController && Engine.HasXmppClient()) { let players = Object.keys(g_PlayerAssignments).map(guid => g_PlayerAssignments[guid].name); Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", ")); } } /** * 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 teamChat = Engine.GetGUIObjectByName("toggleTeamChat").checked; let input = Engine.GetGUIObjectByName("chatInput"); let text = input.caption; input.blur(); // Remove focus input.caption = ""; // Clear chat input toggleChatWindow(); if (!text.length) return; if (executeNetworkCommand(text)) return; if (executeCheat(text)) return; // Observers should only be able to chat with everyone. if (g_IsObserver && text.indexOf("/") == 0 && text.indexOf("/me ") != 0) return; if (teamChat && text.indexOf("/team ") != 0) text = "/team " + text; submitChatDirectly(text); } /** * Displays the prepared chatmessage. * * @param msg {Object} */ function addChatMessage(msg) { if (!g_FormatChatMessage[msg.type]) return; let formatted = g_FormatChatMessage[msg.type](msg); if (!formatted) return; g_ChatMessages.push(formatted); g_ChatTimers.push(setTimeout(removeOldChatMessage, g_ChatTimeout * 1000)); if (g_ChatMessages.length > g_ChatLines) removeOldChatMessage(); else Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } /** * Called when the timer has run out for the oldest chatmessage or when the message limit is reached. */ function removeOldChatMessage() { clearTimeout(g_ChatTimers[0]); g_ChatTimers.shift(); g_ChatMessages.shift(); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } /** * This function is used for AIs, whose names don't exist in g_PlayerAssignments. */ function colorizePlayernameByID(playerID) { let username = playerID > -1 ? escapeText(g_Players[playerID].name) : translate("Unknown Player"); let playerColor = playerID > -1 ? rgbToGuiColor(g_Players[playerID].color) : "white"; return "[color=\"" + playerColor + "\"]" + username + "[/color]"; } function colorizePlayernameByGUID(guid) { let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : translate("Unknown Player"); let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; let playerColor = playerID > 0 ? rgbToGuiColor(g_Players[playerID].color) : "white"; return "[color=\"" + playerColor + "\"]" + username + "[/color]"; } function formatDefeatMessage(msg) { // In singleplayer, the local player is "You". "You has" is incorrect. let message = !g_IsNetworked && msg.player == Engine.GetPlayerID() ? translate("You have been defeated.") : translate("%(player)s has been defeated."); return sprintf(message, { "player": colorizePlayernameByID(msg.player) }); } function formatDiplomacyMessage(msg) { let messageType; switch (Engine.GetPlayerID()) { // Check observer first, since we also want to see if the selected player in the developer-overlay has changed the diplomacy case -1: messageType = "observer"; break; case msg.sourcePlayer: messageType = "active"; break; case msg.targetPlayer: messageType = "passive"; break; default: return ""; } return sprintf(g_DiplomacyMessages[messageType][msg.status], { "player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer), "player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer) }); } function formatTributeMessage(msg) { // Check observer first, since we also want to see if the selected player in the developer-overlay has sent tributes let message = ""; if (Engine.GetPlayerID() == -1) message = translate("%(player)s has sent %(player2)s %(amounts)s."); else if (msg.targetPlayer == Engine.GetPlayerID()) message = translate("%(player)s has sent you %(amounts)s."); return sprintf(message, { "player": colorizePlayernameByID(msg.sourcePlayer), "player2": colorizePlayernameByID(msg.targetPlayer), "amounts": getLocalizedResourceAmounts(msg.amounts) }); } function formatAttackMessage(msg) { - // TODO: Show this to observers? if (msg.player != Engine.GetPlayerID()) return ""; let message = msg.targetIsDomesticAnimal ? translate("Your livestock has been attacked by %(attacker)s!") : translate("You have been attacked by %(attacker)s!"); return sprintf(message, { "attacker": colorizePlayernameByID(msg.attacker) }); } function formatChatCommand(msg) { if (!msg.text) return ""; let isMe = msg.text.indexOf("/me ") == 0; if (!isMe && !checkChatAddressee(msg)) return ""; isMe = msg.text.indexOf("/me ") == 0; if (isMe) msg.text = msg.text.substr("/me ".length); // Translate or escape text if (!msg.text) return ""; if (msg.translate) { msg.text = translate(msg.text); if (msg.translateParameters) { let parameters = msg.parameters || {}; translateObjectKeys(parameters, msg.translateParameters); msg.text = sprintf(msg.text, parameters); } } else msg.text = escapeText(msg.text); // GUID for players, playerID for AIs let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player); return sprintf(g_ChatCommands[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"], { "message": msg.text, "context": msg.context || undefined, "user": coloredUsername, "userTag": sprintf(translate("<%(user)s>"), { "user": coloredUsername }) }); } /** * Checks if the current user is an addressee of the chatmessage sent by another player. * * @param {Object} msg */ function checkChatAddressee(msg) { if (msg.text[0] != '/') return true; if (Engine.GetPlayerID() == -1) return false; let cmd = msg.text.split(/\s/)[0]; msg.text = msg.text.substr(cmd.length + 1); if (cmd == "/ally") cmd = "/allies"; if (cmd == "/enemy") cmd = "/enemies"; // GUID for players, ID for bots let senderID = (g_PlayerAssignments[msg.guid] || msg).player; let addresseeGUID; if (cmd == "/msg") { addresseeGUID = matchUsername(msg.text); let addressee = g_PlayerAssignments[addresseeGUID]; if (!addressee || addressee.player == -1 || senderID == -1) return false; msg.text = msg.text.substr(addressee.name.length + 1); } let isSender = senderID == Engine.GetPlayerID(); if (!g_ChatAddresseeContext[cmd]) { if (isSender) warn("Unknown chat command: " + cmd); return false; } msg.context = g_ChatAddresseeContext[cmd]; return isSender || g_IsChatAddressee[cmd](senderID, addresseeGUID); } /** * Returns the guid of the user with the longest name that is a prefix of the given string. */ function matchUsername(text) { if (!text) return ""; let match = ""; let playerGUID = ""; for (let guid in g_PlayerAssignments) { let pName = g_PlayerAssignments[guid].name; if (text.indexOf(pName + " ") == 0 && pName.length > match.length) { match = pName; playerGUID = guid; } } return playerGUID; } /** * Unused multiplayer-dialog. */ function sendDialogAnswer(guiObject, dialogName) { Engine.GetGUIObjectByName(dialogName+"-dialog").hidden = true; Engine.PostNetworkCommand({ "type": "dialog-answer", "dialog": dialogName, "answer": guiObject.name.split("-").pop(), }); resumeGame(); } /** * Unused multiplayer-dialog. */ 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/selection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 17703) +++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 17704) @@ -1,527 +1,540 @@ // Limits selection size const g_MaxSelectionSize = 200; // Alpha value of hovered/mouseover/highlighted selection overlays // (should probably be greater than always visible alpha value, // see CCmpSelectable) const g_HighlightedAlpha = 0.75; function _setHighlight(ents, alpha, selected) { if (ents.length) Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities":ents, "alpha":alpha, "selected":selected }); } function _setStatusBars(ents, enabled) { if (ents.length) Engine.GuiInterfaceCall("SetStatusBars", { "entities":ents, "enabled":enabled }); } function _setMotionOverlay(ents, enabled) { if (ents.length) Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities":ents, "enabled":enabled }); } function _playSound(ent) { Engine.GuiInterfaceCall("PlaySound", { "name":"select", "entity":ent }); } /** * EntityGroups class for managing grouped entities */ function EntityGroups() { this.groups = {}; this.ents = {}; } EntityGroups.prototype.reset = function() { this.groups = {}; this.ents = {}; }; EntityGroups.prototype.add = function(ents) { for each (var ent in ents) { if (!this.ents[ent]) { var entState = GetEntityState(ent); // When this function is called during group rebuild, deleted // entities will not yet have been removed, so entities might // still be present in the group despite not existing. if (!entState) continue; var templateName = entState.template; var key = GetTemplateData(templateName).selectionGroupName || templateName; // TODO ugly hack, just group them by player too. // Prefix garrisoned unit's selection name with the player they belong to var index = templateName.indexOf("&"); if (index != -1 && key.indexOf("&") == -1) key = templateName.slice(0, index+1) + key; if (this.groups[key]) this.groups[key] += 1; else this.groups[key] = 1; this.ents[ent] = key; } } }; EntityGroups.prototype.removeEnt = function(ent) { var templateName = this.ents[ent]; // Remove the entity delete this.ents[ent]; this.groups[templateName]--; // Remove the entire group if (this.groups[templateName] == 0) delete this.groups[templateName]; }; EntityGroups.prototype.rebuildGroup = function(renamed) { var oldGroup = this.ents; this.reset(); var toAdd = []; for (var ent in oldGroup) toAdd.push(renamed[ent] ? renamed[ent] : +ent); this.add(toAdd); }; EntityGroups.prototype.getCount = function(templateName) { return this.groups[templateName]; }; EntityGroups.prototype.getTotalCount = function() { var totalCount = 0; for each (var group in this.groups) totalCount += group; return totalCount; }; EntityGroups.prototype.getTemplateNames = function() { var templateNames = []; for (var templateName in this.groups) templateNames.push(templateName); //Preserve order even when shuffling units around //Can be optimized by moving the sorting elsewhere templateNames.sort(); return templateNames; }; EntityGroups.prototype.getEntsByName = function(templateName) { var ents = []; for (var ent in this.ents) if (this.ents[ent] == templateName) ents.push(+ent); return ents; }; /** * get a list of entities grouped by templateName */ EntityGroups.prototype.getEntsGrouped = function() { var templateNames = this.getTemplateNames(); var list = []; for (var t of templateNames) { list.push({ "ents": this.getEntsByName(t), "template": t, }); } return list; }; /** * Gets all ents in every group except ones of the specified group */ EntityGroups.prototype.getEntsByNameInverse = function(templateName) { var ents = []; for (var ent in this.ents) if (this.ents[ent] != templateName) ents.push(+ent); return ents; }; /** * EntitySelection class for managing the entity selection list and the primary selection */ function EntitySelection() { // Private properties: //-------------------------------- this.selected = {}; // { id:id, id:id, ... } for each selected entity ID 'id' // { id:id, ... } for mouseover-highlighted entity IDs in these, the key is a string and the value is an int; // we want to use the int form wherever possible since it's more efficient to send to the simulation code) this.highlighted = {}; this.motionDebugOverlay = false; // Public properties: //-------------------------------- this.dirty = false; // set whenever the selection has changed this.groups = new EntityGroups(); } /** * Deselect everything but entities of the chosen type if the modifier is true otherwise deselect just the chosen entity */ EntitySelection.prototype.makePrimarySelection = function(templateName, modifierKey) { var template = GetTemplateData(templateName); var key = template.selectionGroupName || templateName; var ents = []; if (modifierKey) ents = this.groups.getEntsByNameInverse(key); else ents = this.groups.getEntsByName(key); this.reset(); this.addList(ents); }; /** * Get a list of the template names */ EntitySelection.prototype.getTemplateNames = function() { var templateNames = []; for each (var ent in this.selected) { var entState = GetEntityState(ent); if (entState) templateNames.push(entState.template); } return templateNames; }; /** * Update the selection to take care of changes (like units that have been killed) */ EntitySelection.prototype.update = function() { - var changed = false; this.checkRenamedEntities(); - var removeOwnerChanges = !g_DevSettings.controlAll && this.toList().length > 1; - var playerID = Engine.GetPlayerID(); + + let changed = false; + let removeOwnerChanges = g_ViewedPlayer != -1 && !g_DevSettings.controlAll && this.toList().length > 1; + for each (var ent in this.selected) { var entState = GetEntityState(ent); // Remove deleted units if (!entState) { delete this.selected[ent]; this.groups.removeEnt(ent); changed = true; continue; } // Remove non-visible units (e.g. moved back into fog-of-war) // At the next update, mirages will be renamed to the real // entity they replace, so just ignore them now // Futhermore, when multiple selection, remove units which have changed ownership - if ((entState.visibility == "hidden" && !entState.mirage) - || (removeOwnerChanges && entState.player != playerID)) + if (entState.visibility == "hidden" && !entState.mirage || + removeOwnerChanges && entState.player != g_ViewedPlayer) { // Disable any highlighting of the disappeared unit _setHighlight([ent], 0, false); _setStatusBars([ent], false); _setMotionOverlay([ent], false); delete this.selected[ent]; this.groups.removeEnt(ent); changed = true; continue; } } if (changed) this.onChange(); }; /** * Update selection if some selected entities were renamed * (in case of unit promotion or finishing building structure) */ EntitySelection.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for each (var renamedEntity in renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; // Reconstruct the selection if at least one entity has been renamed. for each (var renamedEntity in renamedEntities) { if (this.selected[renamedEntity.entity]) { this.rebuildSelection(renamedLookup); break; } } } }; /** * Add entities to selection. Play selection sound unless quiet is true */ EntitySelection.prototype.addList = function(ents, quiet) { var selection = this.toList(); - var playerID = Engine.GetPlayerID(); // If someone else's player is the sole selected unit, don't allow adding to the selection - if (!g_DevSettings.controlAll && selection.length == 1) + if (g_ViewedPlayer != -1 && !g_DevSettings.controlAll && selection.length == 1) { var firstEntState = GetEntityState(selection[0]); - if (firstEntState && firstEntState.player != playerID) + if (firstEntState && firstEntState.player != g_ViewedPlayer) return; } // Allow selecting things not belong to this player (enemy, ally, gaia) var allowUnownedSelect = g_DevSettings.controlAll || (ents.length == 1 && selection.length == 0); var i = 1; var added = []; for each (var ent in ents) { - // Only add entities we own to our selection + if (selection.length + i > g_MaxSelectionSize) + break; + + if (this.selected[ent]) + continue; + var entState = GetEntityState(ent); - if (!this.selected[ent] && (selection.length + i) <= g_MaxSelectionSize && entState && (allowUnownedSelect || entState.player == playerID)) - { - added.push(ent); - this.selected[ent] = ent; - i++; - } + if (!entState) + continue; + + // For players, only add entities we own to our selection + if (g_ViewedPlayer != -1 && entState.player != g_ViewedPlayer && !allowUnownedSelect) + continue; + + // For observers, select units owned by players except gaia + if (g_ViewedPlayer == -1 && entState.player == 0) + continue; + + added.push(ent); + this.selected[ent] = ent; + ++i; } _setHighlight(added, 1, true); _setStatusBars(added, true); _setMotionOverlay(added, this.motionDebugOverlay); if (added.length) { // Play the sound if the entity is controllable by us or Gaia-owned. var owner = GetEntityState(added[0]).player; - if (!quiet && (owner == playerID || owner == 0 || g_DevSettings.controlAll)) + if (!quiet && (owner == g_ViewedPlayer || owner == 0 || g_DevSettings.controlAll)) _playSound(added[0]); } this.groups.add(this.toList()); // Create Selection Groups this.onChange(); }; EntitySelection.prototype.removeList = function(ents) { var removed = []; for each (var ent in ents) { if (this.selected[ent]) { this.groups.removeEnt(ent); removed.push(ent); delete this.selected[ent]; } } _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setMotionOverlay(removed, false); this.onChange(); }; EntitySelection.prototype.reset = function() { _setHighlight(this.toList(), 0, false); _setStatusBars(this.toList(), false); _setMotionOverlay(this.toList(), false); this.selected = {}; this.groups.reset(); this.onChange(); }; EntitySelection.prototype.rebuildSelection = function(renamed) { var oldSelection = this.selected; this.reset(); var toAdd = []; for each (var ent in oldSelection) toAdd.push(renamed[ent] ? renamed[ent] : ent); this.addList(toAdd, true); // don't play selection sounds }; EntitySelection.prototype.getFirstSelected = function() { for each (var ent in this.selected) return ent; return undefined; }; EntitySelection.prototype.toList = function() { var ents = []; for each (var ent in this.selected) ents.push(ent); return ents; }; EntitySelection.prototype.setHighlightList = function(ents) { var highlighted = {}; for each (var ent in ents) highlighted[ent] = ent; var removed = []; var added = []; // Remove highlighting for the old units that are no longer highlighted // (excluding ones that are actively selected too) for each (var ent in this.highlighted) if (!highlighted[ent] && !this.selected[ent]) removed.push(+ent); // Add new highlighting for units that aren't already highlighted for each (var ent in ents) if (!this.highlighted[ent] && !this.selected[ent]) added.push(+ent); _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setHighlight(added, g_HighlightedAlpha, true); _setStatusBars(added, true); // Store the new highlight list this.highlighted = highlighted; }; EntitySelection.prototype.SetMotionDebugOverlay = function(enabled) { this.motionDebugOverlay = enabled; _setMotionOverlay(this.toList(), enabled); }; EntitySelection.prototype.onChange = function() { this.dirty = true; if (this.isSelection) onSelectionChange(); }; /** * Cache some quantities which depends only on selection */ var g_Selection = new EntitySelection(); g_Selection.isSelection = true; var g_canMoveIntoFormation = {}; var g_allBuildableEntities = undefined; var g_allTrainableEntities = undefined; // Reset cached quantities function onSelectionChange() { g_canMoveIntoFormation = {}; g_allBuildableEntities = undefined; g_allTrainableEntities = undefined; } /** * EntityGroupsContainer class for managing grouped entities */ function EntityGroupsContainer() { this.groups = []; for (var i = 0; i < 10; ++i) this.groups[i] = new EntityGroups(); } EntityGroupsContainer.prototype.addEntities = function(groupName, ents) { for each (var ent in ents) for each (var group in this.groups) if (ent in group.ents) group.removeEnt(ent); this.groups[groupName].add(ents); }; EntityGroupsContainer.prototype.update = function() { this.checkRenamedEntities(); for each (var group in this.groups) { for (var ent in group.ents) { var entState = GetEntityState(+ent); // Remove deleted units if (!entState) group.removeEnt(ent); } } }; /** * Update control group if some entities in the group were renamed * (in case of unit promotion or finishing building structure) */ EntityGroupsContainer.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for each (var renamedEntity in renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; for each (var group in this.groups) { for each (var renamedEntity in renamedEntities) { // Reconstruct the group if at least one entity has been renamed. if (renamedEntity.entity in group.ents) { group.rebuildGroup(renamedLookup); break; } } } } }; var g_Groups = new EntityGroupsContainer(); Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 17703) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 17704) @@ -1,1171 +1,1169 @@ /** * Contains the layout and button settings per selection panel * * getItems returns a list of basic items used to fill the panel. * This method is obligated. If the items list is empty, the panel * won't be rendered. * * Then there's a loop over all items provided. In the loop, * the item and some other standard data is added to a data object. * * The standard data is * var data = { * "i": index * "item": item coming from the getItems function * "selection": list of currently selected items * "playerState": playerState * "unitEntState": first selected entity state * "rowLength": rowLength * "numberOfItems": number of items that will be processed * "button": gui Button object * "icon": gui Icon object * "guiSelection": gui button Selection overlay * "countDisplay": gui caption space * }; * * Then, addData is called, and can be used to abort the processing * of the current item by returning false. * It should return true if you want the panel to be filled. * * addData is used to add data to the data object on top * (or instead of) the standard data. * addData is not obligated, the function will just continue * with the content setters if no addData is present. * * After the addData, all functions starting with "set" are called. * These are used to set various parts of content. */ /* cache some formation info */ var g_availableFormations = new Map(); // available formations per player var g_formationsInfo = new Map(); var g_SelectionPanels = {}; // ALERT g_SelectionPanels.Alert = { "getMaxNumberOfItems": function() { return 2; }, "getItems": function(unitEntState) { if (!unitEntState.alertRaiser) return []; return ["increase", "end"]; }, "setAction": function(data) { data.button.onPress = function() { if (data.item == "increase") increaseAlertLevel(); else if (data.item == "end") endOfAlert(); }; }, "setTooltip": function(data) { if (data.item == "increase") { if (data.unitEntState.alertRaiser.hasRaisedAlert) data.button.tooltip = translate("Increase the alert level to protect more units"); else data.button.tooltip = translate("Raise an alert!"); } else if (data.item == "end") data.button.tooltip = translate("End of alert."); }, "setGraphics": function(data) { if (data.item == "increase") { data.button.hidden = !data.unitEntState.alertRaiser.canIncreaseLevel; if (data.unitEntState.alertRaiser.hasRaisedAlert) data.icon.sprite = "stretched:session/icons/bell_level2.png"; else data.icon.sprite = "stretched:session/icons/bell_level1.png"; } else if (data.item == "end") { data.button.hidden = !data.unitEntState.alertRaiser.hasRaisedAlert; data.icon.sprite = "stretched:session/icons/bell_level0.png"; } data.button.enabled = !data.button.hidden && controlsPlayer(data.unitEntState.player); } }; // BARTER g_SelectionPanels.Barter = { "getMaxNumberOfItems": function() { return 4; }, "rowLength": 4, "getItems": function(unitEntState, selection) { if (!unitEntState.barterMarket) return []; // ["food", "wood", "stone", "metal"] return BARTER_RESOURCES; }, "addData": function(data) { // data.item is the resource name in this case data.button = {}; data.icon = {}; data.amount = {}; for (var a of BARTER_ACTIONS) { data.button[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Button["+data.i+"]"); data.icon[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Icon["+data.i+"]"); data.amount[a] = Engine.GetGUIObjectByName("unitBarter"+a+"Amount["+data.i+"]"); } data.selectionIcon = Engine.GetGUIObjectByName("unitBarterSellSelection["+data.i+"]"); data.amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL; if (Engine.HotkeyIsPressed("session.massbarter")) data.amountToSell *= BARTER_BUNCH_MULTIPLIER; data.isSelected = data.item == g_barterSell; return true; }, "setCountDisplay": function(data) { data.amount.Sell.caption = "-" + data.amountToSell; var sellPrice = data.unitEntState.barterMarket.prices.sell[g_barterSell]; var buyPrice = data.unitEntState.barterMarket.prices.buy[data.item]; data.amount.Buy.caption = "+" + Math.round(sellPrice / buyPrice * data.amountToSell); }, "setTooltip": function(data) { var resource = getLocalizedResourceName(data.item, "withinSentence"); data.button.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource }); data.button.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource }); }, "setAction": function(data) { data.button.Sell.onPress = function() { g_barterSell = data.item; }; var exchangeResourcesParameters = { "sell": g_barterSell, "buy": data.item, "amount": data.amountToSell }; data.button.Buy.onPress = function() { exchangeResources(exchangeResourcesParameters); }; }, "setGraphics": function(data) { var grayscale = data.isSelected ? "color: 0 0 0 100:grayscale:" : ""; // do we have enough of this resource to sell? var neededRes = {}; neededRes[data.item] = data.amountToSell; var canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", { "cost": neededRes, "player": data.unitEntState.player }) ? "color:255 0 0 80:" : ""; // Let's see if we have enough resources to barter. neededRes = {}; neededRes[g_barterSell] = data.amountToSell; var canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", { "cost": neededRes, "player": data.unitEntState.player }) ? "color:255 0 0 80:" : ""; data.icon.Sell.sprite = canSellCurrent + "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png"; data.icon.Buy.sprite = canBuyAny + "stretched:"+grayscale+"session/icons/resources/" + data.item + ".png"; data.button.Buy.hidden = data.isSelected; data.button.Buy.enabled = controlsPlayer(data.unitEntState.player); data.button.Sell.hidden = false; data.selectionIcon.hidden = !data.isSelected; }, "setPosition": function(data) { setPanelObjectPosition(data.button.Sell, data.i, data.rowLength); setPanelObjectPosition(data.button.Buy, data.i + data.rowLength, data.rowLength); } }; // COMMAND g_SelectionPanels.Command = { "getMaxNumberOfItems": function() { return 6; }, "getItems": function(unitEntState) { var commands = []; for (var c in g_EntityCommands) { var info = g_EntityCommands[c].getInfo(unitEntState); if (!info) continue; info.name = c; commands.push(info); } return commands; }, "setTooltip": function(data) { data.button.tooltip = data.item.tooltip; }, "setAction": function(data) { data.button.onPress = function() { if (data.item.callback) data.item.callback(data.item); else performCommand(data.unitEntState.id, data.item.name); }; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.item.count || ""; }, "setGraphics": function(data) { data.icon.sprite = "stretched:session/icons/" + data.item.icon; data.button.enabled = controlsPlayer(data.unitEntState.player); }, "setPosition": function(data) { var size = data.button.size; // count on square buttons, so size.bottom is the width too var spacer = size.bottom + 1; // relative to the center ( = 50%) size.rleft = size.rright = 50; // offset from the center calculation size.left = (data.i - data.numberOfItems/2) * spacer; size.right = size.left + size.bottom; data.button.size = size; } }; //ALLY COMMAND g_SelectionPanels.AllyCommand = { "getMaxNumberOfItems": function() { return 2; }, "getItems": function(unitEntState) { var commands = []; for (var c in g_AllyEntityCommands) { var info = g_AllyEntityCommands[c].getInfo(unitEntState); if (!info) continue; info.name = c; commands.push(info); } return commands; }, "setTooltip": function(data) { data.button.tooltip = data.item.tooltip; }, "setAction": function(data) { data.button.onPress = function() { if (data.item.callback) data.item.callback(data.item); else performAllyCommand(data.unitEntState.id, data.item.name); }; }, "conflictsWith": ["Command"], "setCountDisplay": function(data) { data.countDisplay.caption = data.item.count || ""; }, "setGraphics": function(data) { data.icon.sprite = "stretched:session/icons/" + data.item.icon; data.button.enabled = data.item.count > 0; }, "setPosition": function(data) { var size = data.button.size; // count on square buttons, so size.bottom is the width too var spacer = size.bottom + 1; // relative to the center ( = 50%) size.rleft = size.rright = 50; // offset from the center calculation size.left = (data.i - data.numberOfItems/2) * spacer; size.right = size.left + size.bottom; data.button.size = size; } }; // CONSTRUCTION g_SelectionPanels.Construction = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function() { return getAllBuildableEntitiesFromSelection(); }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) // abort if no template return false; data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.template.requiredTechnology, "player": data.unitEntState.player }); if (data.template.cost) data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.template, 1), "player": data.unitEntState.player }); data.limits = getEntityLimitAndCount(data.playerState, data.entType); return true; }, "setAction": function(data) { data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); }; }, "setTooltip": function(data) { var tooltip = getEntityNamesFormatted(data.template); tooltip += getVisibleEntityClassesFormatted(data.template); tooltip += getAurasTooltip(data.template); if (data.template.tooltip) tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(data.template); tooltip += getPopulationBonusTooltip(data.template); tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers); if (!data.technologyEnabled) tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { "technology": getEntityNames(GetTechnologyData(data.template.requiredTechnology)) }); if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); data.button.tooltip = tooltip; return true; }, "setGraphics": function(data) { var modifier = ""; if (!data.technologyEnabled || data.limits.canBeAddedCount == 0) { data.button.enabled = false; modifier += "color: 0 0 0 127:"; modifier += "grayscale:"; } else if (data.neededResources) { data.button.enabled = false; modifier += resourcesToAlphaMask(data.neededResources) +":"; } else data.button.enabled = controlsPlayer(data.unitEntState.player); if (data.template.icon) data.icon.sprite = modifier + "stretched:session/portraits/" + data.template.icon; }, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; // FORMATION g_SelectionPanels.Formation = { "getMaxNumberOfItems": function() { return 16; }, "rowLength": 4, "conflictsWith": ["Garrison"], "getItems": function(unitEntState) { if (!hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal")) return []; if (!g_availableFormations.has(unitEntState.player)) g_availableFormations.set(unitEntState.player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntState.player)); return g_availableFormations.get(unitEntState.player); }, "addData": function(data) { if (!g_formationsInfo.has(data.item)) g_formationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); data.formationInfo = g_formationsInfo.get(data.item); data.formationOk = canMoveSelectionIntoFormation(data.item); data.formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": data.selection, "formationTemplate": data.item }); return true; }, "setAction": function(data) { data.button.onPress = function() { performFormation(data.unitEntState.id, data.item); }; }, "setTooltip": function(data) { var tooltip = translate(data.formationInfo.name); if (!data.formationOk && data.formationInfo.tooltip) tooltip += "\n" + "[color=\"red\"]" + translate(data.formationInfo.tooltip) + "[/color]"; data.button.tooltip = tooltip; }, "setGraphics": function(data) { data.button.enabled = data.formationOk && controlsPlayer(data.unitEntState.player); var grayscale = data.formationOk ? "" : "grayscale:"; data.guiSelection.hidden = !data.formationSelected; data.icon.sprite = "stretched:"+grayscale+"session/icons/"+data.formationInfo.icon; } }; // GARRISON g_SelectionPanels.Garrison = { "getMaxNumberOfItems": function() { return 12; }, "rowLength": 4, "getItems": function(unitEntState, selection) { if (!unitEntState.garrisonHolder) return []; var groups = new EntityGroups(); for (var ent of selection) { var state = GetEntityState(ent); if (state.garrisonHolder) groups.add(state.garrisonHolder.entities); } return groups.getEntsGrouped(); }, "addData": function(data) { data.entType = data.item.template; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.name = getEntityNames(data.template); data.count = data.item.ents.length; return true; }, "setAction": function(data) { data.button.onPress = function() { unloadTemplate(data.item.template); }; }, "setTooltip": function(data) { var tooltip = sprintf(translate("Unload %(name)s"), { "name": data.name }) + "\n"; tooltip += translate("Single-click to unload 1. Shift-click to unload all of this type."); data.button.tooltip = tooltip; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.count || ""; }, "setGraphics": function(data) { var grayscale = ""; var ents = data.item.ents; var entplayer = GetEntityState(ents[0]).player; data.button.sprite = "color:" + rgbToGuiColor(g_Players[entplayer].color) +":"; if (!controlsPlayer(data.unitEntState.player) && !controlsPlayer(entplayer)) { data.button.enabled = false; grayscale = "grayscale:"; } data.icon.sprite = "stretched:" + grayscale + "session/portraits/" + data.template.icon; } }; // GATE g_SelectionPanels.Gate = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function(unitEntState, selection) { // Allow long wall pieces to be converted to gates var longWallTypes = {}; var walls = []; var gates = []; for (var ent of selection) { var state = GetEntityState(ent); if (hasClass(state, "LongWall") && !state.gate && !longWallTypes[state.template]) { var gateTemplate = getWallGateTemplate(state.id); if (gateTemplate) { var tooltipString = GetTemplateDataWithoutLocalization(state.template).gateConversionTooltip; if (!tooltipString) { warn(state.template + " is supposed to be convertable to a gate, but it's missing the GateConversionTooltip in the Identity template"); tooltipString = ""; } walls.push({ "tooltip": translate(tooltipString), "template": gateTemplate, "callback": function (item) { transformWallToGate(item.template); } }); } // We only need one entity per type. longWallTypes[state.template] = true; } else if (state.gate && !gates.length) { gates.push({ "gate": state.gate, "tooltip": translate("Lock Gate"), "locked": true, "callback": function (item) { lockGate(item.locked); } }); gates.push({ "gate": state.gate, "tooltip": translate("Unlock Gate"), "locked": false, "callback": function (item) { lockGate(item.locked); } }); } // Show both 'locked' and 'unlocked' as active if the selected gates have both lock states. else if (state.gate && state.gate.locked != gates[0].gate.locked) for (var j = 0; j < gates.length; ++j) delete gates[j].gate.locked; } // Place wall conversion options after gate lock/unlock icons. var items = gates.concat(walls); return items; }, "setAction": function(data) { data.button.onPress = function() {data.item.callback(data.item); }; }, "setTooltip": function(data) { var tooltip = data.item.tooltip; if (data.item.template) { data.template = GetTemplateData(data.item.template); data.wallCount = data.selection.reduce(function (count, ent) { var state = GetEntityState(ent); if (hasClass(state, "LongWall") && !state.gate) ++count; return count; }, 0); tooltip += "\n" + getEntityCostTooltip(data.template, data.wallCount); data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.template, data.wallCount) }); if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); } data.button.tooltip = tooltip; }, "setGraphics": function(data) { data.button.enabled = controlsPlayer(data.unitEntState.player); var gateIcon; if (data.item.gate) { // If already a gate, show locking actions gateIcon = "icons/lock_" + GATE_ACTIONS[data.item.locked ? 0 : 1] + "ed.png"; if (data.item.gate.locked === undefined) data.guiSelection.hidden = false; else data.guiSelection.hidden = data.item.gate.locked != data.item.locked; } else { // otherwise show gate upgrade icon var template = GetTemplateData(data.item.template); if (!template) return; gateIcon = data.template.icon ? "portraits/" + data.template.icon : "icons/gate_closed.png"; data.guiSelection.hidden = true; } data.icon.sprite = (data.neededResources ? resourcesToAlphaMask(data.neededResources) + ":" : "") + "stretched:session/" + gateIcon; }, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; // PACK g_SelectionPanels.Pack = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function(unitEntState, selection) { var checks = {}; for (var ent of selection) { var state = GetEntityState(ent); if (!state.pack) continue; if (state.pack.progress == 0) { if (!state.pack.packed) checks.packButton = true; else if (state.pack.packed) checks.unpackButton = true; } else { // Already un/packing - show cancel button if (!state.pack.packed) checks.packCancelButton = true; else if (state.pack.packed) checks.unpackCancelButton = true; } } var items = []; if (checks.packButton) items.push({ "packing": false, "packed": false, "tooltip": translate("Pack"), "callback": function() { packUnit(true); } }); if (checks.unpackButton) items.push({ "packing": false, "packed": true, "tooltip": translate("Unpack"), "callback": function() { packUnit(false); } }); if (checks.packCancelButton) items.push({ "packing": true, "packed": false, "tooltip": translate("Cancel Packing"), "callback": function() { cancelPackUnit(true); } }); if (checks.unpackCancelButton) items.push({ "packing": true, "packed": true, "tooltip": translate("Cancel Unpacking"), "callback": function() { cancelPackUnit(false); } }); return items; }, "setAction": function(data) { data.button.onPress = function() {data.item.callback(data.item); }; }, "setTooltip": function(data) { data.button.tooltip = data.item.tooltip; }, "setGraphics": function(data) { if (data.item.packing) data.icon.sprite = "stretched:session/icons/cancel.png"; else if (data.item.packed) data.icon.sprite = "stretched:session/icons/unpack.png"; else data.icon.sprite = "stretched:session/icons/pack.png"; data.button.enabled = controlsPlayer(data.unitEntState.player); }, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; // QUEUE g_SelectionPanels.Queue = { "getMaxNumberOfItems": function() { return 16; }, "getItems": function(unitEntState, selection) { return getTrainingQueueItems(selection); }, "resizePanel": function(numberOfItems, rowLength) { var numRows = Math.ceil(numberOfItems / rowLength); var panel = Engine.GetGUIObjectByName("unitQueuePanel"); var size = panel.size; var buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom; var margin = 4; size.top = size.bottom - numRows*buttonSize - (numRows+2)*margin; panel.size = size; }, "addData": function(data) { // differentiate between units and techs if (data.item.unitTemplate) { data.entType = data.item.unitTemplate; data.template = GetTemplateData(data.entType); } else if (data.item.technologyTemplate) { data.entType = data.item.technologyTemplate; data.template = GetTechnologyData(data.entType); } data.progress = Math.round(data.item.progress*100) + "%"; return data.template; }, "setAction": function(data) { data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, data.item.id); }; }, "setTooltip": function(data) { var tooltip = getEntityNames(data.template); if (data.item.neededSlots) { tooltip += "\n[color=\"red\"]" + translate("Insufficient population capacity:") + "\n[/color]"; tooltip += sprintf(translate("%(population)s %(neededSlots)s"), { "population": getCostComponentDisplayName("population"), "neededSlots": data.item.neededSlots }); } data.button.tooltip = tooltip; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.item.count > 1 ? data.item.count : ""; }, "setProgressDisplay": function(data) { // show the progress number for the first item if (data.i == 0) Engine.GetGUIObjectByName("queueProgress").caption = data.progress; var guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider["+data.i+"]"); var size = guiObject.size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(data.item.progress * (size.right - size.left)); guiObject.size = size; }, "setGraphics": function(data) { if (data.template.icon) data.icon.sprite = "stretched:session/portraits/" + data.template.icon; data.button.enabled = controlsPlayer(data.unitEntState.player); } }; // RESEARCH g_SelectionPanels.Research = { "getMaxNumberOfItems": function() { return 8; }, "getItems": function(unitEntState, selection) { // TODO 8 is the row lenght, make variable if (getNumberOfRightPanelButtons() > 8 && selection.length > 1) return []; for (var ent of selection) { var entState = GetEntityState(ent); if (entState.production && entState.production.technologies.length) return entState.production.technologies; } return []; }, "hideItem": function(i, rowLength) // called when no item is found { Engine.GetGUIObjectByName("unitResearchButton["+i+"]").hidden = true; // We also remove the paired tech and the pair symbol Engine.GetGUIObjectByName("unitResearchButton["+(i+rowLength)+"]").hidden = true; Engine.GetGUIObjectByName("unitResearchPair["+i+"]").hidden = true; }, "addData": function(data) { data.entType = data.item.pair ? [data.item.top, data.item.bottom] : [data.item]; data.template = data.entType.map(GetTechnologyData); // abort if no template found for any of the techs if (data.template.some(v => !v)) return false; // index one row below var shiftedIndex = data.i + data.rowLength; data.positions = data.item.pair ? [data.i, shiftedIndex] : [shiftedIndex]; data.positionsToHide = data.item.pair ? [] : [data.i]; // add top buttons to the data data.button = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchButton["+p+"]")); data.buttonsToHide = data.positionsToHide.map(p => Engine.GetGUIObjectByName("unitResearchButton["+p+"]")); data.icon = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchIcon["+p+"]")); data.unchosenIcon = data.positions.map(p => Engine.GetGUIObjectByName("unitResearchUnchosenIcon["+p+"]")); data.neededResources = data.template.map(t => Engine.GuiInterfaceCall("GetNeededResources", { "cost": t.cost, "player": data.unitEntState.player })); data.requirementsPassed = data.entType.map(e => Engine.GuiInterfaceCall("CheckTechnologyRequirements", { "tech": e, "player": data.unitEntState.player })); data.pair = Engine.GetGUIObjectByName("unitResearchPair["+data.i+"]"); return true; }, "setTooltip": function(data) { for (var i in data.entType) { var tooltip = ""; var template = data.template[i]; tooltip = getEntityNamesFormatted(template); if (template.tooltip) tooltip += "\n[font=\"sans-13\"]" + template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(template); if (!data.requirementsPassed[i]) { tooltip += "\n" + template.requirementsTooltip; if (template.classRequirements) { var player = data.unitEntState.player; var current = GetSimState().players[player].classCounts[template.classRequirements.class] || 0; var remaining = template.classRequirements.number - current; tooltip += " " + sprintf(translatePlural("Remaining: %(number)s to build.", "Remaining: %(number)s to build.", remaining), { "number": remaining }); } } if (data.neededResources[i]) tooltip += getNeededResourcesTooltip(data.neededResources[i]); data.button[i].tooltip = tooltip; } }, "setAction": function(data) { for (var i in data.entType) { // array containing the indices other buttons var others = Object.keys(data.template); others.splice(i, 1); var button = data.button[i]; // as we're in a loop, we need to limit the scope with a closure // else the last value of the loop will be taken, rather than the current one button.onpress = (function(template) { return function () { addResearchToQueue(data.unitEntState.id, template); }; })(data.entType[i]); // on mouse enter, show a cross over the other icons button.onmouseenter = (function(others, icons) { return function() { for (var j of others) icons[j].hidden = false; }; })(others, data.unchosenIcon); button.onmouseleave = (function(others, icons) { return function() { for (var j of others) icons[j].hidden = true; }; })(others, data.unchosenIcon); } }, "setGraphics": function(data) { for (var i in data.entType) { let button = data.button[i]; button.hidden = false; var modifier = ""; if (!data.requirementsPassed[i]) { button.enabled = false; modifier += "color: 0 0 0 127:"; modifier += "grayscale:"; } else if (data.neededResources[i]) { button.enabled = false; modifier += resourcesToAlphaMask(data.neededResources[i]) + ":"; } else button.enabled = controlsPlayer(data.unitEntState.player); if (data.template[i].icon) data.icon[i].sprite = modifier + "stretched:session/portraits/" + data.template[i].icon; } for (let button of data.buttonsToHide) button.hidden = true; // show the tech connector data.pair.hidden = data.item.pair == null; }, "setPosition": function(data) { for (var i in data.button) setPanelObjectPosition(data.button[i], data.positions[i], data.rowLength); setPanelObjectPosition(data.pair, data.i, data.rowLength); } }; // SELECTION g_SelectionPanels.Selection = { "getMaxNumberOfItems": function() { return 16; }, "rowLength": 4, "getItems": function(unitEntState, selection) { if (selection.length < 2) return []; return g_Selection.groups.getTemplateNames(); }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.name = getEntityNames(data.template); var ents = g_Selection.groups.getEntsByName(data.item); data.count = ents.length; for (var ent of ents) { var state = GetEntityState(ent); if (state.resourceCarrying && state.resourceCarrying.length !== 0) { if (!data.carried) data.carried = {}; var carrying = state.resourceCarrying[0]; if (data.carried[carrying.type]) data.carried[carrying.type] += carrying.amount; else data.carried[carrying.type] = carrying.amount; } if (state.trader && state.trader.goods && state.trader.goods.amount) { if (!data.carried) data.carried = {}; var amount = state.trader.goods.amount; var type = state.trader.goods.type; var totalGain = amount.traderGain; if (amount.market1Gain) totalGain += amount.market1Gain; if (amount.market2Gain) totalGain += amount.market2Gain; if (data.carried[type]) data.carried[type] += totalGain; else data.carried[type] = totalGain; } } return true; }, "setTooltip": function(data) { if (data.carried) { var str = data.name + "\n"; var ress = ["food", "wood", "stone", "metal"]; for (var i = 0; i < 4; ++i) { if (data.carried[ress[i]]) { str += getCostComponentDisplayName(ress[i]) + data.carried[ress[i]]; if (i !== 3) str += " "; } } data.button.tooltip = str; } else data.button.tooltip = data.name; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.count || ""; }, "setAction": function(data) { data.button.onpressright = function() { changePrimarySelectionGroup(data.item, true); }; data.button.onpress = function() { changePrimarySelectionGroup(data.item, false); }; }, "setGraphics": function(data) { if (data.template.icon) data.icon.sprite = "stretched:session/portraits/" + data.template.icon; - - data.button.enabled = controlsPlayer(data.unitEntState.player); } }; // STANCE g_SelectionPanels.Stance = { "getMaxNumberOfItems": function() { return 5; }, "getItems": function(unitEntState) { if (!unitEntState.unitAI || !hasClass(unitEntState, "Unit") || hasClass(unitEntState, "Animal")) return []; return unitEntState.unitAI.possibleStances; }, "addData": function(data) { data.stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", { "ents": data.selection, "stance": data.item }); return true; }, "setAction": function(data) { data.button.onPress = function() { performStance(data.unitEntState, data.item); }; }, "setTooltip": function(data) { data.button.tooltip = getStanceDisplayName(data.item) + "\n[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]"; }, "setGraphics": function(data) { data.guiSelection.hidden = !data.stanceSelected; data.icon.sprite = "stretched:session/icons/stances/"+data.item+".png"; data.button.enabled = controlsPlayer(data.unitEntState.player); } }; // TRAINING g_SelectionPanels.Training = { "getMaxNumberOfItems": function() { return 24 - getNumberOfRightPanelButtons(); }, "getItems": function() { return getAllTrainableEntitiesFromSelection(); }, "addData": function(data) { data.entType = data.item; data.template = GetTemplateData(data.entType); if (!data.template) return false; data.technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.template.requiredTechnology, "player": data.unitEntState.player }); var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingBatchStatus(data.playerState, data.unitEntState.id, data.entType, data.selection); data.buildingsCountToTrainFullBatch = buildingsCountToTrainFullBatch; data.fullBatchSize = fullBatchSize; data.remainderBatch = remainderBatch; data.trainNum = buildingsCountToTrainFullBatch || 1; // train at least one unit if (Engine.HotkeyIsPressed("session.batchtrain")) data.trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; if (data.template.cost) data.neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.template, data.trainNum), "player": data.unitEntState.player }); return true; }, "setAction": function(data) { data.button.onPress = function() { addTrainingToQueue(data.selection, data.item, data.playerState); }; }, "setCountDisplay": function(data) { data.countDisplay.caption = data.trainNum > 1 ? data.trainNum : ""; }, "setTooltip": function(data) { var tooltip = ""; var key = Engine.ConfigDB_GetValue("user", "hotkey.session.queueunit." + (data.i + 1)); if (key) tooltip += "[color=\"255 251 131\"][font=\"sans-bold-16\"]\\[" + key + "][/font][/color] "; tooltip += getEntityNamesFormatted(data.template); tooltip += getVisibleEntityClassesFormatted(data.template); tooltip += getAurasTooltip(data.template); if (data.template.tooltip) tooltip += "\n[font=\"sans-13\"]" + data.template.tooltip + "[/font]"; tooltip += "\n" + getEntityCostTooltip(data.template, data.trainNum, data.unitEntState.id); data.limits = getEntityLimitAndCount(data.playerState, data.entType); tooltip += formatLimitString(data.limits.entLimit, data.limits.entCount, data.limits.entLimitChangers); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") { if (data.template.health) tooltip += "\n[font=\"sans-bold-13\"]" + translate("Health:") + "[/font] " + data.template.health; if (data.template.attack) tooltip += "\n" + getAttackTooltip(data.template); if (data.template.armour) tooltip += "\n" + getArmorTooltip(data.template.armour); if (data.template.speed) tooltip += "\n" + getSpeedTooltip(data.template); } tooltip += "[color=\"255 251 131\"]" + formatBatchTrainingString(data.buildingsCountToTrainFullBatch, data.fullBatchSize, data.remainderBatch) + "[/color]"; if (!data.technologyEnabled) { var techName = getEntityNames(GetTechnologyData(data.template.requiredTechnology)); tooltip += "\n" + sprintf(translate("Requires %(technology)s"), { "technology": techName }); } if (data.neededResources) tooltip += getNeededResourcesTooltip(data.neededResources); data.button.tooltip = tooltip; }, // disable and enable buttons in the same way as when you do for the construction "setGraphics": g_SelectionPanels.Construction.setGraphics, "setPosition": function(data) { var index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); } }; /** * If two panels need the same space, so they collide, * the one appearing first in the order is rendered. * * Note that the panel needs to appear in the list to get rendered. */ var g_PanelsOrder = [ // LEFT PANE "Barter", // must always be visible on markets "Garrison", // more important than Formation, as you want to see the garrisoned units in ships "Alert", "Formation", "Stance", // normal together with formation // RIGHT PANE "Gate", // must always be shown on gates "Pack", // must always be shown on packable entities "Training", "Construction", "Research", // normal together with training // UNIQUE PANES (importance doesn't matter) "Command", "AllyCommand", "Queue", "Selection", ];