Index: ps/trunk/binaries/data/mods/public/gui/civinfo/civinfo.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/civinfo/civinfo.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/civinfo/civinfo.js (revision 23366) @@ -1,153 +1,153 @@ /** * Display selectable civs only. */ const g_CivData = loadCivData(true, false); var g_SelectedCiv = ""; /** * Initialize the dropdown containing all the available civs. */ function init(data = {}) { var civList = Object.keys(g_CivData).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase); var civSelection = Engine.GetGUIObjectByName("civSelection"); civSelection.list = civList.map(civ => civ.name); civSelection.list_data = civList.map(civ => civ.code); civSelection.selected = data.civ ? civSelection.list_data.indexOf(data.civ) : 0; Engine.GetGUIObjectByName("structreeButton").tooltip = colorizeHotkey(translate("%(hotkey)s: Switch to Structure Tree."), "structree"); Engine.GetGUIObjectByName("close").tooltip = colorizeHotkey(translate("%(hotkey)s: Close History."), "cancel"); } /** * Give the first character a larger font. */ function bigFirstLetter(str, size) { return '[font="sans-bold-' + (size + 6) + '"]' + str[0] + '[/font]' + '[font="sans-bold-' + size + '"]' + str.substring(1) + '[/font]'; } /** * Set heading font - bold and mixed caps * * @param string {string} * @param size {number} - Font size * @returns {string} */ function heading(string, size) { var textArray = string.split(" "); for (let i in textArray) { var word = textArray[i]; var wordCaps = word.toUpperCase(); // Check if word is capitalized, if so assume it needs a big first letter // Check if toLowerCase changes the character to avoid false positives from special signs if (word.length && word[0].toLowerCase() != word[0]) textArray[i] = bigFirstLetter(wordCaps, size); else textArray[i] = '[font="sans-bold-' + size + '"]' + wordCaps + '[/font]'; // TODO: Would not be necessary if we could do nested tags } return textArray.join(" "); } /** * Prepends a backslash to all quotation marks. * @param str {string} * @returns {string} */ function escapeQuotation(str) { return str.replace(/"/g, "\\\""); } /** * Returns a styled concatenation of Name, History and Description of the given object. * * @param obj {Object} * @returns {string} */ function subHeading(obj) { if (!obj.Name) return ""; let string = '[font="sans-bold-14"]' + obj.Name + '[/font] '; if (obj.History) string += '[icon="iconInfo" tooltip="' + escapeQuotation(obj.History) + '" tooltip_style="civInfoTooltip"]'; if (obj.Description) string += '\n ' + obj.Description; // Translation: insert an itemization symbol for each entry. return sprintf(translate("• %(string)s"), { "string": string }) + "\n"; } function switchToStrucTreePage() { Engine.PopGuiPage({ "civ": g_SelectedCiv, "nextPage": "page_structree.xml" }); } function closePage() { Engine.PopGuiPage({ "civ": g_SelectedCiv, "page": "page_civinfo.xml" }); } /** * Updates the GUI after the user selected a civ from dropdown. * * @param code {string} */ function selectCiv(code) { var civInfo = g_CivData[code]; g_SelectedCiv = code; if(!civInfo) error(sprintf("Error loading civ data for \"%(code)s\"", { "code": code })); // Update civ gameplay display Engine.GetGUIObjectByName("civGameplayHeading").caption = heading(sprintf(translate("%(civilization)s Gameplay"), { "civilization": civInfo.Name }), 16); // Bonuses var bonusCaption = heading(translatePlural("Civilization Bonus", "Civilization Bonuses", civInfo.CivBonuses.length), 12) + '\n'; for (let bonus of civInfo.CivBonuses) bonusCaption += subHeading(bonus); - // Team Bonuses + // Team bonuses bonusCaption += heading(translatePlural("Team Bonus", "Team Bonuses", civInfo.TeamBonuses.length), 12) + '\n'; for (let bonus of civInfo.TeamBonuses) bonusCaption += subHeading(bonus); Engine.GetGUIObjectByName("civBonuses").caption = bonusCaption; - // Special techs + // Special technologies var techCaption = heading(translate("Special Technologies"), 12) + '\n'; for (let faction of civInfo.Factions) for (let technology of faction.Technologies) techCaption += subHeading(technology); - // Special buildings - techCaption += heading(translatePlural("Special Building", "Special Buildings", civInfo.Structures.length), 12) + '\n'; + // Special structures + techCaption += heading(translatePlural("Special Structure", "Special Structures", civInfo.Structures.length), 12) + '\n'; for (let structure of civInfo.Structures) techCaption += subHeading(structure); Engine.GetGUIObjectByName("civTechs").caption = techCaption; // Heroes var heroCaption = heading(translate("Heroes"), 12) + '\n'; for (let faction of civInfo.Factions) { for (let hero of faction.Heroes) heroCaption += subHeading(hero); heroCaption += '\n'; } Engine.GetGUIObjectByName("civHeroes").caption = heroCaption; // Update civ history display Engine.GetGUIObjectByName("civHistoryHeading").caption = heading(sprintf(translate("History of the %(civilization)s"), { "civilization": civInfo.Name }), 16); Engine.GetGUIObjectByName("civHistoryText").caption = civInfo.History; } Index: ps/trunk/binaries/data/mods/public/gui/manual/intro.txt =================================================================== --- ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 23366) @@ -1,158 +1,158 @@ [font="sans-bold-18"]0 A.D. in-game manual[font="sans-14"] Thank you for installing 0 A.D.! This page will give a brief overview of the features available in this incomplete, under-development, alpha version of the game. [font="sans-bold-16"]Graphics settings[font="sans-14"] You can switch between fullscreen and windowed mode by pressing Alt + Enter. In windowed mode, you can resize the window. If the game runs too slowly, you can change some settings in the options window: try disabling the "HQ Water Effects" and "Shadows" options. [font="sans-bold-16"]Playing the game[font="sans-14"] The controls and gameplay should be familiar to players of traditional real-time strategy (RTS) games. There are currently a lot of missing features and poorly-balanced stats – you will probably have to wait until a beta release for it to work well. Basic controls: • Left-click to select units • Left-click-and-drag to select groups of units • Right-click to order units to the target • Arrow keys or WASD keys to move the camera • Ctrl + arrow keys, or shift + mouse wheel, to rotate the camera • Mouse wheel, or "="/"+" and "-"/"−" keys, to zoom [font="sans-bold-16"]Modes[font="sans-14"] The main menu gives access to two game modes: • [font="sans-bold-14"]Single-player[font="sans-14"] – play a sandbox game or against one or more computer players (AI). The default AI (Petra) is under development and may not always be up to date on the new features, but you can play the game with or against it nonetheless. • [font="sans-bold-14"]Multiplayer[font="sans-14"] – play against human opponents over the internet. To set up a multiplayer game, one player must select the "Host game" option. The game uses UDP port 20595, so the host must configure their NAT/firewall/etc to allow this. Other players can then select "Join game" and enter the host's IP address. [font="sans-bold-16"]Game setup[font="sans-14"] In a multiplayer game, only the host can alter the game setup options. First, select what type of map you want to play: • [font="sans-bold-14"]Random map[font="sans-14"] – A map created automatically from a script • [font="sans-bold-14"]Scenario map[font="sans-14"] – A map created by map designers and with fixed civilizations • [font="sans-bold-14"]Skirmish map[font="sans-14"] – A map created by map designers but where the civilizations can be chosen Then the maps can be further filtered. The default maps are generally playable maps. Naval maps are maps where not all opponents can be reached over land and demo maps are maps used for testing purposes (generally not useful to play on). The "All Maps" filter shows all maps together in one list. Finally change the settings. For random maps this includes the number of players, the size of a map, etc. For scenarios you can only select who controls which player (decides where you start on the map etc). The options are either a human player, an AI or no player at all (the opponent will just be idle). When you are ready to start, click the "Start game" button. [font="sans-bold-16"]Hotkeys:[font="sans-14"] [font="sans-bold-14"]Program-wide[font="sans-14"] Alt + F4 – Immediately close the game, without asking for confirmation Alt + Enter – Toggle between fullscreen and windowed ~ or F9 – Show/hide console Alt + F – Show/hide frame counter (FPS) F11 – Enable/disable real-time profiler (toggles through the displays of information) Shift + F11 – Save current profiler data to "logs/profile.txt" F2 – Take screenshot (in .png format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there) Shift + F2 – Take huge screenshot (6400×4800 pixels, in .bmp format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there) Tab, Alt + S – Switch to the next tab Shift + Tab, Alt + W – Switch to the previous tab [font="sans-bold-14"]In Game[font="sans-14"] Double Left Click \[on unit] – Select all of your units of the same kind on the screen (even if they're different ranks) Triple Left Click \[on unit] – Select all of your units of the same kind and the same rank on the screen Alt + Double Left Click \[on unit] – Select all your units of the same kind on the entire map (even if they have different ranks) Alt + Triple Left Click \[on unit] – Select all your units of the same kind and rank on the entire map Shift + F5 – Quicksave Shift + F8 – Quickload F10 – Open/close menu F12 – Show/hide time elapsed counter Esc – Close all dialogs (chat, menu) or clear selected units Enter/Return – Open/send chat T – Send team chat L – Chat with the previously selected private chat partner Pause – Pause/resume the game - Delete – Delete currently selected unit(s)/building(s), ask for confirmation - Shift + Delete – Immediately delete currently selected unit(s)/building(s), without asking for confirmation + Delete – Delete currently selected unit(s)/structure(s), ask for confirmation + Shift + Delete – Immediately delete currently selected unit(s)/structure(s), without asking for confirmation / (Slash) – Select idle fighter Shift + / (Slash) – Add idle fighter to selection Alt + / (Slash) – Select all idle fighters . (Period) – Select idle worker (including citizen-soldiers) Shift + . (Period) – Add idle worker to selection (including citizen-soldiers) Alt + . (Period) – Select all idle workers (including citizen-soldiers) \\ (Backslash) – Select idle unit Shift + \\ (Backslash) – Add idle unit to selection Alt + \\ (Backslash) – Select all idle units H – Stop (halt) the currently selected units Y – The unit will go back to work - U – Unload the garrisoned units of the selected buildings - Ctrl + 1 (and so on up to Ctrl + 0) – Create control group 1 (to 0) from the selected unit(s)/building(s) - 1 (and so on up to 0) – Select the units/buildings in control group 1 (to 0) - Shift + 1 (to 0) – Add control group 1 (to 0) to the selected units/buildings + U – Unload the garrisoned units of the selected structure(s) + Ctrl + 1 (and so on up to Ctrl + 0) – Create control group 1 (to 0) from the selected unit(s)/structure(s) + 1 (and so on up to 0) – Select the unit(s)/structure(s) in control group 1 (to 0) + Shift + 1 (to 0) – Add control group 1 (to 0) to the selected unit(s)/structure(s) Ctrl + F5 (and so on up to F8) – Mark the current camera position, for jumping back to later F5, F6, F7, and F8 – Move the camera to a marked position. Jump back to the last location if the camera is already over the marked position - Z, X, C, V, B, N, M – With training buildings selected. Add the 1st, 2nd, … unit shown to the training queue for all the selected buildings - PageUp with units selected – Highlights the units/buildings guarded by the selection - PageDown with units/buildings selected – Highlights the units guarding the selection + Z, X, C, V, B, N, M – With training structure selected. Add the 1st, 2nd, … unit shown to the training queue for all the selected structures + PageUp with unit(s) selected – Highlights the unit(s)/structure(s) guarded by the selection + PageDown with unit(s)/structure(s) selected – Highlights the unit(s) guarding the selection Tab – See all status bars (which would also show the building progress) Ctrl + Tab – Toggle summary window Alt + L – Show the multiplayer lobby in a dialog window Ctrl + H – Toggle in-game diplomacy panel Ctrl + B – Toggle in-game barter and trade panel Ctrl + O – Toggle in-game objectives panel Ctrl + P – Toggle in-game tutorial panel Alt + Shift + T – Toggle structure tree panel Alt + Shift + H – Toggle civilization info panel [font="sans-bold-14"]Modify mouse action[font="sans-14"] - Ctrl + Right Click on building – Garrison - J + Right Click on building – Repair + Ctrl + Right Click on structure – Garrison + J + Right Click on structure – Repair P + Right Click – Patrol Shift + Right Click – Queue the move/build/gather/etc order Alt + Right Click – Order one unit from the current selection to move/build/gather/etc and unselect it. Used to quickly dispatch units with specific tasks Shift + Left Click when training units – Add units in batches (the batch size is 5 by default and can be changed in the options) Shift + Left Click or Left Drag over unit on map – Add unit to selection Ctrl + Left Click or Left Drag over unit on map – Remove unit from selection Alt + Left Drag over units on map – Only select military units Alt + Y + Left Drag over units on map – Only select non-military units I + Left Drag over units on map – Only select idle units O + Left Drag over units on map – Only select wounded units Ctrl + Left Click on unit/group icon with multiple units selected – Deselect - Right Click with a building(s) selected – sets a rally point for units created/ungarrisoned from that building + Right Click with a structure(s) selected – sets a rally point for units created/ungarrisoned from that structure Ctrl + Right Click with unit(s) selected: • If the cursor is over an own or allied structure – Garrison - • If the cursor is over an enemy unit or building – Attack (instead of capture or gather) + • If the cursor is over an enemy unit/structure – Attack (instead of capture or gather) • Otherwise – Attack move (by default all enemy units and structures along the way are targeted) Ctrl + Q + Right Click with unit(s) selected – Attack move, only units along the way are targeted Ctrl + Mouse Move near structures – Align the new structure with an existing nearby structure [font="sans-bold-14"]Overlays[font="sans-14"] Alt + G – Hide/show the GUI Alt + D – Show/hide developer overlay (with developer options) Alt + Shift + W – Toggle wireframe mode (press once to get wireframes overlaid over the textured models, twice to get just the wireframes colored by the textures, thrice to get back to normal textured mode) Alt + Shift + S – Toggle unit silhouettes (might give a small performance boost) Alt + Z – Toggle sky Alt + X – Toggle diplomacy colors Alt + C – Toggle attack range visualizations of selected units and structures Alt + V – Toggle aura range visualizations of selected units and structures Alt + B – Toggle heal range visualizations of selected units [font="sans-bold-14"]Camera manipulation[font="sans-14"] W or ↑ (Up) – Pan screen up S or ↓ (Down) – Pan screen down A or ← (Left) – Pan screen left D or → (Right) – Pan screen right Ctrl + W or Ctrl + ↑ (Up) – Rotate camera to look upward Ctrl + S or Ctrl + ↓ (Down) – Rotate camera to look downward Ctrl + A or Ctrl + ← (Left) – Rotate camera clockwise around terrain Ctrl + D or Ctrl + → (Right) – Rotate camera counter-clockwise around terrain Q – Rotate camera clockwise around terrain E – Rotate camera counter-clockwise around terrain Shift + Mouse Wheel Rotate Up – Rotate camera clockwise around terrain Shift + Mouse Wheel Rotate Down – Rotate camera counter-clockwise around terrain F – Follow the selected unit (move the camera to stop following the unit) R – Reset camera zoom/orientation = (Equals) or + (Plus) – Zoom in (keep pressed for continuous zoom) - (Hyphen) or − (Minus) – Zoom out (keep pressed for continuous zoom) Middle Mouse Button – Keep pressed and move the mouse to pan -[font="sans-bold-14"]During Building Placement[font="sans-14"] - \[ (Left Bracket) – Rotate building 15 degrees counter-clockwise - ] (Right Bracket) – Rotate building 15 degrees clockwise - Left Drag – Rotate building using mouse (foundation will be placed on mouse release) +[font="sans-bold-14"]During Structure Placement[font="sans-14"] + \[ (Left Bracket) – Rotate structure 15 degrees counter-clockwise + ] (Right Bracket) – Rotate structure 15 degrees clockwise + Left Drag – Rotate structure using mouse (foundation will be placed on mouse release) [font="sans-bold-14"]When loading a saved game[font="sans-14"] Esc – Cancel Delete – Delete the selected saved game, ask for confirmation Shift + Delete – Immediately delete the selected saved game, without asking for confirmation Index: ps/trunk/binaries/data/mods/public/gui/options/options.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 23366) @@ -1,538 +1,538 @@ [ { "label": "General", "options": [ { "type": "string", "label": "Player Name (Single-player)", "tooltip": "How you want to be addressed in single-player matches.", "config": "playername.singleplayer" }, { "type": "string", "label": "Player Name (Multiplayer)", "tooltip": "How you want to be addressed in multiplayer matches (except lobby).", "config": "playername.multiplayer" }, { "type": "boolean", "label": "Background Pause", "tooltip": "Pause single-player games when window loses focus.", "config": "pauseonfocusloss" }, { "type": "boolean", "label": "Enable Welcome Screen", "tooltip": "If you disable it, the welcome screen will still appear once, each time a new version is available. You can always launch it from the main menu.", "config": "gui.splashscreen.enable" }, { "type": "boolean", "label": "Network Warnings", "tooltip": "Show which player has a bad connection in multiplayer games.", "config": "overlay.netwarnings" }, { "type": "boolean", "label": "FPS Overlay", "tooltip": "Show frames per second in top right corner.", "config": "overlay.fps" }, { "type": "boolean", "label": "Realtime Overlay", "tooltip": "Show current system time in top right corner.", "config": "overlay.realtime" }, { "type": "boolean", "label": "Gametime Overlay", "tooltip": "Show current simulation time in top right corner.", "config": "gui.session.timeelapsedcounter" }, { "type": "boolean", "label": "Ceasefire Time Overlay", "tooltip": "Always show the remaining ceasefire time.", "config": "gui.session.ceasefirecounter" }, { "type": "dropdown", "label": "Late Observer Joins", "tooltip": "Allow everybody or buddies only to join the game as observer after it started.", "config": "network.lateobservers", "list": [ { "value": "everyone", "label": "Everyone" }, { "value": "buddies", "label": "Buddies" }, { "value": "disabled", "label": "Disabled" } ] }, { "type": "number", "label": "Observer Limit", "tooltip": "Prevent further observers from joining if the limit is reached.", "config": "network.observerlimit", "min": 0, "max": 32 }, { "type": "boolean", "label": "Chat Timestamp", "tooltip": "Show time that messages are posted in the lobby, gamesetup and ingame chat.", "config": "chat.timestamp" } ] }, { "label": "Graphics", "tooltip": "Set the balance between performance and visual appearance.", "options": [ { "type": "boolean", "label": "Windowed Mode", "tooltip": "Start 0 A.D. in a window.", "config": "windowed" }, { "type": "boolean", "label": "Prefer GLSL", "tooltip": "Use OpenGL 2.0 shaders (recommended).", "config": "preferglsl", "function": "Renderer_SetPreferGLSLEnabled" }, { "type": "boolean", "label": "Fog", "tooltip": "Enable Fog.", "dependencies": ["preferglsl"], "config": "fog", "function": "Renderer_SetFogEnabled" }, { "type": "boolean", "label": "Post Processing", "tooltip": "Use screen-space postprocessing filters (HDR, Bloom, DOF, etc).", "config": "postproc", "function": "Renderer_SetPostProcEnabled" }, { "type": "slider", "label": "Shader Effects", "tooltip": "Number of shader effects. REQUIRES GAME RESTART", "config": "materialmgr.quality", "min": 0, "max": 10 }, { "type": "boolean", "label": "Shadows", "tooltip": "Enable shadows.", "config": "shadows", "function": "Renderer_SetShadowsEnabled" }, { "type": "dropdown", "label": "Shadow Quality", "tooltip": "Shadow map resolution. High values can crash the game when using a graphics card with low memory!", "dependencies": ["shadows"], "config": "shadowquality", "function": "Renderer_RecreateShadowMap", "list": [ { "value": -2, "label": "Very Low" }, { "value": -1, "label": "Low" }, { "value": 0, "label": "Medium" }, { "value": 1, "label": "High" }, { "value": 2, "label": "Very High" } ] }, { "type": "boolean", "label": "Shadow Filtering", "tooltip": "Smooth shadows.", "dependencies": ["shadows"], "config": "shadowpcf", "function": "Renderer_SetShadowPCFEnabled" }, { "type": "boolean", "label": "Unit Silhouettes", - "tooltip": "Show outlines of units behind buildings.", + "tooltip": "Show outlines of units behind structures.", "config": "silhouettes", "function": "Renderer_SetSilhouettesEnabled" }, { "type": "boolean", "label": "Particles", "tooltip": "Enable particles.", "config": "particles", "function": "Renderer_SetParticlesEnabled" }, { "type": "boolean", "label": "Water Effects", "tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.", "config": "watereffects", "function": "Renderer_SetWaterEffectsEnabled" }, { "type": "boolean", "label": "HQ Water Effects", "tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.", "dependencies": ["watereffects"], "config": "waterfancyeffects", "function": "Renderer_SetWaterFancyEffectsEnabled" }, { "type": "boolean", "label": "Real Water Depth", "tooltip": "Use actual water depth in rendering calculations.", "dependencies": ["watereffects"], "config": "waterrealdepth", "function": "Renderer_SetWaterRealDepthEnabled" }, { "type": "boolean", "label": "Water Reflections", "tooltip": "Allow water to reflect a mirror image.", "dependencies": ["watereffects"], "config": "waterreflection", "function": "Renderer_SetWaterReflectionEnabled" }, { "type": "boolean", "label": "Water Refraction", "tooltip": "Use a real water refraction map and not transparency.", "dependencies": ["watereffects"], "config": "waterrefraction", "function": "Renderer_SetWaterRefractionEnabled" }, { "type": "boolean", "label": "Shadows on Water", "tooltip": "Cast shadows on water.", "dependencies": ["watereffects"], "config": "shadowsonwater", "function": "Renderer_SetWaterShadowsEnabled" }, { "type": "boolean", "label": "Smooth LOS", "tooltip": "Lift darkness and fog-of-war smoothly.", "config": "smoothlos", "function": "Renderer_SetSmoothLOSEnabled" }, { "type": "boolean", "label": "Show Sky", "tooltip": "Render Sky.", "config": "showsky", "function": "Renderer_SetShowSkyEnabled" }, { "type": "boolean", "label": "VSync", "tooltip": "Run vertical sync to fix screen tearing. REQUIRES GAME RESTART", "config": "vsync" }, { "type": "slider", "label": "FPS Throttling in Menus", "tooltip": "To save CPU workload, throttle render frequency in all menus. Set to maximum to disable throttling.", "config": "adaptivefps.menu", "min": 20, "max": 100 }, { "type": "slider", "label": "FPS Throttling in Games", "tooltip": "To save CPU workload, throttle render frequency in running games. Set to maximum to disable throttling.", "config": "adaptivefps.session", "min": 20, "max": 100 } ] }, { "label": "Sound", "options": [ { "type": "slider", "label": "Master Volume", "tooltip": "Master audio gain.", "config": "sound.mastergain", "function": "SetMasterGain", "min": 0, "max": 2 }, { "type": "slider", "label": "Music Volume", "tooltip": "In game music gain.", "config": "sound.musicgain", "function": "SetMusicGain", "min": 0, "max": 2 }, { "type": "slider", "label": "Ambient Volume", "tooltip": "In game ambient sound gain.", "config": "sound.ambientgain", "function": "SetAmbientGain", "min": 0, "max": 2 }, { "type": "slider", "label": "Action Volume", "tooltip": "In game unit action sound gain.", "config": "sound.actiongain", "function": "SetActionGain", "min": 0, "max": 2 }, { "type": "slider", "label": "UI Volume", "tooltip": "UI sound gain.", "config": "sound.uigain", "function": "SetUIGain", "min": 0, "max": 2 }, { "type": "boolean", "label": "Nick Notification", "tooltip": "Receive audio notification when someone types your nick.", "config": "sound.notify.nick" }, { "type": "boolean", "label": "Game Setup - New Player Notification", "tooltip": "Receive audio notification when a new client joins the game setup.", "config": "sound.notify.gamesetup.join" } ] }, { "label": "Game Setup", "options": [ { "type": "boolean", "label": "Enable Game Setting Tips", "tooltip": "Show tips when setting up a game.", "config": "gui.gamesetup.enabletips" }, { "type": "boolean", "label": "Enable Settings Panel Slide", "tooltip": "Slide the settings panel when opening, closing or resizing.", "config": "gui.gamesetup.settingsslide" }, { "type": "boolean", "label": "Persist Match Settings", "tooltip": "Save and restore match settings for quick reuse when hosting another game.", "config": "persistmatchsettings" }, { "type": "dropdown", "label": "Default AI Difficulty", "tooltip": "Default difficulty of the AI.", "config": "gui.gamesetup.aidifficulty", "list": [ { "value": 0, "label": "Sandbox" }, { "value": 1, "label": "Very Easy" }, { "value": 2, "label": "Easy" }, { "value": 3, "label": "Medium" }, { "value": 4, "label": "Hard" }, { "value": 5, "label": "Very Hard" } ] }, { "type": "dropdown", "label": "Default AI Behavior", "tooltip": "Default behavior of the AI.", "config": "gui.gamesetup.aibehavior", "list": [ { "value": "random", "label": "Random" }, { "value": "balanced", "label": "Balanced" }, { "value": "aggressive", "label": "Aggressive" }, { "value": "defensive", "label": "Defensive" } ] }, { "type": "dropdown", "label": "Assign Players", "tooltip": "Automatically assign joining clients to free player slots during the match setup.", "config": "gui.gamesetup.assignplayers", "list": [ { "value": "everyone", "label": "Everyone" }, { "value": "buddies", "label": "Buddies" }, { "value": "disabled", "label": "Disabled" } ] } ] }, { "label": "Lobby", "tooltip": "These settings only affect the multiplayer.", "options": [ { "type": "boolean", "label": "TLS Encryption", "tooltip": "Protect login and data exchanged with the lobby server using TLS encryption.", "config": "lobby.tls" }, { "type": "number", "label": "Chat Backlog", "tooltip": "Number of backlogged messages to load when joining the lobby.", "config": "lobby.history", "min": "0" }, { "type": "boolean", "label": "Game Rating Column", "tooltip": "Show the average rating of the participating players in a column of the gamelist.", "config": "lobby.columns.gamerating" } ] }, { "label": "In-Game", "tooltip": "Change options regarding the in-game settings.", "options": [ { "type": "slider", "label": "Wounded Unit Health", "tooltip": "The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number.", "config": "gui.session.woundedunithotkeythreshold", "min": 0, "max": 100 }, { "type": "number", "label": "Batch Training Size", "tooltip": "Number of units trained per batch by default.", "config": "gui.session.batchtrainingsize", "min": 1, "max": 20 }, { "type": "slider", "label": "Scroll Batch Increment Ratio", "tooltip": "Number of times you have to scroll to increase/decrease the batchsize by 1.", "config": "gui.session.scrollbatchratio", "min": 0.1, "max": 30 }, { "type": "boolean", "label": "Chat Notification Attack", "tooltip": "Show a chat notification if you are attacked by another player.", "config": "gui.session.notifications.attack" }, { "type": "boolean", "label": "Chat Notification Tribute", "tooltip": "Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode.", "config": "gui.session.notifications.tribute" }, { "type": "boolean", "label": "Chat Notification Barter", "tooltip": "Show a chat notification to observers when a player bartered resources.", "config": "gui.session.notifications.barter" }, { "type": "dropdown", "label": "Chat Notification Phase", "tooltip": "Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode.", "config": "gui.session.notifications.phase", "list": [ { "value": "none", "label": "Disable" }, { "value": "completed", "label": "Completed" }, { "value": "all", "label": "All displayed" } ] }, { "type": "boolean", "label": "Attack Range Visualization", "tooltip": "Display the attack range of selected defensive structures (can also be toggled in-game with the hotkey).", "config": "gui.session.attackrange" }, { "type": "boolean", "label": "Aura Range Visualization", "tooltip": "Display the range of auras of selected units and structures (can also be toggled in-game with the hotkey).", "config": "gui.session.aurasrange" }, { "type": "boolean", "label": "Heal Range Visualization", "tooltip": "Display the healing range of selected units (can also be toggled in-game with the hotkey).", "config": "gui.session.healrange" }, { "type": "boolean", "label": "Rank icon above status bar", "tooltip": "Show rank icons above status bars.", "config": "gui.session.rankabovestatusbar" }, { "type": "boolean", "label": "Experience status bar", "tooltip": "Show an experience status bar above each selected unit.", "config": "gui.session.experiencestatusbar" }, { "type": "boolean", "label": "Detailed Tooltips", - "tooltip": "Show detailed tooltips for trainable units in unit-producing buildings.", + "tooltip": "Show detailed tooltips for trainable units in unit-producing structures.", "config": "showdetailedtooltips" }, { "type": "dropdown", "label": "Sort resources and population tooltip", "tooltip": "Dynamically sort players in the resources and population tooltip by value.", "config": "gui.session.respoptooltipsort", "list": [ { "value": 0, "label": "Unordered" }, { "value": -1, "label": "Ascending" }, { "value": 1, "label": "Descending" } ] }, { "type": "color", "label": "Diplomacy Colors: Self", "tooltip": "Color of your units when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.self" }, { "type": "color", "label": "Diplomacy Colors: Ally", "tooltip": "Color of allies when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.ally" }, { "type": "color", "label": "Diplomacy Colors: Neutral", "tooltip": "Color of neutral players when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.neutral" }, { "type": "color", "label": "Diplomacy Colors: Enemy", "tooltip": "Color of enemies when diplomacy colors are enabled.", "config": "gui.session.diplomacycolors.enemy" } ] } ] Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 23366) @@ -1,194 +1,194 @@ /** * Array of structure template names when given a civ and a phase name. */ var g_BuildList = {}; /** * Array of template names that can be trained from a unit, given a civ and unit template name. */ var g_TrainList = {}; /** * Initialize the page * * @param {object} data - Parameters passed from the code that calls this page into existence. */ function init(data = {}) { let civList = Object.keys(g_CivData).map(civ => ({ "name": g_CivData[civ].Name, "code": civ, })).sort(sortNameIgnoreCase); if (!civList.length) { closePage(); return; } g_ParsedData = { "units": {}, "structures": {}, "techs": {}, "phases": {} }; let civSelection = Engine.GetGUIObjectByName("civSelection"); civSelection.list = civList.map(c => c.name); civSelection.list_data = civList.map(c => c.code); civSelection.selected = data.civ ? civSelection.list_data.indexOf(data.civ) : 0; Engine.GetGUIObjectByName("civinfo").tooltip = colorizeHotkey(translate("%(hotkey)s: Switch to History."), "civinfo"); Engine.GetGUIObjectByName("close").tooltip = colorizeHotkey(translate("%(hotkey)s: Close Structure Tree."), "cancel"); } function switchToCivInfoPage() { Engine.PopGuiPage({ "civ": g_SelectedCiv, "nextPage": "page_civinfo.xml" }); } function closePage() { Engine.PopGuiPage({ "civ": g_SelectedCiv, "page": "page_structree.xml" }); } /** * @param {string} civCode */ function selectCiv(civCode) { if (civCode === g_SelectedCiv || !g_CivData[civCode]) return; g_SelectedCiv = civCode; g_CurrentModifiers = deriveModifications(g_AutoResearchTechList); // If a buildList already exists, then this civ has already been parsed if (g_BuildList[g_SelectedCiv]) { draw(); drawPhaseIcons(); return; } let templateLists = compileTemplateLists(civCode); for (let u of templateLists.units.keys()) if (!g_ParsedData.units[u]) g_ParsedData.units[u] = loadEntityTemplate(u); for (let s of templateLists.structures.keys()) if (!g_ParsedData.structures[s]) g_ParsedData.structures[s] = loadEntityTemplate(s); // Load technologies g_ParsedData.techs[civCode] = {}; for (let techcode of templateLists.techs.keys()) if (basename(techcode).startsWith("phase")) g_ParsedData.phases[techcode] = loadPhase(techcode); else g_ParsedData.techs[civCode][techcode] = loadTechnology(techcode); // Establish phase order g_ParsedData.phaseList = UnravelPhases(g_ParsedData.phases); // Load any required generic phases that aren't already loaded for (let phasecode of g_ParsedData.phaseList) if (!g_ParsedData.phases[phasecode]) g_ParsedData.phases[phasecode] = loadPhase(phasecode); // Group production and upgrade lists of structures by phase for (let structCode of templateLists.structures.keys()) { let structInfo = g_ParsedData.structures[structCode]; structInfo.phase = getPhaseOfTemplate(structInfo); let structPhaseIdx = g_ParsedData.phaseList.indexOf(structInfo.phase); - // If this building is shared with another civ, - // it may have already gone through the grouping process already + // If this structure is shared with another civ, + // it may have already gone through the grouping process already. if (!Array.isArray(structInfo.production.techs)) continue; // Sort techs by phase let newProdTech = {}; for (let prod of structInfo.production.techs) { let phase = getPhaseOfTechnology(prod); if (phase === false) continue; if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!(phase in newProdTech)) newProdTech[phase] = []; newProdTech[phase].push(prod); } // Sort units by phase let newProdUnits = {}; for (let prod of structInfo.production.units) { let phase = getPhaseOfTemplate(g_ParsedData.units[prod]); if (phase === false) continue; if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!(phase in newProdUnits)) newProdUnits[phase] = []; newProdUnits[phase].push(prod); } g_ParsedData.structures[structCode].production = { "techs": newProdTech, "units": newProdUnits }; // Sort upgrades by phase let newUpgrades = {}; if (structInfo.upgrades) for (let upgrade of structInfo.upgrades) { let phase = getPhaseOfTemplate(upgrade); if (g_ParsedData.phaseList.indexOf(phase) < structPhaseIdx) phase = structInfo.phase; if (!newUpgrades[phase]) newUpgrades[phase] = []; newUpgrades[phase].push(upgrade); } g_ParsedData.structures[structCode].upgrades = newUpgrades; } // Determine the buildList for the civ (grouped by phase) let buildList = {}; let trainerList = []; for (let pha of g_ParsedData.phaseList) buildList[pha] = []; for (let structCode of templateLists.structures.keys()) { let phase = g_ParsedData.structures[structCode].phase; buildList[phase].push(structCode); } for (let unitCode of templateLists.units.keys()) { let unitTemplate = g_ParsedData.units[unitCode]; if (!unitTemplate.production.units.length && !unitTemplate.production.techs.length && !unitTemplate.upgrades) continue; trainerList.push(unitCode); } g_BuildList[g_SelectedCiv] = buildList; g_TrainList[g_SelectedCiv] = trainerList; draw(); drawPhaseIcons(); } Index: ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 23366) @@ -1,215 +1,215 @@ /** * Holder for the template file being displayed. */ var g_Template = {}; /** * Holder for the template lists generated by compileTemplateLists(). */ var g_TemplateLists = {}; /** * Used to display textual information and the build/train lists of the * template being displayed. * * At present, these are drawn in the main body of the page. */ var g_InfoFunctions = [ getEntityTooltip, getHistoryTooltip, getDescriptionTooltip, getAurasTooltip, getVisibleEntityClassesFormatted, getBuiltByText, getTrainedByText, getResearchedByText, getBuildText, getTrainText, getResearchText, getUpgradeText ]; /** * Override style so we can get a bigger specific name. */ g_TooltipTextFormats.nameSpecificBig.font = "sans-bold-20"; g_TooltipTextFormats.nameSpecificSmall.font = "sans-bold-16"; g_TooltipTextFormats.nameGeneric.font = "sans-bold-16"; /** * Path to unit rank icons. */ var g_RankIconPath = "session/icons/ranks/"; /** * Page initialisation. May also eventually pre-draw/arrange objects. * * @param {object} data - Contains the civCode and the name of the template to display. * @param {string} data.templateName * @param {string} [data.civ] */ function init(data) { if (!data || !data.templateName) { error("Viewer: No template provided"); closePage(); return; } let templateName = removeFiltersFromTemplateName(data.templateName); let isTech = techDataExists(templateName); // Attempt to get the civ code from the template, or, if // it's a technology, from the researcher's template. if (!isTech) { // Catch and redirect if template is a non-promotion variant of // another (ie. units/{civ}_support_female_citizen_house). templateName = getBaseTemplateName(templateName); g_SelectedCiv = loadTemplate(templateName).Identity.Civ; } if (g_SelectedCiv == "gaia" && data.civ) g_SelectedCiv = data.civ; g_TemplateLists = compileTemplateLists(g_SelectedCiv); g_CurrentModifiers = deriveModifications(g_AutoResearchTechList); g_Template = isTech ? loadTechnology(templateName) : loadEntityTemplate(templateName); if (!g_Template) { error("Viewer: unable to recognise or load template (" + templateName + ")"); closePage(); return; } g_StatsFunctions = [getEntityCostTooltip].concat(g_StatsFunctions); draw(); } /** * Populate the UI elements. */ function draw() { Engine.GetGUIObjectByName("entityName").caption = getEntityNamesFormatted(g_Template); let entityIcon = Engine.GetGUIObjectByName("entityIcon"); entityIcon.sprite = "stretched:session/portraits/" + g_Template.icon; let entityStats = Engine.GetGUIObjectByName("entityStats"); entityStats.caption = buildText(g_Template, g_StatsFunctions); let entityInfo = Engine.GetGUIObjectByName("entityInfo"); let infoSize = entityInfo.size; // The magic '8' below provides a gap between the bottom of the icon, and the start of the info text. infoSize.top = Math.max(entityIcon.size.bottom + 8, entityStats.size.top + entityStats.getTextSize().height); entityInfo.size = infoSize; entityInfo.caption = buildText(g_Template, g_InfoFunctions, "\n\n"); if (g_Template.promotion) Engine.GetGUIObjectByName("entityRankGlyph").sprite = "stretched:" + g_RankIconPath + g_Template.promotion.current_rank + ".png"; Engine.GetGUIObjectByName("entityRankGlyph").hidden = !g_Template.promotion; } function getBuiltByText(template) { if (g_SelectedCiv == "gaia" || !g_TemplateLists.structures.has(template.name.internal)) return ""; let builders = g_TemplateLists.structures.get(template.name.internal); if (!builders.length) return ""; // Translation: Label before a list of the names of units that build the structure selected. return buildListText(translate("Built by:"), builders.map(builder => getEntityNames(loadEntityTemplate(builder)))); } function getTrainedByText(template) { if (g_SelectedCiv == "gaia" || !g_TemplateLists.units.has(template.name.internal)) return ""; let trainers = g_TemplateLists.units.get(template.name.internal); if (!trainers.length) return ""; // Translation: Label before a list of the names of structures or units that train the unit selected. return buildListText(translate("Trained by:"), trainers.map(trainer => getEntityNames(loadEntityTemplate(trainer)))); } function getResearchedByText(template) { if (g_SelectedCiv == "gaia" || !g_TemplateLists.techs.has(template.name.internal)) return ""; let researchers = g_TemplateLists.techs.get(template.name.internal); if (!researchers.length) return ""; // Translation: Label before a list of names of structures or units that research the technology selected. return buildListText(translate("Researched at:"), researchers.map(researcher => getEntityNames(loadEntityTemplate(researcher)))); } /** - * @return {string} List of the names of the buildings the selected unit can build. + * @return {string} List of the names of the structures the selected unit can build. */ function getBuildText(template) { if (!template.builder || !template.builder.length) return ""; // Translation: Label before a list of the names of structures the selected unit can construct or build. return buildListText(translate("Builds:"), template.builder.map(prod => getEntityNames(loadEntityTemplate(prod)))); } /** * @return {string} List of the names of the technologies the selected structure/unit can research. */ function getResearchText(template) { if (!template.production || !template.production.techs || !template.production.techs.length) return ""; let researchNames = []; for (let tech of template.production.techs) { let techTemplate = loadTechnology(tech); if (techTemplate.reqs) researchNames.push(getEntityNames(techTemplate)); } // Translation: Label before a list of the names of technologies the selected unit or structure can research. return buildListText(translate("Researches:"), researchNames); } /** * @return {string} List of the names of the units the selected unit can train. */ function getTrainText(template) { if (!template.production || !template.production.units || !template.production.units.length) return ""; // Translation: Label before a list of the names of units the selected unit or structure can train. return buildListText(translate("Trains:"), template.production.units.map(prod => getEntityNames(loadEntityTemplate(prod)))); } /** - * @return {string} List of the names of the buildings/units the selected structure/unit can upgrade to. + * @return {string} List of the names of the structures/units the selected structure/unit can upgrade to. */ function getUpgradeText(template) { if (!template.upgrades) return ""; // Translation: Label before a list of the names of units or structures the selected unit or structure can be upgradable to. return buildListText(translate("Upgradable to:"), template.upgrades.map(upgrade => getEntityNames(upgrade.name ? upgrade : loadEntityTemplate(upgrade.entity)))); } Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 23366) @@ -1,1655 +1,1655 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; const ACTION_GUARD = 3; const ACTION_PATROL = 4; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; const INPUT_SELECTING = 1; const INPUT_BANDBOXING = 2; const INPUT_BUILDING_PLACEMENT = 3; const INPUT_BUILDING_CLICK = 4; const INPUT_BUILDING_DRAG = 5; const INPUT_BATCHTRAINING = 6; const INPUT_PRESELECTEDACTION = 7; const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; const INPUT_UNIT_POSITION_START = 10; const INPUT_UNIT_POSITION = 11; var inputState = INPUT_NORMAL; const INVALID_ENTITY = 0; var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; /** * Containing the ingame position which span the line. */ var g_FreehandSelection_InputLine = []; /** * Minimum squared distance when a mouse move is called a drag. */ const g_FreehandSelection_ResolutionInputLineSquared = 1; /** * Minimum length a dragged line should have to use the freehand selection. */ const g_FreehandSelection_MinLengthOfLine = 8; /** * To start the freehandSelection function you need a minimum number of units. * Minimum must be 2, for better performance you could set it higher. */ const g_FreehandSelection_MinNumberOfUnits = 2; /** * Number of pixels the mouse can move before the action is considered a drag. */ const g_MaxDragDelta = 4; /** * Used for remembering mouse coordinates at start of drag operations. */ var g_DragStart; /** * Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. * If any mousedown or mouseup of a sequence of clicks lands on a unit, * that unit will be selected, which makes it easier to click on moving units. */ var clickedEntity = INVALID_ENTITY; // Same double-click behaviour for hotkey presses const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { var cursorSet = false; var tooltipSet = false; var informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION)) { let action = determineAction(mouseX, mouseY); if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); cursorSet = true; } if (action.tooltip) { tooltipSet = true; informationTooltip.caption = action.tooltip; informationTooltip.hidden = false; } } } if (!cursorSet) Engine.ResetCursor(); if (!tooltipSet) informationTooltip.hidden = true; var placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); if (placementSupport.tooltipMessage) placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip"; placementTooltip.caption = placementSupport.tooltipMessage || ""; placementTooltip.hidden = !placementSupport.tooltipMessage; } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to. // See onSimulationUpdate in session.js. if (placementSupport.mode === "building") { if (placementSupport.template && placementSupport.position) { var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); // Show placement info tooltip if invalid position placementSupport.tooltipError = !result.success; placementSupport.tooltipMessage = ""; if (!result.success) { if (result.message && result.parameters) { var message = result.message; if (result.translateMessage) if (result.pluralMessage) message = translatePlural(result.message, result.pluralMessage, result.pluralCount); else message = translate(message); var parameters = result.parameters; if (result.translateParameters) translateObjectKeys(parameters, result.translateParameters); placementSupport.tooltipMessage = sprintf(message, parameters); } return false; } if (placementSupport.attack && placementSupport.attack.Ranged) { - // building can be placed here, and has an attack - // show the range advantage in the tooltip + // Structure can be placed here, and has an attack. + // Show the range advantage in the tooltip. var cmd = { "x": placementSupport.position.x, "z": placementSupport.position.z, "range": placementSupport.attack.Ranged.maxRange, "elevationBonus": placementSupport.attack.Ranged.elevationBonus, }; var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); var range = Math.round(cmd.range); placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); } return true; } } else if (placementSupport.mode === "wall") { if (placementSupport.wallSet && placementSupport.position) { // Fetch an updated list of snapping candidate entities placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities( placementSupport.wallSet.templates.tower, placementSupport.wallSnapEntitiesIncludeOffscreen, true, // require exact template match true // include foundations ); return Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": placementSupport.wallSet, "start": placementSupport.position, "end": placementSupport.wallEndPosition, "snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment }); } } return false; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMinimap) { var selection = g_Selection.toList(); // No action if there's no selection if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return undefined; // If the selection isn't friendly units, no action if (!selection.every(ownsEntity) && !(g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll)) return undefined; var target = undefined; if (!fromMinimap) { var ent = Engine.PickEntityAtPoint(x, y); if (ent != INVALID_ENTITY) target = ent; } // decide between the following ordered actions // if two actions are possible, the first one is taken // so the most specific should appear first var actions = Object.keys(g_UnitActions).slice(); actions.sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness); var actionInfo = undefined; if (preSelectedAction != ACTION_NONE) { for (var action of actions) if (g_UnitActions[action].preSelectedActionCheck) { var r = g_UnitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } for (var action of actions) if (g_UnitActions[action].hotkeyActionCheck) { var r = g_UnitActions[action].hotkeyActionCheck(target, selection); if (r) return r; } for (var action of actions) if (g_UnitActions[action].actionCheck) { var r = g_UnitActions[action].actionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } function ownsEntity(ent) { let entState = GetEntityState(ent); return entState && entState.player == g_ViewedPlayer; } function tryPlaceBuilding(queued) { if (placementSupport.mode !== "building") { error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'"); return false; } if (!updateBuildingPlacementPreview()) { // invalid location - don't build it // TODO: play a sound? return false; } var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); if (!queued) placementSupport.Reset(); else placementSupport.RandomizeActorSeed(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'"); return false; } var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) { error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo)); return false; } if (!wallPlacementInfo) return false; var selection = g_Selection.toList(); var cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, }; // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed // (this is somewhat non-ideal and hardcode-ish) var hasWallSegment = false; for (let piece of cmd.pieces) { if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( { hasWallSegment = true; break; } } if (hasWallSegment) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); } return true; } /** * Updates the bandbox object with new positions and visibility. * @returns {array} The coordinates of the vertices of the bandbox. */ function updateBandbox(bandbox, ev, hidden) { let scale = +Engine.ConfigDB_GetValue("user", "gui.scale"); let vMin = Vector2D.min(g_DragStart, ev); let vMax = Vector2D.max(g_DragStart, ev); bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale); bandbox.hidden = hidden; return [vMin.x, vMin.y, vMax.x, vMax.y]; } // Define some useful unit filters for getPreferredEntities var unitFilters = { "isUnit": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Unit"); }, "isDefensive": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Defensive"); }, "isMilitary": entity => { var entState = GetEntityState(entity); return entState && g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isNonMilitary": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && !g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isIdle": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.unitAI && entState.unitAI.isIdle && !hasClass(entState, "Domestic"); }, "isWounded": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.maxHitpoints && 100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold"); }, "isAnything": entity => { return true; } }; // Choose, inside a list of entities, which ones will be selected. // We may use several entity filters, until one returns at least one element. function getPreferredEntities(ents) { // Default filters var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; // Handle hotkeys if (Engine.HotkeyIsPressed("selection.militaryonly")) filters = [unitFilters.isMilitary]; if (Engine.HotkeyIsPressed("selection.nonmilitaryonly")) filters = [unitFilters.isNonMilitary]; if (Engine.HotkeyIsPressed("selection.idleonly")) filters = [unitFilters.isIdle]; if (Engine.HotkeyIsPressed("selection.woundedonly")) filters = [unitFilters.isWounded]; var preferredEnts = []; for (var i = 0; i < filters.length; ++i) { preferredEnts = ents.filter(filters[i]); if (preferredEnts.length) break; } return preferredEnts; } function handleInputBeforeGui(ev, hoveredObject) { if (GetSimState().cinemaPlaying) return false; // Capture mouse position so we can use it for displaying cursors, // and key states switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } // Remember whether the mouse is over a GUI object or not mouseIsOverObject = (hoveredObject != null); // Close the menu when interacting with the game world if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) g_Menu.close(); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: var bandbox = Engine.GetGUIObjectByName("bandbox"); switch (ev.type) { case "mousemotion": var rect = updateBandbox(bandbox, ev, false); var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); var preferredEntities = getPreferredEntities(ents); g_Selection.setHighlightList(preferredEntities); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var rect = updateBandbox(bandbox, ev, true); // Get list of entities limited to preferred entities var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); // Remove the bandbox hover highlighting g_Selection.setHighlightList([]); // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION: switch (ev.type) { case "mousemotion": return positionUnitsFreehandSelectionMouseMove(ev); case "mousebuttonup": return positionUnitsFreehandSelectionMouseUp(ev); } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_CLICK: // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. switch (ev.type) { case "mousebuttonup": if (ev.button === SDL_BUTTON_LEFT) { inputState = INPUT_BUILDING_WALL_PATHING; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_PATHING: // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the // user to continue building walls. switch (ev.type) { case "mousemotion": placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); - // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!) + // Update the structure placement preview, and by extension, the list of snapping candidate entities for both (!) // the ending point and the starting point to snap to. // // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers // in them. Might be useful to query only for entities within a certain range around the starting point and ending // points. placementSupport.wallSnapEntitiesIncludeOffscreen = true; var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost }); placementSupport.tooltipMessage = [ getEntityCostTooltip(result), getNeededResourcesTooltip(neededResources) ].filter(tip => tip).join("\n"); } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceWall(queued)) { if (queued) { // continue building, just set a new starting position where we left off placementSupport.position = placementSupport.wallEndPosition; placementSupport.wallEndPosition = undefined; inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.Reset(); inputState = INPUT_NORMAL; } } else placementSupport.tooltipMessage = translate("Cannot build wall here!"); updateBuildingPlacementPreview(); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // reset to normal input mode placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_DRAG: switch (ev.type) { case "mousemotion": let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) { // Rotate in the direction of the mouse placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); } else { // If the mouse is near the center, snap back to the default orientation placementSupport.SetDefaultAngle(); } let snapToEdges = Engine.HotkeyIsPressed("session.snaptoedges"); let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": snapToEdges && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } updateBuildingPlacementPreview(); break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { - // If shift is down, let the player continue placing another of the same building + // If shift is down, let the player continue placing another of the same structure. var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BATCHTRAINING: if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") { flushTrainingBatch(); inputState = INPUT_NORMAL; } break; } return false; } function handleInputAfterGui(ev) { if (GetSimState().cinemaPlaying) return false; if (ev.hotkey === undefined) ev.hotkey = null; if (ev.hotkey == "session.highlightguarding") { g_ShowGuarding = (ev.type == "hotkeydown"); updateAdditionalHighlight(); } else if (ev.hotkey == "session.highlightguarded") { g_ShowGuarded = (ev.type == "hotkeydown"); updateAdditionalHighlight(); } if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) clickedEntity = INVALID_ENTITY; // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_SELECTING; // If a single click occured, reset the clickedEntity. // Also set it if we're double/triple clicking and missed the unit earlier. if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { if (!controlsPlayer(g_ViewedPlayer)) break; g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_UNIT_POSITION_START; } break; case "hotkeydown": if (ev.hotkey.indexOf("selection.group.") == 0) { let now = Date.now(); if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { var sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { var sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now; prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { var action = determineAction(ev.x, ev.y); if (!action) break; if (!Engine.HotkeyIsPressed("session.queue")) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; } return doAction(action, ev); } else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } // else default: // Slight hack: If selection is empty, reset the input state if (g_Selection.toList().length == 0) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to bandbox mode if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta) { inputState = INPUT_BANDBOXING; return false; } var ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { if (clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); // Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event. if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity)) { clickedEntity = INVALID_ENTITY; if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { g_Selection.reset(); resetIdleUnit(); } inputState = INPUT_NORMAL; return true; } // If camera following and we select different unit, stop if (Engine.GetFollowedEntity() != clickedEntity) Engine.CameraFollow(0); var ents = []; if (ev.clicks == 1) ents = [clickedEntity]; else { // Double click or triple click has occurred var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); var matchRank = true; var templateToMatch; // Check for double click or triple click if (ev.clicks == 2) { // Select similar units regardless of rank templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName; if (templateToMatch) matchRank = false; else // No selection group name defined, so fall back to exact match templateToMatch = GetEntityState(clickedEntity).template; } else // Triple click // Select units matching exact template name (same rank) templateToMatch = GetEntityState(clickedEntity).template; // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); } // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) g_Selection.addList(ents); else if (Engine.HotkeyIsPressed("selection.remove")) g_Selection.removeList(ents); else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION_START: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to unit position mode if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta)) { inputState = INPUT_UNIT_POSITION; return false; } break; case "mousebuttonup": inputState = INPUT_NORMAL; if (ev.button == SDL_BUTTON_RIGHT) { let action = determineAction(ev.x, ev.y); if (action) return doAction(action, ev); } break; } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (placementSupport.mode === "wall") { // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities // itself happens in the call to updateBuildingPlacementPreview below). placementSupport.wallSnapEntitiesIncludeOffscreen = false; } else { // cancel if not enough resources if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost })) { placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } let snapToEdges = Engine.HotkeyIsPressed("session.snaptoedges"); let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "snapToEdges": snapToEdges && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } updateBuildingPlacementPreview(); // includes an update of the snap entity candidates return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { if (placementSupport.mode === "wall") { var validPlacement = updateBuildingPlacementPreview(); if (validPlacement !== false) inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); let snapToEdges = Engine.HotkeyIsPressed("session.snaptoedges"); if (snapToEdges) { let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "snapToEdges": Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( placementSupport.position.x, placementSupport.position.z) }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_BUILDING_CLICK; } return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; case "hotkeydown": var rotation_step = Math.PI / 12; // 24 clicks make a full rotation switch (ev.hotkey) { case "session.rotate.cw": placementSupport.angle += rotation_step; updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementSupport.angle -= rotation_step; updateBuildingPlacementPreview(); break; } break; } break; } return false; } function doAction(action, ev) { if (!controlsPlayer(g_ViewedPlayer)) return false; return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action); } function positionUnitsFreehandSelectionMouseMove(ev) { // Converting the input line into a List of points. // For better performance the points must have a minimum distance to each other. let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); if (!g_FreehandSelection_InputLine.length || target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= g_FreehandSelection_ResolutionInputLineSquared) g_FreehandSelection_InputLine.push(target); return false; } function positionUnitsFreehandSelectionMouseUp(ev) { inputState = INPUT_NORMAL; let inputLine = g_FreehandSelection_InputLine; g_FreehandSelection_InputLine = []; if (ev.button != SDL_BUTTON_RIGHT) return true; let lengthOfLine = 0; for (let i = 1; i < inputLine.length; ++i) lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); let selection = g_Selection.toList().filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); // Checking the line for a minimum length to save performance. if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) { let action = determineAction(ev.x, ev.y); return !!action && doAction(action, ev); } // Even distribution of the units on the line. let p0 = inputLine[0]; let entityDistribution = [p0]; let distanceBetweenEnts = lengthOfLine / (selection.length - 1); let freeDist = -distanceBetweenEnts; for (let i = 1; i < inputLine.length; ++i) { let p1 = inputLine[i]; freeDist += inputLine[i - 1].distanceTo(p1); while (freeDist >= 0) { p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); entityDistribution.push(p0); freeDist -= distanceBetweenEnts; } } // Rounding errors can lead to missing or too many points. entityDistribution = entityDistribution.slice(0, selection.length); entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1])); if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) > Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0])) entityDistribution.reverse(); Engine.PostNetworkCommand({ "type": Engine.HotkeyIsPressed("session.attackmove") ? "attack-walk-custom" : "walk-custom", "entities": selection, "targetPositions": entityDistribution.map(pos => pos.toFixed(2)), "targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, "queued": Engine.HotkeyIsPressed("session.queue") }); // Add target markers with a minimum distance of 5 to each other. let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts); for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker) DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; } function handleUnitAction(target, action) { if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute) { error("Invalid action.type " + action.type); return false; } let selection = g_Selection.toList(); if (Engine.HotkeyIsPressed("session.orderone")) { // Pick the first unit that can do this order. let unit = selection.find(entity => ["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method => g_UnitActions[action.type][method] && g_UnitActions[action.type][method](action.target || undefined, [entity]) )); if (unit) { selection = [unit]; g_Selection.removeList(selection); } } // If the session.queue hotkey is down, add the order to the unit's order queue instead // of running it immediately return g_UnitActions[action.type].execute(target, action, selection, Engine.HotkeyIsPressed("session.queue")); } function getEntityLimitAndCount(playerState, entType) { let ret = { "entLimit": undefined, "entCount": undefined, "entLimitChangers": undefined, "canBeAddedCount": undefined }; if (!playerState.entityLimits) return ret; let template = GetTemplateData(entType); let entCategory = template.trainingRestrictions && template.trainingRestrictions.category || template.buildRestrictions && template.buildRestrictions.category; if (entCategory && playerState.entityLimits[entCategory] !== undefined) { ret.entLimit = playerState.entityLimits[entCategory] || 0; ret.entCount = playerState.entityCounts[entCategory] || 0; ret.entLimitChangers = playerState.entityLimitChangers[entCategory]; ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0); } return ret; } // Called by GUI when user clicks construction button // @param buildTemplate Template name of the entity the user wants to build function startBuildingPlacement(buildTemplate, playerState) { if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) return; // TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI - // to start building a structure, then the highlight selection rings are kept during the construction of the building. - // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing. + // to start building a structure, then the highlight selection rings are kept during the construction of the structure. + // Gives the impression that somehow the hovered-over entity has something to do with the structure you're building. placementSupport.Reset(); // find out if we're building a wall, and change the entity appropriately if so var templateData = GetTemplateData(buildTemplate); if (templateData.wallSet) { placementSupport.mode = "wall"; placementSupport.wallSet = templateData.wallSet; inputState = INPUT_BUILDING_PLACEMENT; } else { placementSupport.mode = "building"; placementSupport.template = buildTemplate; inputState = INPUT_BUILDING_PLACEMENT; } if (templateData.attack && templateData.attack.Ranged && templateData.attack.Ranged.maxRange) { // add attack information to display a good tooltip placementSupport.attack = templateData.attack; } } // Batch training: // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING // When the user releases shift, or clicks on a different training button, we create the batched units var g_BatchTrainingEntities; var g_BatchTrainingType; var g_NumberOfBatches; var g_BatchTrainingEntityAllowedCount; var g_BatchSize = getDefaultBatchTrainingSize(); function OnTrainMouseWheel(dir) { if (!Engine.HotkeyIsPressed("session.batchtrain")) return; g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio"); if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize)) g_BatchSize = 1; updateSelectionDetails(); } function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) { return entitiesToCheck.filter(entity => { let state = GetEntityState(entity); return state && state.production && state.production.entities.length && state.production.entities.indexOf(trainEntType) != -1; }); } function initBatchTrain() { registerConfigChangeHandler(changes => { if (changes.has("gui.session.batchtrainingsize")) updateDefaultBatchSize(); }); } function getDefaultBatchTrainingSize() { let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize"); return Number.isInteger(num) && num > 0 ? num : 5; } function getBatchTrainingSize() { return Math.max(Math.round(g_BatchSize), 1); } function updateDefaultBatchSize() { g_BatchSize = getDefaultBatchTrainingSize(); } // Add the unit shown at position to the training queue for all entities in the selection function addTrainingByPosition(position) { let playerState = GetSimState().players[Engine.GetPlayerID()]; let selection = g_Selection.toList(); if (!playerState || !selection.length) return; let trainableEnts = getAllTrainableEntitiesFromSelection(); let entToTrain = trainableEnts[position]; - // When we have no building to train or the position is invalid + // When we have no structure to train units or the position is invalid if (!entToTrain) return; addTrainingToQueue(selection, entToTrain, playerState); return; } // Called by GUI when user clicks training button function addTrainingToQueue(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; let decrement = Engine.HotkeyIsPressed("selection.remove"); let template; if (!decrement) template = GetTemplateData(trainEntType); // Batch training only possible if we can train at least 2 units if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) { if (inputState == INPUT_BATCHTRAINING) { - // Check if we are training in the same building(s) as the last batch + // Check if we are training in the same structure(s) as the last batch // NOTE: We just check if the arrays are the same and if the order is the same // If the order changed, we have a new selection and we should create a new batch. - // If we're already creating a batch of this unit (in the same building(s)), then just extend it + // If we're already creating a batch of this unit (in the same structure(s)), then just extend it // (if training limits allow) if (g_BatchTrainingEntities.length == selection.length && g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && g_BatchTrainingType == trainEntType) { if (decrement) { --g_NumberOfBatches; if (g_NumberOfBatches <= 0) inputState = INPUT_NORMAL; } else if (canBeAddedCount == undefined || canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) { if (Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) })) return; ++g_NumberOfBatches; } g_BatchTrainingEntityAllowedCount = canBeAddedCount; return; } // Otherwise start a new one else if (!decrement) flushTrainingBatch(); // fall through to create the new batch } // Don't start a new batch if decrementing or unable to afford it. if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, getBatchTrainingSize()) })) return; inputState = INPUT_BATCHTRAINING; g_BatchTrainingEntities = selection; g_BatchTrainingType = trainEntType; g_BatchTrainingEntityAllowedCount = canBeAddedCount; g_NumberOfBatches = 1; } else { - // Non-batched - just create a single entity in each building + // Non-batched - just create a single entity in each structure // (but no more than entity limit allows) let buildingsForTraining = appropriateBuildings; if (canBeAddedCount !== undefined) buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); Engine.PostNetworkCommand({ "type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining }); } } /** * Returns the number of units that will be present in a batch if the user clicks * the training button depending on the batch training modifier hotkey */ function getTrainingStatus(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let nextBatchTrainingCount = 0; let canBeAddedCount; if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType) { nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize(); canBeAddedCount = g_BatchTrainingEntityAllowedCount; } else canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; - // We need to calculate count after the next increment if it's possible + // We need to calculate count after the next increment if possible. if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && Engine.HotkeyIsPressed("session.batchtrain")) nextBatchTrainingCount += getBatchTrainingSize(); nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); - // If training limits don't allow us to train batchTrainingCount in each appropriate building - // train as many full batches as we can and remainer in one more building. + // If training limits don't allow us to train batchedSize in each appropriate structure, + // train as many full batches as we can and the remainder in one more structure. let buildingsCountToTrainFullBatch = appropriateBuildings.length; let remainderToTrain = 0; if (canBeAddedCount !== undefined && canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) { buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); remainderToTrain = canBeAddedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; } function flushTrainingBatch() { let batchedSize = g_NumberOfBatches * getBatchTrainingSize(); let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); - // If training limits don't allow us to train batchedSize in each appropriate building + // If training limits don't allow us to train batchedSize in each appropriate structure. if (g_BatchTrainingEntityAllowedCount !== undefined && g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) { - // Train as many full batches as we can + // Train as many full batches as we can. let buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / batchedSize); Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), "template": g_BatchTrainingType, "count": batchedSize }); - // Train remainer in one more building + // Train remainer in one more structure. let remainer = g_BatchTrainingEntityAllowedCount % batchedSize; if (remainer) Engine.PostNetworkCommand({ "type": "train", "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], "template": g_BatchTrainingType, "count": remainer }); } else Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings, "template": g_BatchTrainingType, "count": batchedSize }); } function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": var toSelect = []; g_Groups.update(); for (var ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) { let entState = GetEntityState(toSelect[0]); let position = entState.position; if (position && entState.visibility != "hidden") Engine.CameraMoveTo(position.x, position.z); } break; case "save": case "breakUp": g_Groups.groups[groupId].reset(); if (action == "save") g_Groups.addEntities(groupId, g_Selection.toList()); updateGroups(); break; } } var lastIdleUnit = 0; var currIdleClassIndex = 0; var lastIdleClasses = []; function resetIdleUnit() { lastIdleUnit = 0; currIdleClassIndex = 0; lastIdleClasses = []; } function findIdleUnit(classes) { var append = Engine.HotkeyIsPressed("selection.add"); var selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i])) resetIdleUnit(); lastIdleClasses = classes; var data = { "viewedPlayer": g_ViewedPlayer, "excludeUnits": append ? g_Selection.toList() : [], // If the current idle class index is not 0, put the class at that index first. "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) }; if (!selectall) { data.limit = 1; data.prevUnit = lastIdleUnit; } var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (!idleUnits.length) { // TODO: display a message or play a sound to indicate no more idle units, or something // Reset for next cycle resetIdleUnit(); return; } if (!append) g_Selection.reset(); g_Selection.addList(idleUnits); if (selectall) return; lastIdleUnit = idleUnits[0]; var entityState = GetEntityState(lastIdleUnit); var position = entityState.position; if (position) Engine.CameraMoveTo(position.x, position.z); // Move the idle class index to the first class an idle unit was found for. var indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; } function clearSelection() { if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING) { inputState = INPUT_NORMAL; placementSupport.Reset(); } else g_Selection.reset(); preSelectedAction = ACTION_NONE; } Index: ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/lobby/LobbyRatingReport/Buildings.js (revision 23366) @@ -1,23 +1,23 @@ /** - * This class reports the buildings built, lost and destroyed of some selected structure classes. + * This class reports the structures built, lost, and destroyed of some selected structure classes. */ LobbyRatingReport.prototype.Buildings = class { insertValues(report, playerStates) { let lower = txt => txt.substr(0, 1).toLowerCase() + txt.substr(1); let time = playerStates[0].sequences.time.length - 1; for (let buildingClass in playerStates[0].sequences.buildingsConstructed) report[lower(buildingClass) + "BuildingsConstructed"] = playerStates.map(playerState => playerState.sequences.buildingsConstructed[buildingClass][time]).join(",") + ","; for (let buildingClass in playerStates[0].sequences.buildingsLost) report[lower(buildingClass) + "BuildingsLost"] = playerStates.map(playerState => playerState.sequences.buildingsLost[buildingClass][time]).join(",") + ","; for (let buildingClass in playerStates[0].sequences.enemyBuildingsDestroyed) report["enemy" + buildingClass + "BuildingsDestroyed"] = playerStates.map(playerState => playerState.sequences.enemyBuildingsDestroyed[buildingClass][time]).join(",") + ","; } }; Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 23366) @@ -1,562 +1,562 @@ /** * All tutorial messages received so far. */ var g_TutorialMessages = []; /** * GUI tags applied to the most recent tutorial message. */ var g_TutorialNewMessageTags = { "color": "yellow" }; /** * These handlers are called everytime a client joins or disconnects. */ var g_PlayerAssignmentsChangeHandlers = new Set(); /** * These handlers are called when the ceasefire time has run out. */ var g_CeasefireEndedHandlers = new Set(); /** * These handlers are fired when the match is networked and * the current client established the connection, authenticated, * finished the loading screen, starts or finished synchronizing after a rejoin. * The messages are constructed in NetClient.cpp. */ var g_NetworkStatusChangeHandlers = new Set(); /** * These handlers are triggered whenever a client finishes the loading screen. */ var g_ClientsLoadingHandlers = new Set(); /** * These handlers are fired if the server informed the players that the networked game is out of sync. */ var g_NetworkOutOfSyncHandlers = new Set(); /** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => { handleNetStatusMessage(msg); }, "netwarn": msg => { addNetworkWarning(msg); }, "out-of-sync": msg => { for (let handler of g_NetworkOutOfSyncHandlers) handler(msg); }, "players": msg => { handlePlayerAssignmentsMessage(msg); }, "paused": msg => { g_PauseControl.setClientPauseState(msg.guid, msg.pause); }, "clients-loading": msg => { for (let handler of g_ClientsLoadingHandlers) handler(msg.guids); }, "rejoined": msg => { addChatMessage({ "type": "rejoined", "guid": msg.guid }); }, "kicked": msg => { addChatMessage({ "type": "kicked", "username": msg.username, "banned": msg.banned }); }, "chat": msg => { addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }); }, "gamesetup": msg => {}, // Needed for autostart "start": msg => {} }; var g_PlayerStateMessages = { "won": translate("You have won!"), "defeated": translate("You have been defeated!") }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. * Don't open new pages (message boxes) here! Otherwise further notifications * handled in the same turn can't access the GUI objects anymore. */ var g_NotificationsTypes = { "aichat": function(notification, player) { let message = { "type": "message", "text": notification.message, "guid": findGuidForPlayerID(player) || -1, "player": player, "translate": true }; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; colorizePlayernameParameters(notification.parameters); } addChatMessage(message); }, "defeat": function(notification, player) { playersFinished(notification.allies, notification.message, false); }, "won": function(notification, player) { playersFinished(notification.allies, notification.message, true); }, "diplomacy": function(notification, player) { updatePlayerData(); g_DiplomacyColors.onDiplomacyChange(); addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); }, "ceasefire-ended": function(notification, player) { updatePlayerData(); for (let handler of g_CeasefireEndedHandlers) handler(); }, "tutorial": function(notification, player) { updateTutorial(notification); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "barter": function(notification, player) { addChatMessage({ "type": "barter", "player": player, "amountsSold": notification.amountsSold, "amountsBought": notification.amountsBought, "resourceSold": notification.resourceSold, "resourceBought": notification.resourceBought }); }, "spy-response": function(notification, player) { g_DiplomacyDialog.onSpyResponse(notification, player); if (notification.entity && g_ViewedPlayer == player) { g_DiplomacyDialog.close(); setCameraFollow(notification.entity); } }, "attack": function(notification, player) { if (player != g_ViewedPlayer) return; // Focus camera on attacks if (g_FollowPlayer) { setCameraFollow(notification.target); g_Selection.reset(); if (notification.target) g_Selection.addList([notification.target]); } if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.attack") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "phase": function(notification, player) { addChatMessage({ "type": "phase", "player": player, "phaseName": notification.phaseName, "phaseState": notification.phaseState }); }, "dialog": function(notification, player) { if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, "resetselectionpannel": function(notification, player) { if (player != Engine.GetPlayerID()) return; g_Selection.rebuildSelection({}); }, "playercommand": function(notification, player) { // For observers, focus the camera on units commanded by the selected player if (!g_FollowPlayer || player != g_ViewedPlayer) return; let cmd = notification.cmd; // Ignore rallypoint commands of trained animals let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]); if (g_ViewedPlayer != 0 && entState && entState.identity && entState.identity.classes && entState.identity.classes.indexOf("Animal") != -1) return; - // Focus the building to construct + // Focus the structure to build. if (cmd.type == "repair") { let targetState = GetEntityState(cmd.target); if (targetState) Engine.CameraMoveTo(targetState.position.x, targetState.position.z); } else if (cmd.type == "delete-entities" && notification.position) Engine.CameraMoveTo(notification.position.x, notification.position.y); // Focus commanded entities, but don't lose previous focus when training units else if (cmd.type != "train" && cmd.type != "research" && entState) setCameraFollow(cmd.entities[0]); if (["walk", "attack-walk", "patrol"].indexOf(cmd.type) != -1) DrawTargetMarker(cmd); // Select units affected by that command let selection = []; if (cmd.entities) selection = cmd.entities; if (cmd.target) selection.push(cmd.target); // Allow gaia in selection when gathering g_Selection.reset(); g_Selection.addList(selection, false, cmd.type == "gather"); }, "play-tracks": function(notification, player) { if (notification.lock) { global.music.storeTracks(notification.tracks.map(track => ({ "Type": "custom", "File": track }))); global.music.setState(global.music.states.CUSTOM); } global.music.setLocked(notification.lock); } }; function registerPlayerAssignmentsChangeHandler(handler) { g_PlayerAssignmentsChangeHandlers.add(handler); } function registerCeasefireEndedHandler(handler) { g_CeasefireEndedHandlers.add(handler); } function registerNetworkOutOfSyncHandler(handler) { g_NetworkOutOfSyncHandlers.add(handler); } function registerNetworkStatusChangeHandler(handler) { g_NetworkStatusChangeHandlers.add(handler); } function registerClientsLoadingHandler(handler) { g_ClientsLoadingHandlers.add(handler); } function findGuidForPlayerID(playerID) { return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID); } /** * Processes all pending notifications sent from the GUIInterface simulation component. */ function handleNotifications() { for (let notification of Engine.GuiInterfaceCall("GetNotifications")) { if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type]) { error("Invalid GUI notification: " + uneval(notification)); continue; } for (let player of notification.players) g_NotificationsTypes[notification.type](notification, player); } } function toggleTutorial() { let tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel"); tutorialPanel.hidden = !tutorialPanel.hidden || !Engine.GetGUIObjectByName("tutorialText").caption; } /** * Updates the tutorial panel when a new goal. */ function updateTutorial(notification) { // Show the tutorial panel if not yet done Engine.GetGUIObjectByName("tutorialPanel").hidden = false; if (notification.warning) { Engine.GetGUIObjectByName("tutorialWarning").caption = coloredText(translate(notification.warning), "orange"); return; } let notificationText = notification.instructions.reduce((instructions, item) => instructions + (typeof item == "string" ? translate(item) : colorizeHotkey(translate(item.text), item.hotkey)), ""); Engine.GetGUIObjectByName("tutorialText").caption = g_TutorialMessages.concat(setStringTags(notificationText, g_TutorialNewMessageTags)).join("\n"); g_TutorialMessages.push(notificationText); if (notification.readyButton) { Engine.GetGUIObjectByName("tutorialReady").hidden = false; if (notification.leave) { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click to quit this tutorial."); Engine.GetGUIObjectByName("tutorialReady").caption = translate("Quit"); Engine.GetGUIObjectByName("tutorialReady").onPress = endGame; } else Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click when ready."); } else { Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Follow the instructions."); Engine.GetGUIObjectByName("tutorialReady").hidden = true; } } /** * Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. */ function handleNetMessages() { while (true) { let msg = Engine.PollNetworkClient(); if (!msg) return; log("Net message: " + uneval(msg)); if (g_NetMessageTypes[msg.type]) g_NetMessageTypes[msg.type](msg); else error("Unrecognised net message type '" + msg.type + "'"); } } function handleNetStatusMessage(message) { if (g_Disconnected) return; g_IsNetworkedActive = message.status == "active"; if (message.status == "disconnected") { g_Disconnected = true; updateCinemaPath(); closeOpenDialogs(); } for (let handler of g_NetworkStatusChangeHandlers) handler(message); } function handlePlayerAssignmentsMessage(message) { for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) onClientLeave(guid); let joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.newAssignments; joins.forEach(guid => { onClientJoin(guid); }); for (let handler of g_PlayerAssignmentsChangeHandlers) handler(); // TODO: use subscription instead updateGUIObjects(); } function onClientJoin(guid) { let playerID = g_PlayerAssignments[guid].player; if (g_Players[playerID]) { g_Players[playerID].guid = guid; g_Players[playerID].name = g_PlayerAssignments[guid].name; g_Players[playerID].offline = false; } addChatMessage({ "type": "connect", "guid": guid }); } function onClientLeave(guid) { g_PauseControl.setClientPauseState(guid, false); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; addChatMessage({ "type": "disconnect", "guid": guid }); } function addChatMessage(msg) { g_Chat.ChatMessageHandler.handleMessage(msg); } function clearChatMessages() { g_Chat.ChatOverlay.clearChatMessages(); } /** * This function is used for AIs, whose names don't exist in g_PlayerAssignments. */ function colorizePlayernameByID(playerID) { let username = g_Players[playerID] && escapeText(g_Players[playerID].name); return colorizePlayernameHelper(username, playerID); } function colorizePlayernameByGUID(guid) { let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : ""; let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; return colorizePlayernameHelper(username, playerID); } function colorizePlayernameHelper(username, playerID) { let playerColor = playerID > -1 ? g_DiplomacyColors.getPlayerColor(playerID) : "white"; return coloredText(username || translate("Unknown Player"), playerColor); } /** * Insert the colorized playername to chat messages sent by the AI and time notifications. */ function colorizePlayernameParameters(parameters) { for (let param in parameters) if (param.startsWith("_player_")) parameters[param] = colorizePlayernameByID(parameters[param]); } /** * Custom dialog response handling, usable by trigger maps. */ function sendDialogAnswer(guiObject, dialogName) { Engine.GetGUIObjectByName(dialogName + "-dialog").hidden = true; Engine.PostNetworkCommand({ "type": "dialog-answer", "dialog": dialogName, "answer": guiObject.name.split("-").pop(), }); resumeGame(); } /** * Custom dialog opening, usable by trigger maps. */ function openDialog(dialogName, data, player) { let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog"); if (!dialog) { warn("messages.js: Unknow dialog with name " + dialogName); return; } dialog.hidden = false; for (let objName in data) { let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName); if (!obj) { warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog."); continue; } for (let key in data[objName]) { let n = data[objName][key]; if (typeof n == "object" && n.message) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); obj[key] = sprintf(message, parameters); } else obj[key] = n; } } g_PauseControl.implicitPause(); } Index: ps/trunk/binaries/data/mods/public/gui/session/placement.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/placement.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/placement.js (revision 23366) @@ -1,42 +1,42 @@ function PlacementSupport() { this.Reset(); } PlacementSupport.DEFAULT_ANGLE = Math.PI * 3 / 4; /** - * Resets the building placement support state. Use this to cancel construction of an entity. + * Reset the structure placement support state. Use this to cancel construction of an entity. */ PlacementSupport.prototype.Reset = function() { this.mode = null; this.position = null; this.template = null; - this.tooltipMessage = ""; // tooltip text to show while the user is placing a building + this.tooltipMessage = ""; // tooltip text to show while the user is placing a structure this.tooltipError = false; this.wallSet = null; // maps types of wall pieces ("tower", "long", "short", ...) to template names this.wallSnapEntities = null; // list of candidate entities to snap the starting and (!) ending positions to when building walls this.wallEndPosition = null; this.wallSnapEntitiesIncludeOffscreen = false; // should the next update of the snap candidate list include offscreen towers? this.SetDefaultAngle(); this.RandomizeActorSeed(); this.attack = null; Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": "" }); Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": null }); }; PlacementSupport.prototype.SetDefaultAngle = function() { this.angle = PlacementSupport.DEFAULT_ANGLE; }; PlacementSupport.prototype.RandomizeActorSeed = function() { this.actorSeed = randIntExclusive(0, Math.pow(2, 16)); }; var placementSupport = new PlacementSupport(); Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 23366) @@ -1,1190 +1,1190 @@ /** * 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 4; }, "rowLength": 4, "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 = g_EntityCommands[command].getInfo(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 = g_IsObserver && data.item.name == "focus-rally" || controlsPlayer(data.player) && (data.item.name != "delete" || data.unitEntStates.some(state => !isUndeletable(state))); 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.AllyCommand = { "getMaxNumberOfItems": function() { return 2; }, "conflictsWith": ["Command"], "getItems": function(unitEntStates) { let commands = []; for (let command in g_AllyEntityCommands) for (let state of unitEntStates) { let info = g_AllyEntityCommands[command].getInfo(state); if (info) { info.name = command; commands.push(info); break; } } 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 performAllyCommand(data.unitEntStates[0].id, data.item.name); }; data.countDisplay.caption = data.item.count || ""; data.button.enabled = !!data.item.count; let grayscale = data.button.enabled ? "" : "grayscale:"; data.icon.sprite = "stretched:" + grayscale + "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 24 - getNumberOfRightPanelButtons(); }, "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); }; data.button.onPressRight = function() { showTemplateDetails(data.item); }; let tooltips = [ getEntityNamesFormatted, getVisibleEntityClassesFormatted, getAurasTooltip, getEntityTooltip ].map(func => func(template)); tooltips.push( getEntityCostTooltip(template, data.player), getGarrisonTooltip(template), getPopulationBonusTooltip(template), showTemplateViewerOnRightClickTooltip(template) ); let limits = getEntityLimitAndCount(data.playerState, data.item); tooltips.push( formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), 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 16; }, "rowLength": 4, "conflictsWith": ["Garrison"], "getItems": function(unitEntStates) { if (unitEntStates.some(state => !hasClass(state, "Unit"))) return []; if (!g_AvailableFormations.has(unitEntStates[0].player)) g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player)); let availableFormations = g_AvailableFormations.get(unitEntStates[0].player); // Hide the panel if all formations are disabled if (availableFormations.some(formation => canMoveSelectionIntoFormation(formation))) return availableFormations; return []; }, "setupButton": function(data) { if (!g_FormationsInfo.has(data.item)) g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); let formationInfo = g_FormationsInfo.get(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); }; let tooltip = translate(formationInfo.name); 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.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 = g_ViewedPlayer == data.player || g_ViewedPlayer == entState.player; data.button.enabled = canUngarrison && controlsPlayer(g_ViewedPlayer); data.button.tooltip = (canUngarrison || g_IsObserver ? 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 building. + // 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 24 - getNumberOfRightPanelButtons(); }, "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 24 - getNumberOfRightPanelButtons(); }, "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) { let queue = []; let foundNew = true; for (let i = 0; foundNew; ++i) { foundNew = false; for (let state of unitEntStates) { if (!state.production || !state.production.queue[i]) continue; queue.push({ "producingEnt": state.id, "queuedItem": state.production.queue[i] }); foundNew = true; } } 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); }; let tooltip = getEntityNames(template); if (queuedItem.neededSlots) { tooltip += "\n" + coloredText(translate("Insufficient population capacity:"), "red"); tooltip += "\n" + sprintf(translate("%(population)s %(neededSlots)s"), { "population": resourceIcon("population"), "neededSlots": queuedItem.neededSlots }); } data.button.tooltip = tooltip; data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : ""; // 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")); let guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]"); let 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; if (template.icon) data.icon.sprite = "stretched:session/portraits/" + template.icon; data.button.enabled = controlsPlayer(data.player); setPanelObjectPosition(data.button, data.i, data.rowLength); return true; } }; g_SelectionPanels.Research = { "getMaxNumberOfItems": function() { return 8; }, "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 })); for (let state of unitEntStates) { 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 }))); } 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; 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": 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); button.onPressRight = (t => function () { showTemplateDetails( t, GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv); })(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 (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); }; 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 24 - getNumberOfRightPanelButtons(); }, "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); }; data.button.onPressRight = function() { showTemplateDetails(data.item); }; 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)); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") tooltips = tooltips.concat([ getHealthTooltip, getAttackTooltip, getSplashDamageTooltip, getHealerTooltip, getArmorTooltip, getGarrisonTooltip, getProjectilesTooltip, getSpeedTooltip ].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 (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 24 - getNumberOfRightPanelButtons(); }, "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 technologyEnabled = true; if (data.item.requiredTechnology) technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": data.item.requiredTechnology, "player": data.player }); let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(data.item, data.unitEntStates.length), "player": data.player }); let limits = getEntityLimitAndCount(data.playerState, data.item.entity); let progress = data.unitEntStates[0].upgrade.progress || 0; let isUpgrading = data.unitEntStates[0].upgrade.template == data.item.entity; let tooltip; if (!progress) { let tooltips = []; if (data.item.tooltip) tooltips.push(sprintf(translate("Upgrade to %(name)s. %(tooltip)s"), { "name": template.name.generic, "tooltip": translate(data.item.tooltip) })); else tooltips.push(sprintf(translate("Upgrade to %(name)s."), { "name": template.name.generic })); tooltips.push( getEntityCostComponentsTooltipString(data.item, undefined, data.unitEntStates.length), formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), 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); }; } else if (isUpgrading) { tooltip = translate("Cancel Upgrading"); data.button.onPress = function() { cancelUpgradeEntity(); }; } else { tooltip = translate("Cannot upgrade when the entity is already upgrading."); data.button.onPress = function() {}; } data.button.enabled = controlsPlayer(data.player); data.button.tooltip = tooltip; data.button.onPressRight = function() { showTemplateDetails(data.item.entity); }; let modifier = ""; if (!isUpgrading) if (progress || !technologyEnabled || limits.canBeAddedCount == 0 && !hasSameRestrictionCategory(data.item.entity, data.unitEntStates[0].template)) { data.button.enabled = false; modifier = "color:0 0 0 127:grayscale:"; } else if (neededResources) { data.button.enabled = false; modifier = resourcesToAlphaMask(neededResources) + ":"; } data.icon.sprite = modifier + "stretched:session/" + (data.item.icon || "portraits/" + template.icon); data.countDisplay.caption = data.unitEntStates.length > 1 ? data.unitEntStates.length : ""; let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]"); if (isUpgrading) { let size = progressOverlay.size; size.top = size.left + Math.round(progress * (size.right - size.left)); progressOverlay.size = size; } progressOverlay.hidden = !isUpgrading; 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) { 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", "AllyCommand", "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 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js (revision 23366) @@ -1,479 +1,479 @@ /** * @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 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 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 building with full batch or one building with the full batch and + // 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) { g_Selection.makePrimarySelection(templateName, Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup); } function performCommand(entStates, commandName) { if (!entStates.length) return; // Don't check all entities, because we assume a player cannot // select entities from more than one player if (!controlsPlayer(entStates[0].player) && !(g_IsObserver && commandName == "focus-rally")) return; if (g_EntityCommands[commandName]) g_EntityCommands[commandName].execute(entStates); } function performAllyCommand(entity, commandName) { if (!entity) return; let entState = GetEntityState(entity); let playerState = GetSimState().players[Engine.GetPlayerID()]; if (!playerState.isMutualAlly[entState.player] || g_IsObserver) return; if (g_AllyEntityCommands[commandName]) g_AllyEntityCommands[commandName].execute(entState); } function performFormation(entities, formationTemplate) { if (!entities) return; Engine.PostNetworkCommand({ "type": "formation", "entities": entities, "name": 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) { Engine.PostNetworkCommand({ "type": "upgrade", "entities": g_Selection.toList(), "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.toList().filter(ent => { let state = GetEntityState(ent); return state && !!state.garrisonHolder; }) }); } function unloadSelection() { let parent = 0; let ents = []; for (let ent in g_Selection.selected) { let state = GetEntityState(+ent); if (!state || !state.turretParent) continue; if (!parent) { parent = state.turretParent; ents.push(+ent); } else if (state.turretParent == parent) ents.push(+ent); } if (parent) Engine.PostNetworkCommand({ "type": "unload", "entities": ents, "garrisonHolder": parent }); } function unloadAll() { let garrisonHolders = g_Selection.toList().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 backToWork() { Engine.PostNetworkCommand({ "type": "back-to-work", // Filter out all entities that can't go back to work. "entities": g_Selection.toList().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.toList().filter(ent => { let state = GetEntityState(ent); return state && state.unitAI && state.unitAI.isGuarding; }) }); } function raiseAlert() { Engine.PostNetworkCommand({ "type": "alert-raise", "entities": g_Selection.toList().filter(ent => { let state = GetEntityState(ent); return state && !!state.alertRaiser; }) }); } function endOfAlert() { Engine.PostNetworkCommand({ "type": "alert-end", "entities": g_Selection.toList().filter(ent => { let state = GetEntityState(ent); return state && !!state.alertRaiser; }) }); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23366) @@ -1,806 +1,806 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_CivData = loadCivData(false, true); const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; var g_Ambient; var g_Chat; var g_Cheats; var g_DeveloperOverlay; var g_DiplomacyColors; var g_DiplomacyDialog; var g_GameSpeedControl; var g_Menu; var g_MiniMapPanel; var g_NetworkStatusOverlay; var g_ObjectivesDialog; var g_OutOfSyncNetwork; var g_OutOfSyncReplay; var g_PanelEntityManager; var g_PauseControl; var g_PauseOverlay; var g_PlayerViewControl; var g_QuitConfirmationDefeat; var g_QuitConfirmationReplay; var g_RangeOverlayManager; var g_ResearchProgress; var g_TimeNotificationOverlay; var g_TopPanel; var g_TradeDialog; /** * Map, player and match settings set in gamesetup. */ const g_GameAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes")); /** * True if this is a multiplayer game. */ const g_IsNetworked = Engine.HasNetClient(); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController = !g_IsNetworked || Engine.HasNetServer(); /** * Whether we have finished the synchronization and * can start showing simulation related message boxes. */ var g_IsNetworkedActive = false; /** * True if the connection to the server has been lost. */ var g_Disconnected = false; /** * True if the current user has observer capabilities. */ var g_IsObserver = false; /** * True if the current user has rejoined (or joined the game after it started). */ var g_HasRejoined = false; /** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); /** * True if the camera should focus on attacks and player commands * and select the affected units. */ var g_FollowPlayer = false; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var g_LastTickTime = Date.now(); /** * Recalculate which units have their status bars shown with this frequency in milliseconds. */ var g_StatusBarUpdate = 200; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; /** * Remembers which clients are assigned to which player slots. * The keys are GUIDs or "local" in single-player. */ var g_PlayerAssignments; /** * Whether the entire UI should be hidden (useful for promotional screenshots). * Can be toggled with a hotkey. */ var g_ShowGUI = true; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_ResourceData = new Resources(); /** * These handlers are called each time a new turn was simulated. * Use this as sparely as possible. */ var g_SimulationUpdateHandlers = new Set(); /** * These handlers are called after the player states have been initialized. */ var g_PlayersInitHandlers = new Set(); /** * These handlers are called when a player has been defeated or won the game. */ var g_PlayerFinishedHandlers = new Set(); /** * These events are fired whenever the player added or removed entities from the selection. */ var g_EntitySelectionChangeHandlers = new Set(); /** * These events are fired when the user has performed a hotkey assignment change. * Currently only fired on init, but to be fired from any hotkey editor dialog. */ var g_HotkeyChangeHandlers = new Set(); /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Order in which the panel entities are shown. */ var g_PanelEntityOrder = ["Hero", "Relic"]; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"]; /** * Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey. */ var g_MilitaryTypes = ["Melee", "Ranged"]; function GetSimState() { if (!g_SimState) g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState")); return g_SimState; } function GetMultipleEntityStates(ents) { if (!ents.length) return null; let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents); for (let item of entityStates) g_EntityStates[item.entId] = item.state && deepfreeze(item.state); return entityStates; } function GetEntityState(entId) { if (!g_EntityStates[entId]) { let entityState = Engine.GuiInterfaceCall("GetEntityState", entId); g_EntityStates[entId] = entityState && deepfreeze(entityState); } return g_EntityStates[entId]; } /** * Returns template data calling GetTemplateData defined in GuiInterface.js * and deepfreezing returned object. * @param {string} templateName - Data of this template will be returned. * @param {number|undefined} owner - Modifications of this player will be applied to the template. * If undefined, id of player calling this method will be used. */ function GetTemplateData(templateName, owner) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "owner": owner }); translateObjectKeys(template, ["specific", "generic", "tooltip"]); g_TemplateData[templateName] = deepfreeze(template); } return g_TemplateData[templateName]; } function GetTechnologyData(technologyName, civ) { if (!g_TechnologyData[civ]) g_TechnologyData[civ] = {}; if (!(technologyName in g_TechnologyData[civ])) { let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); g_TechnologyData[civ][technologyName] = deepfreeze(template); } return g_TechnologyData[civ][technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } // Fallback used by atlas g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } }; // Fallback used by atlas and autostart games if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name) g_PlayerAssignments.local.name = singleplayerName(); if (initData) { g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); } g_Cheats = new Cheats(); g_DiplomacyColors = new DiplomacyColors(); g_PlayerViewControl = new PlayerViewControl(); g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects); g_PauseControl = new PauseControl(); g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay); g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates); g_Ambient = new Ambient(); g_Chat = new Chat(g_PlayerViewControl, g_Cheats); g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection); g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors); g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl); g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes); g_NetworkStatusOverlay = new NetworkStatusOverlay(); g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl); g_OutOfSyncNetwork = new OutOfSyncNetwork(); g_OutOfSyncReplay = new OutOfSyncReplay(); g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder); g_PauseOverlay = new PauseOverlay(g_PauseControl); g_QuitConfirmationDefeat = new QuitConfirmationDefeat(); g_QuitConfirmationReplay = new QuitConfirmationReplay(); g_RangeOverlayManager = new RangeOverlayManager(g_Selection); g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection); g_TradeDialog = new TradeDialog(g_PlayerViewControl); g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl); g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl); initBatchTrain(); initSelectionPanels(); LoadModificationTemplates(); updatePlayerData(); initializeMusic(); // before changing the perspective Engine.SetBoundingBoxDebugOverlay(false); for (let handler of g_PlayersInitHandlers) handler(); for (let handler of g_HotkeyChangeHandlers) handler(); if (hotloadData) { g_Selection.selected = hotloadData.selection; g_PlayerAssignments = hotloadData.playerAssignments; g_Players = hotloadData.player; } // TODO: use event instead onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); } function registerPlayersInitHandler(handler) { g_PlayersInitHandlers.add(handler); } function registerPlayersFinishedHandler(handler) { g_PlayerFinishedHandlers.add(handler); } function registerSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.add(handler); } function unregisterSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.delete(handler); } function registerEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.add(handler); } function unregisterEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.delete(handler); } function registerHotkeyChangeHandler(handler) { g_HotkeyChangeHandlers.add(handler); } function updatePlayerData() { let simState = GetSimState(); if (!simState) return; let playerData = []; for (let i = 0; i < simState.players.length; ++i) { let playerState = simState.players[i]; playerData.push({ "name": playerState.name, "civ": playerState.civ, "color": { "r": playerState.color.r * 255, "g": playerState.color.g * 255, "b": playerState.color.b * 255, "a": playerState.color.a * 255 }, "team": playerState.team, "teamsLocked": playerState.teamsLocked, "cheatsEnabled": playerState.cheatsEnabled, "state": playerState.state, "isAlly": playerState.isAlly, "isMutualAlly": playerState.isMutualAlly, "isNeutral": playerState.isNeutral, "isEnemy": playerState.isEnemy, "guid": undefined, // network guid for players controlled by hosts "offline": g_Players[i] && !!g_Players[i].offline }); } for (let guid in g_PlayerAssignments) { let playerID = g_PlayerAssignments[guid].player; if (!playerData[playerID]) continue; playerData[playerID].guid = guid; playerData[playerID].name = g_PlayerAssignments[guid].name; } g_Players = playerData; } /** * Returns the entity itself except when garrisoned where it returns its garrisonHolder */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length && entState.unitAI.orders[0].type == "Garrison") return getEntityOrHolder(entState.unitAI.orders[0].data.target); return ent; } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); } function resetTemplates() { // Update GUI and clear player-dependent cache g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); // TODO: do this more selectively onSimulationUpdate(); } /** * Returns true if the player with that ID is in observermode. */ function isPlayerObserver(playerID) { let playerStates = GetSimState().players; return !playerStates[playerID] || playerStates[playerID].state != "active"; } /** * Returns true if the current user can issue commands for that player. */ function controlsPlayer(playerID) { let playerStates = GetSimState().players; return !!playerStates[Engine.GetPlayerID()] && playerStates[Engine.GetPlayerID()].controlsAll || Engine.GetPlayerID() == playerID && !!playerStates[playerID] && playerStates[playerID].state != "defeated"; } /** * Called when one or more players have won or were defeated. * * @param {array} - IDs of the players who have won or were defeated. * @param {object} - a plural string stating the victory reason. * @param {boolean} - whether these players have won or lost. */ function playersFinished(players, victoryString, won) { addChatMessage({ "type": "playerstate", "message": victoryString, "players": players }); updatePlayerData(); // TODO: The other calls in this function should move too for (let handler of g_PlayerFinishedHandlers) handler(players, won); if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning()) return; global.music.setState( won ? global.music.states.VICTORY : global.music.states.DEFEAT ); } function resumeGame() { g_PauseControl.implicitResume(); } function closeOpenDialogs() { g_Menu.close(); g_Chat.closePage(); g_DiplomacyDialog.close(); g_ObjectivesDialog.close(); g_TradeDialog.close(); } function endGame() { // Before ending the game let replayDirectory = Engine.GetCurrentReplayDirectory(); let simData = Engine.GuiInterfaceCall("GetReplayMetadata"); let playerID = Engine.GetPlayerID(); Engine.EndGame(); // After the replay file was closed in EndGame // Done here to keep EndGame small if (!g_IsReplay) Engine.AddReplayToCache(replayDirectory); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_summary.xml", { "sim": simData, "gui": { "dialog": false, "assignedPlayer": playerID, "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected, "playerAssignments": g_PlayerAssignments, "player": g_Players, }; } function getSavedGameData() { return { "groups": g_Groups.groups }; } function restoreSavedGameData(data) { // Restore camera if any if (data.camera) Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ, data.camera.RotX, data.camera.RotY, data.camera.Zoom); // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (let groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } /** * Called every frame. */ function onTick() { if (!g_Settings) return; let now = Date.now(); let tickLength = now - g_LastTickTime; g_LastTickTime = now; handleNetMessages(); updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; // When selection changed, get the entityStates of new entities GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId])); for (let handler of g_EntitySelectionChangeHandlers) handler(); updateGUIObjects(); - // Display rally points for selected buildings + // Display rally points for selected structures. if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); updateTimers(); Engine.GuiInterfaceCall("ClearRenamedEntities"); } function onSimulationUpdate() { // Templates change depending on technologies and auras, so they have to be reloaded after such a change. // g_TechnologyData data never changes, so it shouldn't be deleted. g_EntityStates = {}; if (Engine.GuiInterfaceCall("IsTemplateModified")) { g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); } g_SimState = undefined; if (!GetSimState()) return; GetMultipleEntityStates(g_Selection.toList()); for (let handler of g_SimulationUpdateHandlers) handler(); // TODO: Move to handlers updateCinemaPath(); handleNotifications(); updateGUIObjects(); } function toggleGUI() { g_ShowGUI = !g_ShowGUI; updateCinemaPath(); } function updateCinemaPath() { let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath; Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true"); } // TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateGroups(); updateSelectionDetails(); updateBuildingPlacementPreview(); if (!g_IsObserver) { // Update music state on basis of battle state. let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer); if (battleState) global.music.setState(global.music.states[battleState]); } } function updateGroups() { g_Groups.update(); // Determine the sum of the costs of a given template let getCostSum = (ent) => { let cost = GetTemplateData(GetEntityState(ent).template).cost; return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0; }; for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children) { Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i; let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]"); button.hidden = g_Groups.groups[i].getTotalCount() == 0; button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i); button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i); button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i); // Choose the icon of the most common template (or the most costly if it's not unique) if (g_Groups.groups[i].getTotalCount() > 0) { let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => { if (pre.ents.length == cur.ents.length) return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur; return pre.ents.length > cur.ents.length ? pre : cur; }).ents[0]).template).icon; Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite = icon ? ("stretched:session/portraits/" + icon) : "groupsIcon"; } setPanelObjectPosition(button, i, 1); } } /** * Toggles the display of status bars for all of the player's entities. * * @param {Boolean} remove - Whether to hide all previously shown status bars. */ function recalculateStatusBarDisplay(remove = false) { let entities; if (g_ShowAllStatusBars && !remove) entities = g_ViewedPlayer == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer); else { let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); // Remove selected entities from the 'all entities' array, // to avoid disabling their status bars. entities = Engine.GuiInterfaceCall( g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", { "viewedPlayer": g_ViewedPlayer }).filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars && !remove, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } function removeStatusBarDisplay() { if (g_ShowAllStatusBars) recalculateStatusBarDisplay(true); } /** * Inverts the given configuration boolean and returns the current state. * For example "silhouettes". */ function toggleConfigBool(configName) { let enabled = Engine.ConfigDB_GetValue("user", configName) != "true"; Engine.ConfigDB_CreateAndWriteValueToFile("user", configName, String(enabled), "config/user.cfg"); return enabled; } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { let entsAdd = []; // list of entities units to be highlighted let entsRemove = []; let highlighted = g_Selection.toList(); for (let ent in g_Selection.highlighted) highlighted.push(g_Selection.highlighted[ent]); if (g_ShowGuarding) // flag the guarding entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.guard || !state.guard.entities.length) continue; for (let ent of state.guard.entities) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } if (g_ShowGuarded) // flag the guarded entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.unitAI || !state.unitAI.isGuarding) continue; let ent = state.unitAI.isGuarding; if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } // flag the entities to remove (from the previously added) from this additional highlight for (let ent of g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd, g_HighlightedAlpha, true); _setHighlight(entsRemove, 0, false); g_AdditionalHighlight = entsAdd; } Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 23365) +++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 23366) @@ -1,111 +1,111 @@