Index: ps/trunk/binaries/data/mods/public/gui/session/selection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 25947) +++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 25948) @@ -1,573 +1,578 @@ // Limits selection size var g_MaxSelectionSize = 200; // Alpha value of hovered/mouseover/highlighted selection overlays // (should probably be greater than always visible alpha value, // see CCmpSelectable) var 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) return; Engine.GuiInterfaceCall("SetStatusBars", { "entities": ents, "enabled": enabled, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } 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 (let ent of ents) { if (this.ents[ent]) continue; 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; // Group the ents by player and template if (entState.player !== undefined) key = "p" + entState.player + "&" + key; if (this.groups[key]) this.groups[key] += 1; else this.groups[key] = 1; this.ents[ent] = key; } }; EntityGroups.prototype.removeEnt = function(ent) { var key = this.ents[ent]; // Remove the entity delete this.ents[ent]; --this.groups[key]; // Remove the entire group if (this.groups[key] == 0) delete this.groups[key]; }; 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(key) { return this.groups[key]; }; EntityGroups.prototype.getTotalCount = function() { let totalCount = 0; for (let key in this.groups) totalCount += this.groups[key]; return totalCount; }; EntityGroups.prototype.getKeys = function() { // Preserve order even when shuffling units around // Can be optimized by moving the sorting elsewhere return Object.keys(this.groups).sort(); }; EntityGroups.prototype.getEntsByKey = function(key) { var ents = []; for (var ent in this.ents) if (this.ents[ent] == key) ents.push(+ent); return ents; }; /** * get a list of entities grouped by a key */ EntityGroups.prototype.getEntsGrouped = function() { return this.getKeys().map(key => ({ "ents": this.getEntsByKey(key), "key": key })); }; /** * Gets all ents in every group except ones of the specified group */ EntityGroups.prototype.getEntsByKeyInverse = function(key) { var ents = []; for (var ent in this.ents) if (this.ents[ent] != key) ents.push(+ent); return ents; }; /** * EntitySelection class for managing the entity selection list and the primary selection */ function EntitySelection() { // Private properties: this.selected = new Set(); // For mouseover-highlighted entity IDs in these. this.highlighted = new Set(); 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 inverse is true otherwise deselect just the chosen entity + * Deselect everything but entities of the chosen type. */ -EntitySelection.prototype.makePrimarySelection = function(key, inverse) +EntitySelection.prototype.makePrimarySelection = function(key) { - let ents = inverse ? - this.groups.getEntsByKeyInverse(key) : - this.groups.getEntsByKey(key); - + const ents = this.groups.getEntsByKey(key); this.reset(); this.addList(ents); }; /** + * Deselect entities of the chosen type. + */ +EntitySelection.prototype.removeGroupFromSelection = function(key) +{ + this.removeList(this.groups.getEntsByKey(key)); +}; + +/** * Get a list of the template names */ EntitySelection.prototype.getTemplateNames = function() { const templateNames = []; for (const ent of this.selected) { const 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() { this.checkRenamedEntities(); const controlsAll = g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll; const removeOwnerChanges = !g_IsObserver && !controlsAll && this.selected.size > 1; let changed = false; for (const ent of this.selected) { const entState = GetEntityState(ent); if (!entState) { this.selected.delete(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 != g_ViewedPlayer) { // Disable any highlighting of the disappeared unit _setHighlight([ent], 0, false); _setStatusBars([ent], false); _setMotionOverlay([ent], false); this.selected.delete(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 (let renamedEntity of renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; // Reconstruct the selection if at least one entity has been renamed. for (let renamedEntity of renamedEntities) if (this.selected.has(renamedEntity.entity)) { this.rebuildSelection(renamedLookup); return; } } }; /** * Add entities to selection. Play selection sound unless quiet is true */ EntitySelection.prototype.addList = function(ents, quiet, force = false) { // If someone else's player is the sole selected unit, don't allow adding to the selection. const firstEntState = this.selected.size == 1 && GetEntityState(this.getFirstSelected()); if (firstEntState && firstEntState.player != g_ViewedPlayer && !force) return; const added = []; for (const ent of this.addFormationMembers(ents)) { if (this.selected.size >= g_MaxSelectionSize) break; if (this.selected.has(ent)) continue; const entState = GetEntityState(ent); if (!entState) continue; let isUnowned = g_ViewedPlayer != -1 && entState.player != g_ViewedPlayer || g_ViewedPlayer == -1 && entState.player == 0; // Don't add unowned entities to the list, unless a single entity was selected if (isUnowned && (ents.length > 1 || this.selected.size) && !force) continue; added.push(ent); this.selected.add(ent); } _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 && (controlsPlayer(owner) || g_IsObserver || owner == 0)) _playSound(added[0]); } this.groups.add(this.toList()); // Create Selection Groups this.onChange(); }; /** * @param {number[]} ents - The entities to remove. * @param {boolean} dontAddFormationMembers - If true we need to exclude adding formation members. */ EntitySelection.prototype.removeList = function(ents, dontAddFormationMembers = false) { const removed = []; for (const ent of dontAddFormationMembers ? ents : this.addFormationMembers(ents)) if (this.selected.has(ent)) { this.groups.removeEnt(ent); removed.push(ent); this.selected.delete(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.clear(); this.groups.reset(); this.onChange(); }; EntitySelection.prototype.rebuildSelection = function(renamed) { const toAdd = []; for (const ent of this.selected) toAdd.push(renamed[ent] || ent); this.reset(); this.addList(toAdd, true); // don't play selection sounds }; EntitySelection.prototype.getFirstSelected = function() { for (const ent of this.selected) return ent; return undefined; }; /** * TODO: This array should not be recreated every call */ EntitySelection.prototype.toList = function() { return Array.from(this.selected); }; /** * @return {number} - The number of entities selected. */ EntitySelection.prototype.size = function() { return this.selected.size; }; EntitySelection.prototype.find = function(condition) { for (const ent of this.selected) if (condition(ent)) return ent; return null; }; /** * @param {function} condition - A function. * @return {number[]} - The entities passing the condition. */ EntitySelection.prototype.filter = function(condition) { const result = []; for (const ent of this.selected) if (condition(ent)) result.push(ent); return result; }; EntitySelection.prototype.setHighlightList = function(entities) { const highlighted = new Set(); const ents = this.addFormationMembers(entities); for (const ent of ents) highlighted.add(ent); const removed = []; const added = []; // Remove highlighting for the old units that are no longer highlighted // (excluding ones that are actively selected too). for (const ent of this.highlighted) if (!highlighted.has(ent) && !this.selected.has(ent)) removed.push(ent); // Add new highlighting for units that aren't already highlighted. for (const ent of ents) if (!this.highlighted.has(ent) && !this.selected.has(ent)) added.push(ent); _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setHighlight(added, g_HighlightedAlpha, true); _setStatusBars(added, true); 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(); }; EntitySelection.prototype.selectAndMoveTo = function(entityID) { let entState = GetEntityState(entityID); if (!entState || !entState.position) return; this.reset(); this.addList([entityID]); Engine.CameraMoveTo(entState.position.x, entState.position.z); } /** * Adds the formation members of a selected entities to the selection. * @param {number[]} entities - The entity IDs of selected entities. * @return {number[]} - Some more entity IDs if part of a formation was selected. */ EntitySelection.prototype.addFormationMembers = function(entities) { if (!entities.length || Engine.HotkeyIsPressed("selection.singleselection")) return entities; const result = new Set(entities); for (const entity of entities) { const entState = GetEntityState(+entity); if (entState?.unitAI?.formation) for (const member of GetEntityState(+entState.unitAI.formation).formation.members) result.add(member); } return result; }; /** * Cache some quantities which depends only on selection */ var g_Selection = new EntitySelection(); g_Selection.isSelection = true; var g_canMoveIntoFormation = {}; var g_allBuildableEntities; var g_allTrainableEntities; // 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(); } /** * Add entities to a group. * @param {string} groupName - The number of the group to add the entities to. * @param {number[]} ents - The entities to add to the group. */ EntityGroupsContainer.prototype.addEntities = function(groupName, ents) { if (Engine.ConfigDB_GetValue("user", "gui.session.disjointcontrolgroups") == "true") for (let ent of ents) for (let group of this.groups) if (ent in group.ents) group.removeEnt(ent); this.groups[groupName].add(ents); }; EntityGroupsContainer.prototype.update = function() { this.checkRenamedEntities(); for (let group of 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 (let renamedEntity of renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; for (let group of this.groups) for (let renamedEntity of 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 25947) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 25948) @@ -1,1265 +1,1270 @@ /** * 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 * { * "i": index * "item": item coming from the getItems function * "playerState": playerState * "unitEntStates": states of the selected entities * "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 for every data object, the setupButton function is called which * sets the view and handlers of the button. */ // Cache some formation info // Available formations per player var g_AvailableFormations = new Map(); var g_FormationsInfo = new Map(); var g_SelectionPanels = {}; var g_SelectionPanelBarterButtonManager; g_SelectionPanels.Alert = { "getMaxNumberOfItems": function() { return 2; }, "getItems": function(unitEntStates) { return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : []; }, "setupButton": function(data) { data.button.onPress = function() { switch (data.item) { case "raise": raiseAlert(); return; case "end": endOfAlert(); return; } }; switch (data.item) { case "raise": data.icon.sprite = "stretched:session/icons/bell_level1.png"; data.button.tooltip = translate("Raise an alert!"); break; case "end": data.button.tooltip = translate("End of alert."); data.icon.sprite = "stretched:session/icons/bell_level0.png"; break; } data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength); return true; } }; g_SelectionPanels.Barter = { "getMaxNumberOfItems": function() { return 5; }, "rowLength": 5, "conflictsWith": ["Garrison"], "getItems": function(unitEntStates) { // If more than `rowLength` resources, don't display icons. if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetBarterableCodes().length > this.rowLength) return []; return g_ResourceData.GetBarterableCodes(); }, "setupButton": function(data) { if (g_SelectionPanelBarterButtonManager) { g_SelectionPanelBarterButtonManager.setViewedPlayer(data.player); g_SelectionPanelBarterButtonManager.update(); } return true; } }; g_SelectionPanels.Command = { "getMaxNumberOfItems": function() { return 6; }, "getItems": function(unitEntStates) { let commands = []; for (let command in g_EntityCommands) { let info = getCommandInfo(command, unitEntStates); if (info) { info.name = command; commands.push(info); } } return commands; }, "setupButton": function(data) { data.button.tooltip = data.item.tooltip; data.button.onPress = function() { if (data.item.callback) data.item.callback(data.item); else performCommand(data.unitEntStates, data.item.name); }; data.countDisplay.caption = data.item.count || ""; data.button.enabled = data.item.enabled == true; data.icon.sprite = "stretched:session/icons/" + data.item.icon; let size = data.button.size; // relative to the center ( = 50%) size.rleft = 50; size.rright = 50; // offset from the center calculation, count on square buttons, so size.bottom is the width too size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1); size.right = size.left + size.bottom; data.button.size = size; return true; } }; g_SelectionPanels.Construction = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function() { return getAllBuildableEntitiesFromSelection(); }, "setupButton": function(data) { let template = GetTemplateData(data.item, data.player); if (!template) return false; let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": data.player }); let neededResources; if (template.cost) neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, 1), "player": data.player }); data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); }; let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; let tooltips = [ getEntityNamesFormatted, getVisibleEntityClassesFormatted, getAurasTooltip, getEntityTooltip ].map(func => func(template)); tooltips.push( getEntityCostTooltip(template, data.player), getResourceDropsiteTooltip(template), getGarrisonTooltip(template), getTurretsTooltip(template), getPopulationBonusTooltip(template), showTemplateViewerOnRightClickTooltip(template) ); let limits = getEntityLimitAndCount(data.playerState, data.item); tooltips.push( formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; if (!technologyEnabled || limits.canBeAddedCount == 0) { data.button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; } else if (neededResources) { data.button.enabled = false; modifier += resourcesToAlphaMask(neededResources) + ":"; } else data.button.enabled = controlsPlayer(data.player); if (template.icon) data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; g_SelectionPanels.Formation = { "getMaxNumberOfItems": function() { return 15; }, "rowLength": 5, "conflictsWith": ["Garrison"], "getItems": function(unitEntStates) { if (unitEntStates.some(state => !hasClass(state, "Unit"))) return []; if (unitEntStates.every(state => !state.identity || !state.identity.hasSomeFormation)) return []; if (!g_AvailableFormations.has(unitEntStates[0].player)) g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player)); return g_AvailableFormations.get(unitEntStates[0].player).filter(formation => unitEntStates.some(state => !!state.identity && state.identity.formations.indexOf(formation) != -1)); }, "setupButton": function(data) { if (!g_FormationsInfo.has(data.item)) g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); let formationOk = canMoveSelectionIntoFormation(data.item); let unitIds = data.unitEntStates.map(state => state.id); let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": unitIds, "formationTemplate": data.item }); data.button.onPress = function() { performFormation(unitIds, data.item); }; data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item); let formationInfo = g_FormationsInfo.get(data.item); let tooltip = translate(formationInfo.name); let isDefaultFormation = g_AutoFormation.isDefault(data.item); if (data.item === NULL_FORMATION) tooltip += "\n" + (isDefaultFormation ? translate("Default formation is disabled.") : translate("Right-click to disable the default formation feature.")); else tooltip += "\n" + (isDefaultFormation ? translate("This is the default formation, used for movement orders.") : translate("Right-click to set this as the default formation.")); if (!formationOk && formationInfo.tooltip) tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red"); data.button.tooltip = tooltip; data.button.enabled = formationOk && controlsPlayer(data.player); let grayscale = formationOk ? "" : "grayscale:"; data.guiSelection.hidden = !formationSelected; data.countDisplay.hidden = !isDefaultFormation; data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Garrison = { "getMaxNumberOfItems": function() { return 12; }, "rowLength": 4, "conflictsWith": ["Barter"], "getItems": function(unitEntStates) { if (unitEntStates.every(state => !state.garrisonHolder)) return []; let groups = new EntityGroups(); for (let state of unitEntStates) if (state.garrisonHolder) groups.add(state.garrisonHolder.entities); return groups.getEntsGrouped(); }, "setupButton": function(data) { let entState = GetEntityState(data.item.ents[0]); let template = GetTemplateData(entState.template); if (!template) return false; data.button.onPress = function() { unloadTemplate(template.selectionGroupName || entState.template, entState.player); }; data.countDisplay.caption = data.item.ents.length || ""; let canUngarrison = controlsPlayer(data.player) || controlsPlayer(entState.player); data.button.enabled = canUngarrison; data.button.tooltip = (canUngarrison ? sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) }) + "\n" + translate("Single-click to unload 1. Shift-click to unload all of this type.") : getEntityNames(template)) + "\n" + sprintf(translate("Player: %(playername)s"), { "playername": g_Players[entState.player].name }); data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(entState.player, 160); data.button.sprite_disabled = data.button.sprite; // Selection panel buttons only appear disabled if they // also appear disabled to the owner of the structure. data.icon.sprite = (canUngarrison || g_IsObserver ? "" : "grayscale:") + "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Gate = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function(unitEntStates) { let hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked); let hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked); if (hideLocked && hideUnlocked) return []; return [ { "hidden": hideLocked, "tooltip": translate("Lock Gate"), "icon": "session/icons/lock_locked.png", "locked": true }, { "hidden": hideUnlocked, "tooltip": translate("Unlock Gate"), "icon": "session/icons/lock_unlocked.png", "locked": false } ]; }, "setupButton": function(data) { data.button.onPress = function() { lockGate(data.item.locked); }; data.button.tooltip = data.item.tooltip; data.button.enabled = controlsPlayer(data.player); data.guiSelection.hidden = data.item.hidden; data.icon.sprite = "stretched:" + data.item.icon; setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; g_SelectionPanels.Pack = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function(unitEntStates) { let checks = {}; for (let state of unitEntStates) { if (!state.pack) continue; if (state.pack.progress == 0) { if (state.pack.packed) checks.unpackButton = true; else checks.packButton = true; } else if (state.pack.packed) checks.unpackCancelButton = true; else checks.packCancelButton = true; } let 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; }, "setupButton": function(data) { data.button.onPress = function() {data.item.callback(data.item); }; data.button.tooltip = data.item.tooltip; 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.player); setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; g_SelectionPanels.Queue = { "getMaxNumberOfItems": function() { return 16; }, /** * Returns a list of all items in the productionqueue of the selection * The first entry of every entity's production queue will come before * the second entry of every entity's production queue */ "getItems": function(unitEntStates) { const queue = []; let foundNew = true; for (let i = 0; foundNew; ++i) { foundNew = false; for (const state of unitEntStates) { if (!state.production || !state.production.queue[i]) continue; queue.push({ "producingEnt": state.id, "queuedItem": state.production.queue[i], "autoqueue": state.production.autoqueue && state.production.queue[i].unitTemplate, }); foundNew = true; } } if (!queue.length) return queue; // Add 'ghost' items to show autoqueues. const repeat = []; for (const item of queue) if (item.autoqueue) { const ghostItem = clone(item); ghostItem.ghost = true; repeat.push(ghostItem); } if (repeat.length) for (let i = 0; queue.length < g_SelectionPanels.Queue.getMaxNumberOfItems(); ++i) queue.push(repeat[i % repeat.length]); return queue; }, "resizePanel": function(numberOfItems, rowLength) { let numRows = Math.ceil(numberOfItems / rowLength); let panel = Engine.GetGUIObjectByName("unitQueuePanel"); let size = panel.size; let buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom; let margin = 4; size.top = size.bottom - numRows * buttonSize - (numRows + 2) * margin; panel.size = size; }, "setupButton": function(data) { let queuedItem = data.item.queuedItem; // Differentiate between units and techs let template; if (queuedItem.unitTemplate) template = GetTemplateData(queuedItem.unitTemplate); else if (queuedItem.technologyTemplate) template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ); else { warning("Unknown production queue template " + uneval(queuedItem)); return false; } data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); }; const tooltips = [getEntityNames(template)]; if (data.item.ghost) tooltips.push(translate("The auto-queue will try to train this item later.")); if (queuedItem.neededSlots) { tooltips.push(coloredText(translate("Insufficient population capacity:"), "red")); tooltips.push(sprintf(translate("%(population)s %(neededSlots)s"), { "population": resourceIcon("population"), "neededSlots": queuedItem.neededSlots })); } tooltips.push(showTemplateViewerOnRightClickTooltip(template)); data.button.tooltip = tooltips.join("\n"); data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : ""; if (data.item.ghost) { data.button.enabled = false; Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]").sprite="color:0 150 250 50"; } else { // Show the time remaining to finish the first item if (data.i == 0) Engine.GetGUIObjectByName("queueTimeRemaining").caption = Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss")); const guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]"); guiObject.sprite = "queueProgressSlider"; const 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(queuedItem.progress * (size.right - size.left)); guiObject.size = size; data.button.enabled = controlsPlayer(data.player); } if (template.icon) data.icon.sprite = (data.item.ghost ? "grayscale:" : "") + "stretched:session/portraits/" + template.icon; const showTemplateFunc = () => { showTemplateDetails(data.item.queuedItem.unitTemplate || data.item.queuedItem.technologyTemplate, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Research = { "getMaxNumberOfItems": function() { return 10; }, "rowLength": 10, "getItems": function(unitEntStates) { let ret = []; if (unitEntStates.length == 1) return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret : unitEntStates[0].production.technologies.map(tech => ({ "tech": tech, "techCostMultiplier": unitEntStates[0].production.techCostMultiplier, "researchFacilityId": unitEntStates[0].id, "isUpgrading": !!unitEntStates[0].upgrade && unitEntStates[0].upgrade.isUpgrading })); let sortedEntStates = unitEntStates.sort((a, b) => (!b.upgrade || !b.upgrade.isUpgrading) - (!a.upgrade || !a.upgrade.isUpgrading) || (!a.production ? 0 : a.production.queue.length) - (!b.production ? 0 : b.production.queue.length) ); for (let state of sortedEntStates) { if (!state.production || !state.production.technologies) continue; // Remove the techs we already have in ret (with the same name and techCostMultiplier) let filteredTechs = state.production.technologies.filter( tech => tech != null && !ret.some( item => (item.tech == tech || item.tech.pair && tech.pair && item.tech.bottom == tech.bottom && item.tech.top == tech.top) && Object.keys(item.techCostMultiplier).every( k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k]) )); if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() && getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2)) ret = ret.concat(filteredTechs.map(tech => ({ "tech": tech, "techCostMultiplier": state.production.techCostMultiplier, "researchFacilityId": state.id, "isUpgrading": !!state.upgrade && state.upgrade.isUpgrading }))); } return ret; }, "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; }, "setupButton": function(data) { if (!data.item.tech) { g_SelectionPanels.Research.hideItem(data.i, data.rowLength); return false; } // Start position (start at the bottom) let position = data.i + data.rowLength; // Only show the top button for pairs if (!data.item.tech.pair) Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; // Set up the tech connector let pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]"); pair.hidden = data.item.tech.pair == null; setPanelObjectPosition(pair, data.i, data.rowLength); // Handle one or two techs (tech pair) let player = data.player; let playerState = GetSimState().players[player]; for (let tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech]) { // Don't change the object returned by GetTechnologyData let template = clone(GetTechnologyData(tech, playerState.civ)); if (!template) return false; // Not allowed by civ. if (!template.reqs) { // One of the pair may still be researchable by the current civ, // hence don't hide everything. Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true; pair.hidden = true; continue; } for (let res in template.cost) template.cost[res] *= data.item.techCostMultiplier[res]; let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": template.cost, "player": player }); let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", { "tech": tech, "player": player }); let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]"); let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]"); let tooltips = [ getEntityNamesFormatted, getEntityTooltip, getEntityCostTooltip, showTemplateViewerOnRightClickTooltip ].map(func => func(template)); if (!requirementsPassed) { let tip = template.requirementsTooltip; let reqs = template.reqs; for (let req of reqs) { if (!req.entities) continue; let entityCounts = []; for (let entity of req.entities) { let current = 0; switch (entity.check) { case "count": current = playerState.classCounts[entity.class] || 0; break; case "variants": current = playerState.typeCountsByClass[entity.class] ? Object.keys(playerState.typeCountsByClass[entity.class]).length : 0; break; } let remaining = entity.number - current; if (remaining < 1) continue; entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), { "number": remaining, "class": translate(entity.class) })); } tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), { "entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", ")) }); } tooltips.push(tip); } tooltips.push(getNeededResourcesTooltip(neededResources)); button.tooltip = tooltips.filter(tip => tip).join("\n"); button.onPress = (t => function() { addResearchToQueue(data.item.researchFacilityId, t); })(tech); let showTemplateFunc = (t => function() { showTemplateDetails( t, GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv); }); button.onPressRight = showTemplateFunc(tech); button.onPressRightDisabled = showTemplateFunc(tech); if (data.item.tech.pair) { // On mouse enter, show a cross over the other icon let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]"); button.onMouseEnter = function() { unchosenIcon.hidden = false; }; button.onMouseLeave = function() { unchosenIcon.hidden = true; }; } button.hidden = false; let modifier = ""; if (!requirementsPassed) { button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; } else if (neededResources) { button.enabled = false; modifier += resourcesToAlphaMask(neededResources) + ":"; } else button.enabled = controlsPlayer(data.player); if (data.item.isUpgrading) { button.enabled = false; modifier += "color:0 0 0 127:grayscale:"; button.tooltip += "\n" + coloredText(translate("Cannot research while upgrading."), "red"); } if (template.icon) icon.sprite = modifier + "stretched:session/portraits/" + template.icon; setPanelObjectPosition(button, position, data.rowLength); // Prepare to handle the top button (if any) position -= data.rowLength; } return true; } }; g_SelectionPanels.Selection = { "getMaxNumberOfItems": function() { return 16; }, "rowLength": 4, "getItems": function(unitEntStates) { if (unitEntStates.length < 2) return []; return g_Selection.groups.getEntsGrouped(); }, "setupButton": function(data) { let entState = GetEntityState(data.item.ents[0]); let template = GetTemplateData(entState.template); if (!template) return false; for (let ent of data.item.ents) { let state = GetEntityState(ent); if (state.resourceCarrying && state.resourceCarrying.length !== 0) { if (!data.carried) data.carried = {}; let 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 = {}; let amount = state.trader.goods.amount; let type = state.trader.goods.type; let 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; } } let unitOwner = GetEntityState(data.item.ents[0]).player; let tooltip = getEntityNames(template); if (data.carried) tooltip += "\n" + Object.keys(data.carried).map(res => resourceIcon(res) + data.carried[res] ).join(" "); if (g_IsObserver) tooltip += "\n" + sprintf(translate("Player: %(playername)s"), { "playername": g_Players[unitOwner].name }); data.button.tooltip = tooltip; data.guiSelection.sprite = "color:" + g_DiplomacyColors.getPlayerColor(unitOwner, 160); data.guiSelection.hidden = !g_IsObserver; data.countDisplay.caption = data.item.ents.length || ""; - data.button.onPress = function() { changePrimarySelectionGroup(data.item.key, false); }; - data.button.onPressRight = function() { changePrimarySelectionGroup(data.item.key, true); }; + data.button.onPress = function() { + if (Engine.HotkeyIsPressed("session.deselectgroup")) + removeFromSelectionGroup(data.item.key); + else + makePrimarySelectionGroup(data.item.key); + }; + data.button.onPressRight = function() { removeFromSelectionGroup(data.item.key); }; if (template.icon) data.icon.sprite = "stretched:session/portraits/" + template.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Stance = { "getMaxNumberOfItems": function() { return 5; }, "getItems": function(unitEntStates) { if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal"))) return []; return unitEntStates[0].unitAI.selectableStances; }, "setupButton": function(data) { let unitIds = data.unitEntStates.map(state => state.id); data.button.onPress = function() { performStance(unitIds, data.item); }; data.button.tooltip = getStanceDisplayName(data.item) + "\n" + "[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]"; data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", { "ents": unitIds, "stance": data.item }); data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png"; data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Training = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function() { return getAllTrainableEntitiesFromSelection(); }, "setupButton": function(data) { let template = GetTemplateData(data.item, data.player); if (!template) return false; let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": data.player }); let unitIds = data.unitEntStates.map(status => status.id); let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = getTrainingStatus(unitIds, data.item, data.playerState); let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; let neededResources; if (template.cost) neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, trainNum), "player": data.player }); data.button.onPress = function() { if (!neededResources) addTrainingToQueue(unitIds, data.item, data.playerState); }; let showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; data.countDisplay.caption = trainNum > 1 ? trainNum : ""; let tooltips = [ "[font=\"sans-bold-16\"]" + colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) + "[/font]" + " " + getEntityNamesFormatted(template), getVisibleEntityClassesFormatted(template), getAurasTooltip(template), getEntityTooltip(template), getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) ]; let limits = getEntityLimitAndCount(data.playerState, data.item); tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type)); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") tooltips = tooltips.concat([ getHealthTooltip, getAttackTooltip, getHealerTooltip, getResistanceTooltip, getGarrisonTooltip, getTurretsTooltip, getProjectilesTooltip, getSpeedTooltip, getResourceDropsiteTooltip ].map(func => func(template))); tooltips.push(showTemplateViewerOnRightClickTooltip()); tooltips.push( formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch), getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); data.button.tooltip = tooltips.filter(tip => tip).join("\n"); let modifier = ""; if (!technologyEnabled || limits.canBeAddedCount == 0) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } else { data.button.enabled = controlsPlayer(data.player); if (neededResources) modifier = resourcesToAlphaMask(neededResources) + ":"; } if (data.unitEntStates.every(state => state.upgrade && state.upgrade.isUpgrading)) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; data.button.tooltip += "\n" + coloredText(translate("Cannot train while upgrading."), "red"); } if (template.icon) data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon; let index = data.i + getNumberOfRightPanelButtons(); setPanelObjectPosition(data.button, index, data.rowLength); return true; } }; g_SelectionPanels.Upgrade = { "getMaxNumberOfItems": function() { return 40 - getNumberOfRightPanelButtons(); }, "rowLength": 10, "getItems": function(unitEntStates) { // Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units. if (unitEntStates.some(state => state.template != unitEntStates[0].template)) return false; return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades; }, "setupButton": function(data) { let template = GetTemplateData(data.item.entity); if (!template) return false; let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]"); progressOverlay.hidden = true; let technologyEnabled = true; if (data.item.requiredTechnology) technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.item.requiredTechnology, "player": data.player }); let limits = getEntityLimitAndCount(data.playerState, data.item.entity); let upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.template == data.item.entity); let upgradableEntStates = data.unitEntStates.filter(state => !state.upgrade.progress && (!state.production || !state.production.queue || !state.production.queue.length)); let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.item, upgradableEntStates.length), "player": data.player }); let tooltip; let modifier = ""; if (!upgradingEntStates.length && upgradableEntStates.length) { let primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic; let secondaryName; if (g_ShowSecondaryNames) secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific; let tooltips = []; if (g_ShowSecondaryNames) { if (data.item.tooltip) tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s). %(tooltip)s"), { "primaryName": primaryName, "secondaryName": secondaryName, "tooltip": translate(data.item.tooltip) })); else tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s)."), { "primaryName": primaryName, "secondaryName": secondaryName })); } else { if (data.item.tooltip) tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s. %(tooltip)s"), { "primaryName": primaryName, "tooltip": translate(data.item.tooltip) })); else tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s."), { "primaryName": primaryName })); } tooltips.push( getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length), formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources), showTemplateViewerOnRightClickTooltip()); tooltip = tooltips.filter(tip => tip).join("\n"); data.button.onPress = function() { upgradeEntity( data.item.entity, upgradableEntStates.map(state => state.id)); }; if (!technologyEnabled || limits.canBeAddedCount == 0 && !upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.template))) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } else if (neededResources) { data.button.enabled = false; modifier = resourcesToAlphaMask(neededResources) + ":"; } data.countDisplay.caption = upgradableEntStates.length > 1 ? upgradableEntStates.length : ""; } else if (upgradingEntStates.length) { tooltip = translate("Cancel Upgrading"); data.button.onPress = function() { cancelUpgradeEntity(); }; data.countDisplay.caption = upgradingEntStates.length > 1 ? upgradingEntStates.length : ""; let progress = 0; for (let state of upgradingEntStates) progress = Math.max(progress, state.upgrade.progress || 1); let progressOverlaySize = progressOverlay.size; // TODO This is bad: we assume the progressOverlay is square progressOverlaySize.top = progressOverlaySize.bottom + Math.round((1 - progress) * (progressOverlaySize.left - progressOverlaySize.right)); progressOverlay.size = progressOverlaySize; progressOverlay.hidden = false; } else { tooltip = coloredText(translatePlural( "Cannot upgrade when the entity is training, researching or already upgrading.", "Cannot upgrade when all entities are training, researching or already upgrading.", data.unitEntStates.length), "red"); data.button.onPress = function() {}; data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } data.button.enabled = controlsPlayer(data.player); data.button.tooltip = tooltip; let showTemplateFunc = () => { showTemplateDetails(data.item.entity, data.playerState.civ); }; data.button.onPressRight = showTemplateFunc; data.button.onPressRightDisabled = showTemplateFunc; data.icon.sprite = modifier + "stretched:session/" + (data.item.icon || "portraits/" + template.icon); setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength); return true; } }; function initSelectionPanels() { let unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel"); if (BarterButtonManager.IsAvailable(unitBarterPanel)) g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel); } /** * Pauses game and opens the template details viewer for a selected entity or technology. * * Technologies don't have a set civ, so we pass along the native civ of * the template of the entity that's researching it. * * @param {string} [civCode] - The template name of the entity that researches the selected technology. */ function showTemplateDetails(templateName, civCode) { if (inputState != INPUT_NORMAL) return; g_PauseControl.implicitPause(); Engine.PushGuiPage( "page_viewer.xml", { "templateName": templateName, "civ": civCode }, resumeGame); } /** * 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. */ let 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 "Upgrade", // Must always be shown on upgradable entities "Training", "Construction", "Research", // Normal together with training // UNIQUE PANES (importance doesn't matter) "Command", "Queue", "Selection", ]; Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25947) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 25948) @@ -1,572 +1,576 @@ /** * @file Contains all helper functions that are needed only for selection_panels.js * and some that are needed for hotkeys, but not for anything inside input.js. */ const UPGRADING_NOT_STARTED = -2; const UPGRADING_CHOSEN_OTHER = -1; function canMoveSelectionIntoFormation(formationTemplate) { if (formationTemplate == NULL_FORMATION) return true; if (!(formationTemplate in g_canMoveIntoFormation)) g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", { "ents": g_Selection.toList(), "formationTemplate": formationTemplate }); return g_canMoveIntoFormation[formationTemplate]; } function hasSameRestrictionCategory(templateName1, templateName2) { let template1 = GetTemplateData(templateName1); let template2 = GetTemplateData(templateName2); if (template1.trainingRestrictions && template2.trainingRestrictions) return template1.trainingRestrictions.category == template2.trainingRestrictions.category; if (template1.buildRestrictions && template2.buildRestrictions) return template1.buildRestrictions.category == template2.buildRestrictions.category; return false; } /** * Returns a "color:255 0 0 Alpha" string based on how many resources are needed. */ function resourcesToAlphaMask(neededResources) { let totalCost = 0; for (let resource in neededResources) totalCost += +neededResources[resource]; return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50); } function getStanceDisplayName(name) { switch (name) { case "violent": return translateWithContext("stance", "Violent"); case "aggressive": return translateWithContext("stance", "Aggressive"); case "defensive": return translateWithContext("stance", "Defensive"); case "passive": return translateWithContext("stance", "Passive"); case "standground": return translateWithContext("stance", "Standground"); default: warn("Internationalization: Unexpected stance found: " + name); return name; } } function getStanceTooltip(name) { switch (name) { case "violent": return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible"); case "aggressive": return translateWithContext("stance", "Attack nearby opponents"); case "defensive": return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location"); case "passive": return translateWithContext("stance", "Flee if attacked"); case "standground": return translateWithContext("stance", "Attack opponents in range, but don't move"); default: return ""; } } /** * Format entity count/limit message for the tooltip */ function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers) { if (trainEntLimit == undefined) return ""; var text = sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), { "count": trainEntCount, "limit": trainEntLimit }); if (trainEntCount >= trainEntLimit) text = coloredText(text, "red"); for (var c in trainEntLimitChangers) { if (!trainEntLimitChangers[c]) continue; let string = trainEntLimitChangers[c] > 0 ? translate("%(changer)s enlarges the limit with %(change)s.") : translate("%(changer)s lessens the limit with %(change)s."); text += "\n" + sprintf(string, { "changer": translate(c), "change": trainEntLimitChangers[c] }); } return text; } /** * Format template match count/limit message for the tooltip. * * @param {number} matchEntLimit - The limit of the entity. * @param {number} matchEntCount - The count of the entity. * @param {string} type - The type of the action (i.e. "build" or "training"). * * @return {string} - The string to show the user with information regarding the limit of this template. */ function formatMatchLimitString(matchEntLimit, matchEntCount, type) { if (matchEntLimit == undefined) return ""; let passedLimit = matchEntCount >= matchEntLimit; let count = matchEntLimit - matchEntCount; let text; if (type == "build") { if (passedLimit) text = sprintf(translatePlural("Could only be constructed once.", "Could only be constructed %(limit)s times.", matchEntLimit), { "limit": matchEntLimit }); else if (matchEntLimit == 1) text = translate("Can be constructed only once."); else text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), { "count": count }); } else if (type == "training") { if (passedLimit) text = sprintf(translatePlural("Could only be trained once.", "Could only be trained %(limit)s times.", matchEntLimit), { "limit": matchEntLimit }); else if (matchEntLimit == 1) text = translate("Can be trained only once."); else text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), { "count": count }); } else { if (passedLimit) text = sprintf(translatePlural("Could only be created once.", "Could only be created %(limit)s times.", matchEntLimit), { "limit": matchEntLimit }); else if (matchEntLimit == 1) text = translate("Can be created only once."); else text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), { "count": count }); } return passedLimit ? coloredText(text, "red") : text; } /** * Format batch training string for the tooltip * Examples: * buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0: * "Shift-click to train 5" * buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0: * "Shift-click to train 10 (2*5)" * buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12: * "Shift-click to train 27 (15 + 12)" */ function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) { var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; // Don't show the batch training tooltip if either units of this type can't be trained at all // or only one unit can be trained if (totalBatchTrainingCount < 2) return ""; let fullBatchesString = ""; if (buildingsCountToTrainFullBatch > 1) fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), { "buildings": buildingsCountToTrainFullBatch, "batchSize": fullBatchSize }); else if (buildingsCountToTrainFullBatch == 1) fullBatchesString = fullBatchSize; // We need to display the batch details part if there is either more than // one structure with full batch or one structure with the full batch and // another with a partial batch let batchString; if (buildingsCountToTrainFullBatch > 1 || buildingsCountToTrainFullBatch == 1 && remainderBatch > 0) if (remainderBatch > 0) batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s)."); else batchString = translate("%(action)s to train %(number)s (%(fullBatch)s)."); else batchString = translate("%(action)s to train %(number)s."); return "[font=\"sans-13\"]" + setStringTags( sprintf(batchString, { "action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]", "number": totalBatchTrainingCount, "fullBatch": fullBatchesString, "remainderBatch": remainderBatch }), g_HotkeyTags) + "[/font]"; } /** * Camera jumping: when the user presses a hotkey the current camera location is marked. * When pressing another camera jump hotkey the camera jumps back to that position. * When the camera is already roughly at that location, jump back to where it was previously. */ var g_JumpCameraPositions = []; var g_JumpCameraLast; function jumpCamera(index) { let position = g_JumpCameraPositions[index]; if (!position) return; let threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold"); let cameraPivot = Engine.GetCameraPivot(); if (g_JumpCameraLast && Math.abs(cameraPivot.x - position.x) < threshold && Math.abs(cameraPivot.z - position.z) < threshold) { Engine.CameraMoveTo(g_JumpCameraLast.x, g_JumpCameraLast.z); } else { g_JumpCameraLast = cameraPivot; Engine.CameraMoveTo(position.x, position.z); } } function setJumpCamera(index) { g_JumpCameraPositions[index] = Engine.GetCameraPivot(); } /** * Called by GUI when user clicks a research button. */ function addResearchToQueue(entity, researchType) { Engine.PostNetworkCommand({ "type": "research", "entity": entity, "template": researchType }); } /** * Called by GUI when user clicks a production queue item. */ function removeFromProductionQueue(entity, id) { Engine.PostNetworkCommand({ "type": "stop-production", "entity": entity, "id": id }); } /** * Called by unit selection buttons. */ -function changePrimarySelectionGroup(templateName, deselectGroup) +function makePrimarySelectionGroup(templateName) { - g_Selection.makePrimarySelection(templateName, - Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup); + g_Selection.makePrimarySelection(templateName); +} + +function removeFromSelectionGroup(templateName) +{ + g_Selection.removeGroupFromSelection(templateName); } function performCommand(entStates, commandName) { if (!entStates.length) return; if (getCommandInfo(commandName, entStates)) g_EntityCommands[commandName].execute(entStates); } function performFormation(entities, formationTemplate) { if (!entities) return; Engine.PostNetworkCommand({ "type": "formation", "entities": entities, "formation": formationTemplate }); } function performStance(entities, stanceName) { if (!entities) return; Engine.PostNetworkCommand({ "type": "stance", "entities": entities, "name": stanceName }); } function lockGate(lock) { Engine.PostNetworkCommand({ "type": "lock-gate", "entities": g_Selection.toList(), "lock": lock }); } function packUnit(pack) { Engine.PostNetworkCommand({ "type": "pack", "entities": g_Selection.toList(), "pack": pack, "queued": false }); } function cancelPackUnit(pack) { Engine.PostNetworkCommand({ "type": "cancel-pack", "entities": g_Selection.toList(), "pack": pack, "queued": false }); } function upgradeEntity(Template, selection) { Engine.PostNetworkCommand({ "type": "upgrade", "entities": selection, "template": Template, "queued": false }); } function cancelUpgradeEntity() { Engine.PostNetworkCommand({ "type": "cancel-upgrade", "entities": g_Selection.toList(), "queued": false }); } /** * Set the camera to follow the given entity if it's a unit. * Otherwise stop following. */ function setCameraFollow(entity) { let entState = entity && GetEntityState(entity); if (entState && hasClass(entState, "Unit")) Engine.CameraFollow(entity); else Engine.CameraFollow(0); } function stopUnits(entities) { Engine.PostNetworkCommand({ "type": "stop", "entities": entities, "queued": false }); } function unloadTemplate(template, owner) { Engine.PostNetworkCommand({ "type": "unload-template", "all": Engine.HotkeyIsPressed("session.unloadtype"), "template": template, "owner": owner, // Filter out all entities that aren't garrisonable. "garrisonHolders": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && !!state.garrisonHolder; }) }); } function unloadAll() { const garrisonHolders = g_Selection.filter(e => { let state = GetEntityState(e); return state && !!state.garrisonHolder; }); if (!garrisonHolders.length) return; let ownEnts = []; let otherEnts = []; for (let ent of garrisonHolders) { if (controlsPlayer(GetEntityState(ent).player)) ownEnts.push(ent); else otherEnts.push(ent); } if (ownEnts.length) Engine.PostNetworkCommand({ "type": "unload-all", "garrisonHolders": ownEnts }); if (otherEnts.length) Engine.PostNetworkCommand({ "type": "unload-all-by-owner", "garrisonHolders": otherEnts }); } function unloadAllTurrets() { const turretHolders = g_Selection.filter(e => { let state = GetEntityState(e); return state && !!state.turretHolder; }); if (!turretHolders.length) return; let ownedHolders = []; let ejectables = []; for (let ent of turretHolders) { let turretHolderState = GetEntityState(ent); if (controlsPlayer(turretHolderState.player)) ownedHolders.push(ent); else { for (let turret of turretHolderState.turretHolder.turretPoints.map(tp => tp.entity)) if (turret && controlsPlayer(GetEntityState(turret).player)) ejectables.push(turret); } } if (ejectables.length) Engine.PostNetworkCommand({ "type": "leave-turret", "entities": ejectables }); if (ownedHolders.length) Engine.PostNetworkCommand({ "type": "unload-turrets", "entities": ownedHolders }); } function leaveTurretPoints() { const entities = g_Selection.filter(entity => { let entState = GetEntityState(entity); return entState && entState.turretable && entState.turretable.holder != INVALID_ENTITY; }); Engine.PostNetworkCommand({ "type": "leave-turret", "entities": entities }); } function backToWork() { Engine.PostNetworkCommand({ "type": "back-to-work", // Filter out all entities that can't go back to work. "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && state.unitAI && state.unitAI.hasWorkOrders; }) }); } function removeGuard() { Engine.PostNetworkCommand({ "type": "remove-guard", // Filter out all entities that are currently guarding/escorting. "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && state.unitAI && state.unitAI.isGuarding; }) }); } function raiseAlert() { Engine.PostNetworkCommand({ "type": "alert-raise", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && !!state.alertRaiser; }) }); } function endOfAlert() { Engine.PostNetworkCommand({ "type": "alert-end", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return state && !!state.alertRaiser; }) }); } function turnAutoQueueOn() { Engine.PostNetworkCommand({ "type": "autoqueue-on", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return !!state?.production?.entities.length && !state.production.autoqueue; }) }); } function turnAutoQueueOff() { Engine.PostNetworkCommand({ "type": "autoqueue-off", "entities": g_Selection.filter(ent => { let state = GetEntityState(ent); return !!state?.production?.entities.length && state.production.autoqueue; }) }); }