Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 26075) +++ ps/trunk/binaries/data/config/default.cfg (revision 26076) @@ -1,576 +1,577 @@ ; Global Configuration Settings ; ; ************************************************************** ; * DO NOT EDIT THIS FILE if you want personal customisations: * ; * create a text file called "local.cfg" instead, and copy * ; * the lines from this file that you want to change. * ; * * ; * If a setting is part of a section (for instance [hotkey]) * ; * you need to append the section name at the beginning of * ; * your custom line (for instance you need to write * ; * "hotkey.pause = Space" if you want to change the pausing * ; * hotkey to the spacebar). * ; * * ; * On Linux, create: * ; * $XDG_CONFIG_HOME/0ad/config/local.cfg * ; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) * ; * * ; * On OS X, create: * ; * ~/Library/Application\ Support/0ad/config/local.cfg * ; * * ; * On Windows, create: * ; * %appdata%\0ad\config\local.cfg * ; * * ; ************************************************************** ; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.) windowed = false ; Show detailed tooltips (Unit stats) showdetailedtooltips = false ; Pause the game on window focus loss (Only applicable to single player mode) pauseonfocusloss = true ; Persist settings after leaving the game setup screen persistmatchsettings = true ; Default player name to use in multiplayer ; playername = "anonymous" ; Default server name or IP to use in multiplayer multiplayerserver = "127.0.0.1" ; Force a particular resolution. (If these are 0, the default is ; to keep the current desktop resolution in fullscreen mode or to ; use 1024x768 in windowed mode.) xres = 0 yres = 0 ; Force a non-standard bit depth (if 0 then use the current desktop bit depth) bpp = 0 ; Preferred display (for multidisplay setups, only works with SDL 2.0) display = 0 ; Enable Hi-DPI where supported, currently working only for testing. hidpi = false ; Allows to force GL version for SDL forceglversion = false forceglprofile = "compatibility" ; Possible values: compatibility, core, es forceglmajorversion = 3 forceglminorversion = 3 ; Big screenshot tiles screenshot.tiles = 8 screenshot.tilewidth = 480 screenshot.tileheight = 270 ; Emulate right-click with Ctrl+Click on Mac mice macmouse = false ; System settings: ; if false, actors won't be rendered but anything entity will be. renderactors = true watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly. waterfancyeffects = false waterrealdepth = true waterrefraction = true waterreflection = true shadows = true shadowquality = 0 ; Shadow map resolution. (-1 - Low, 0 - Medium, 1 - High, 2 - Very High) ; High values can crash the game when using a graphics card with low memory! shadowpcf = true ; Increases details closer to the camera but decreases performance ; especially on low hardware. shadowscascadecount = 1 shadowscascadedistanceratio = 1.7 ; Hides shadows after the distance. shadowscutoffdistance = 300.0 ; If true shadows cover the whole map instead of the camera frustum. shadowscovermap = false texturequality = 5 ; Texture resolution and quality (0 - Lowest, 1 Very Low, 2 - Low, 3 - Medium, 4 - High, 5 - Very High, 6 - Ultra) vsync = false particles = true fog = true silhouettes = true showsky = true ; Uses a synchonized call to a GL driver to get an error state. Useful ; for a debugging of a system without GL_KHR_debug. gl.checkerrorafterswap = false ; Different ways to draw a cursor, possible values are "sdl" and "system". ; The "system" one doesn't support a visual change of the cursor. cursorbackend = "sdl" ; Backends for all graphics rendering: ; glarb - GL with legacy assembler-like shaders, might used only for buggy drivers. ; gl - GL with GLSL shaders, should be used by default. rendererbackend = "gl" ; Should not be edited. It's used only for preventing of running fixed pipeline. renderpath = default ;;;;; EXPERIMENTAL ;;;;; ; Experimental probably-non-working GPU skinning support; requires GLSL; use at own risk gpuskinning = false ; Use smooth LOS interpolation smoothlos = false ; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath. postproc = false ; Use anti-aliasing techniques. antialiasing = "disabled" ; Use sharpening techniques. sharpening = "disabled" sharpness = 0.3 ; Quality used for actors. max_actor_quality=200 ; Whether or not actor variants are selected randomly, possible values are "full", "limited", "none". variant_diversity = "full" ; Quality level of shader effects (set to 10 to display all effects) materialmgr.quality = 2.0 ; Maximum distance to display parallax effect. Set to 0 to disable parallax. materialmgr.PARALLAX_DIST.max = 150 ; Maximum distance to display high quality parallax effect. materialmgr.PARALLAX_HQ_DIST.max = 75 ; Maximum distance to display very high quality parallax effect. Set to 30 to enable. materialmgr.PARALLAX_VHQ_DIST.max = 0 ;;;;;;;;;;;;;;;;;;;;;;;; ; Replace alpha-blending with alpha-testing, for performance experiments forcealphatest = false ; Color of the sky (in "r g b" format) skycolor = "0 0 0" [adaptivefps] session = 60 ; Throttle FPS in running games (prevents 100% CPU workload). menu = 60 ; Throttle FPS in menus only. [profiler2] server = "127.0.0.1" server.port = "8000" ; Use a free port on your machine. server.threads = "6" ; Enough for the browser's parallel connection limit [hotkey] ; Each one of the specified keys will trigger the action on the left ; for multiple-key combinations, separate keys with '+'. ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS exit = "" ; 'Custom' exit to desktop, SDL handles the native command via SDL_Quit. cancel = Escape ; Close or cancel the current dialog box/popup confirm = Return ; Confirm the current command pause = Pause, "Shift+Space" ; Pause/unpause game screenshot = F2 ; Take PNG screenshot bigscreenshot = "Shift+F2" ; Take large BMP screenshot togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots wireframe = "Alt+Shift+W" ; Toggle wireframe mode silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes ; > DIALOG HOTKEYS summary = "Ctrl+Tab" ; Toggle in-game summary lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window. structree = "Alt+Shift+T" ; Show structure tree civinfo = "Alt+Shift+H" ; Show civilization info ; > CLIPBOARD CONTROLS copy = "Ctrl+C" ; Copy to clipboard paste = "Ctrl+V" ; Paste from clipboard cut = "Ctrl+X" ; Cut selected text and copy to the clipboard ; > CONSOLE SETTINGS console.toggle = BackQuote, F9 ; Open/close console ; > OVERLAY KEYS fps.toggle = "Alt+F" ; Toggle frame counter realtime.toggle = "Alt+T" ; Toggle current display of computer time timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter ceasefirecounter.toggle = "" ; Toggle ceasefire counter ; > HOTKEYS ONLY chat = Return ; Toggle chat window teamchat = "T" ; Toggle chat window in team chat mode privatechat = "L" ; Toggle chat window and select the previous private chat partner ; > QUICKSAVE quicksave = "Shift+F5" quickload = "Shift+F8" [hotkey.camera] reset = "R" ; Reset camera rotation to default. follow = "F" ; Follow the first unit in the selection rallypointfocus = "" ; Focus the camera on the rally point of the selected building lastattackfocus = "Space" ; Focus the camera on the last notified attack zoom.in = Plus, NumPlus ; Zoom camera in (continuous control) zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control) rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control) pan = MouseMiddle ; Enable scrolling by moving mouse left = A, LeftArrow ; Scroll or rotate left right = D, RightArrow ; Scroll or rotate right up = W, UpArrow ; Scroll or rotate up/forwards down = S, DownArrow ; Scroll or rotate down/backwards scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed [hotkey.camera.jump] 1 = F5 ; Jump to position N 2 = F6 3 = F7 4 = F8 ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.camera.jump.set] 1 = "Ctrl+F5" ; Set jump position N 2 = "Ctrl+F6" 3 = "Ctrl+F7" 4 = "Ctrl+F8" ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.profile] toggle = "F11" ; Enable/disable real-time profiler save = "Shift+F11" ; Save current profiler data to logs/profile.txt [hotkey.profile2] toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler [hotkey.selection] cancel = Esc ; Un-select all units and cancel building placement add = Shift ; Add units to selection militaryonly = Alt ; Add only military units to the selection nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection idleonly = "I" ; Select only idle units woundedonly = "O" ; Select only wounded units remove = Ctrl ; Remove units from selection idlebuilder = Semicolon ; Select next idle builder idleworker = Period, NumDecimal ; Select next idle worker idlewarrior = Slash, NumDivide ; Select next idle warrior idleunit = BackSlash ; Select next idle unit offscreen = Alt ; Include offscreen units in selection singleselection = "" ; Modifier to select units individually, opposed to per formation. [hotkey.selection.group.add] 0 = "Shift+0", "Shift+Num0" 1 = "Shift+1", "Shift+Num1" 2 = "Shift+2", "Shift+Num2" 3 = "Shift+3", "Shift+Num3" 4 = "Shift+4", "Shift+Num4" 5 = "Shift+5", "Shift+Num5" 6 = "Shift+6", "Shift+Num6" 7 = "Shift+7", "Shift+Num7" 8 = "Shift+8", "Shift+Num8" 9 = "Shift+9", "Shift+Num9" [hotkey.selection.group.save] 0 = "Ctrl+0", "Ctrl+Num0" 1 = "Ctrl+1", "Ctrl+Num1" 2 = "Ctrl+2", "Ctrl+Num2" 3 = "Ctrl+3", "Ctrl+Num3" 4 = "Ctrl+4", "Ctrl+Num4" 5 = "Ctrl+5", "Ctrl+Num5" 6 = "Ctrl+6", "Ctrl+Num6" 7 = "Ctrl+7", "Ctrl+Num7" 8 = "Ctrl+8", "Ctrl+Num8" 9 = "Ctrl+9", "Ctrl+Num9" [hotkey.selection.group.select] 0 = 0, Num0 1 = 1, Num1 2 = 2, Num2 3 = 3, Num3 4 = 4, Num4 5 = 5, Num5 6 = 6, Num6 7 = 7, Num7 8 = 8, Num8 9 = 9, Num9 [hotkey.gamesetup] mapbrowser.open = "M" [hotkey.session] kill = Delete, Backspace ; Destroy selected units stop = "H" ; Stop the current action backtowork = "Y" ; The unit will go back to work unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected unloadturrets = "U" ; Unload turreted units. leaveturret = "U" ; Leave turret point. move = "" ; Modifier to move to a point instead of another action (e.g. gather) attack = Ctrl ; Modifier to attack instead of another action (e.g. capture) attackmove = Ctrl ; Modifier to attackmove when clicking on a point attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point garrison = Ctrl ; Modifier to garrison when clicking on building occupyturret = Ctrl ; Modifier to occupy a turret when clicking on a turret holder. autorallypoint = Ctrl ; Modifier to set the rally point on the building itself guard = "G" ; Modifier to escort/guard when clicking on unit/building patrol = "P" ; Modifier to patrol a unit repair = "J" ; Modifier to repair when clicking on building/mechanical unit queue = Shift ; Modifier to queue unit orders instead of replacing pushorderfront = "" ; Modifier to push unit orders to the front instead of replacing. orderone = Alt ; Modifier to order only one entity in selection. batchtrain = Shift ; Modifier to train units in batches massbarter = Shift ; Modifier to barter bunch of resources masstribute = Shift ; Modifier to tribute bunch of resources noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit fulltradeswap = Shift ; Modifier to put the desired trade resource to 100% unloadtype = Shift ; Modifier to unload all units of type deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting rotate.cw = RightBracket ; Rotate building placement preview clockwise rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure toggledefaultformation = "" ; Switch between null default formation and the last default formation used (defaults to "box") flare = K ; Modifier to send a flare to your allies flareactivate = "" ; Modifier to activate the mode to send a flare to your allies calltoarms = "" ; Modifier to call the selected units to the arms. ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel highlightguarding = PageDown ; Toggle highlight of guarding units highlightguarded = PageUp ; Toggle highlight of guarded units diplomacycolors = "Alt+X" ; Toggle diplomacy colors toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units [hotkey.session.gui] toggle = "Alt+G" ; Toggle visibility of session GUI menu.toggle = "F10" ; Toggle in-game menu diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel [hotkey.session.savedgames] delete = Delete, Backspace ; Delete the selected saved game asking confirmation noconfirmation = Shift ; Do not ask confirmation when deleting a game [hotkey.session.queueunit] ; > UNIT TRAINING 1 = "Z" ; add first unit type to queue 2 = "X" ; add second unit type to queue 3 = "C" ; add third unit type to queue 4 = "V" ; add fourth unit type to queue 5 = "B" ; add fivth unit type to queue 6 = "N" ; add sixth unit type to queue 7 = "M" ; add seventh unit type to queue 8 = Comma ; add eighth unit type to queue [hotkey.session.timewarp] fastforward = "Ctrl+Space" ; If timewarp mode enabled, speed up the game rewind = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game [hotkey.tab] next = "Tab", "Alt+S" ; Show the next tab prev = "Shift+Tab", "Alt+W" ; Show the previous tab [hotkey.text] ; > GUI TEXTBOX HOTKEYS delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor delete.right = "Ctrl+Del" ; Delete word to the right of cursor move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor [gui] cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking) scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays [gui.gamesetup] enabletips = true ; Enable/Disable tips during gamesetup (for newcomers) assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled. aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest) aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive) settingsslide = true ; Enable/Disable settings panel slide [gui.loadingscreen] progressdescription = false ; Whether to display the progress percent or a textual description [gui.session] camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one? timeelapsedcounter = false ; Show the game duration in the top right corner ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey) scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1 flarelifetime = 6 ; How long the flare markers on the minimap are displayed in seconds woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number attackrange = true ; Display attack range overlays of selected defensive structures aurasrange = true ; Display aura range overlays of selected units and structures healrange = true ; Display heal range overlays of selected units rankabovestatusbar = true ; Show rank icons above status bars experiencestatusbar = true ; Show an experience status bar above each selected unit respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending) snaptoedges = "disabled" ; Possible values: disabled, enabled. snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time. defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already. formationwalkonly = "true" ; Formations are disabled when giving gather/attack/... orders. howtoshownames = 0 ; Whether the specific names are show as default, as opposed to the generic names. And whether the secondary names are shown. (0 - show both; specific names primary, 1 - show both; generic names primary, 2 - show only specific names, 3 - show only generic names) +selectformationasone = "true" ; Whether to select formations as a whole by default. [gui.session.minimap] blinkduration = 1.7 ; The blink duration while pinging pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification [gui.session.notifications] attack = true ; Show a chat notification if you are attacked by another player tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode barter = true ; Show a chat notification to observers when a player bartered resources phase = completed ; 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. Possible values: none, completed, all. [gui.splashscreen] enable = true ; Enable/disable the splashscreen version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch [gui.session.diplomacycolors] self = "21 55 149" ; Color of your units when diplomacy colors are enabled ally = "86 180 31" ; Color of allies when diplomacy colors are enabled neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled [joystick] ; EXPERIMENTAL: joystick/gamepad settings enable = false deadzone = 8192 [chat] timestamp = true ; Show at which time chat messages have been sent [chat.session] extended = true ; Whether to display the chat history [lobby] history = 0 ; Number of past messages to display on join room = "arena26" ; Default MUC room to join server = "lobby.wildfiregames.com" ; Address of lobby server tls = true ; Whether to use TLS encryption when connecting to the server. verify_certificate = false ; Whether to reject connecting to the lobby if the TLS certificate is invalid (TODO: wait for Gloox GnuTLS trust implementation to be fixed) terms_url = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/prelobby/common/terms/"; Allows the user to save the text and print the terms terms_of_service = "0" ; Version (hash) of the Terms of Service that the user has accepted terms_of_use = "0" ; Version (hash) of the Terms of Use that the user has accepted privacy_policy = "0" ; Version (hash) of the Privacy Policy that the user has accepted xpartamupp = "wfgbot26" ; Name of the server-side XMPP-account that manage games echelon = "echelon26" ; Name of the server-side XMPP-account that manages ratings buddies = "," ; Comma separated list of playernames that the current user has marked as buddies rememberpassword = true ; Whether to store the encrypted password in the user config [lobby.columns] gamerating = false ; Show the average rating of the participating players in a column of the gamelist [lobby.stun] enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router. ; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding. server = "lobby.wildfiregames.com" ; Address of the STUN server. port = 3478 ; Port of the STUN server. delay = 200 ; Duration in milliseconds that is waited between STUN messages. ; Smaller numbers speed up joins but also become less stable. [mod] enabledmods = "mod public" [modio] public_key = "RWR/X/zDEJSHEfioDC/EO2So3TRMUmAH4O6A1a3ZhMwcqQA61xqBPPGa" ; Public key corresponding to the private key valid mods are signed with disclaimer = "0" ; Version (hash) of the Disclaimer that the user has accepted [modio.v1] baseurl = "https://api.mod.io/v1" api_key = "23df258a71711ea6e4b50893acc1ba55" name_id = "0ad" [network] duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join. lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled. observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached observermaxlag = 10 ; Make clients wait for observers if they lag more than X turns behind. -1 means "never wait for observers". autocatchup = true ; Auto-accelerate the sim rate if lagging behind (as an observer). [overlay] fps = "false" ; Show frames per second in top right corner realtime = "false" ; Show current system time in top right corner netwarnings = "true" ; Show warnings if the network connection is bad [profiler2] autoenable = false ; Enable HTTP server output at startup (default off for security/performance) gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available [rlinterface] address = "127.0.0.1:6000" [sound] mastergain = 0.9 musicgain = 0.2 ambientgain = 0.6 actiongain = 0.7 uigain = 0.7 mindistance = 1 maxdistance = 350 maxstereoangle = 0.62 ; About PI/5 radians [sound.notify] nick = true ; Play a sound when someone mentions your name in the lobby or game gamesetup.join = false ; Play a sound when a new client joins the game setup [tinygettext] debug = false ; Print error messages each time a translation for an English string is not found. [userreport] ; Opt-in online user reporting system url_upload = "https://feedback.wildfiregames.com/report/upload/v1/" ; URL where UserReports are uploaded to url_publication = "https://feedback.wildfiregames.com/" ; URL where UserReports were analyzed and published url_terms = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/userreport/Terms_and_Conditions.txt"; Allows the user to save the text and print the terms terms = "0" ; Version (hash) of the UserReporter Terms that the user has accepted [view] ; Camera control settings scroll.speed = 120.0 scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed rotate.x.speed = 1.2 rotate.x.min = 28.0 rotate.x.max = 60.0 rotate.x.default = 35.0 rotate.y.speed = 2.0 rotate.y.speed.wheel = 0.45 rotate.y.default = 0.0 rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed drag.speed = 0.5 zoom.speed = 256.0 zoom.speed.wheel = 32.0 zoom.min = 50.0 zoom.max = 200.0 zoom.default = 120.0 zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed pos.smoothness = 0.1 zoom.smoothness = 0.4 rotate.x.smoothness = 0.5 rotate.y.smoothness = 0.3 near = 2.0 ; Near plane distance far = 4096.0 ; Far plane distance fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide height.smoothness = 0.5 height.min = 16 Index: ps/trunk/binaries/data/mods/public/gui/options/options.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 26075) +++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 26076) @@ -1,743 +1,749 @@ [ { "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", "function": "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": "FPS overlay", "tooltip": "Show frames per second in top right corner.", "config": "overlay.fps" }, { "type": "boolean", "label": "Real time overlay", "tooltip": "Show current system time in top right corner.", "config": "overlay.realtime" }, { "type": "boolean", "label": "Game time 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": "boolean", "label": "Chat timestamp", "tooltip": "Display the time at which a chat message was posted.", "config": "chat.timestamp" }, { "type": "dropdown", "label": "Naming of entities.", "tooltip": "How to show entity names.", "config": "gui.session.howtoshownames", "list": [ { "value": 0, "label": "Specific names first", "tooltip": "Display specific names before generic names." }, { "value": 1, "label": "Generic names first", "tooltip": "Display generic names before specific names." }, { "value": 2, "label": "Only specific names", "tooltip": "Display only specific names for entities." }, { "value": 3, "label": "Only generic names", "tooltip": "Display only generic names for entities." } ] } ] }, { "label": "Graphics (general)", "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": "Fog", "tooltip": "Enable fog.", "config": "fog" }, { "type": "boolean", "label": "Post-processing", "tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).", "config": "postproc" }, { "type": "boolean", "label": "Shadows", "tooltip": "Enable shadows.", "config": "shadows" }, { "type": "boolean", "label": "Unit silhouettes", "tooltip": "Show outlines of units behind structures.", "config": "silhouettes" }, { "type": "boolean", "label": "Particles", "tooltip": "Enable particles.", "config": "particles" }, { "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 }, { "type": "dropdownNumber", "label": "GUI scale", "timeout": 500, "tooltip": "GUI scale", "config": "gui.scale", "function": "SetGUIScale", "list": [ { "value": 0.75, "label": "75%" }, { "value": 1.00, "label": "100%" }, { "value": 1.25, "label": "125%" }, { "value": 1.50, "label": "150%" }, { "value": 1.75, "label": "175%" }, { "value": 2.00, "label": "200%" }, { "value": 2.25, "label": "225%" }, { "value": 2.50, "label": "250%" } ] } ] }, { "label": "Graphics (advanced)", "tooltip": "More specific rendering settings.", "options": [ { "type": "dropdown", "label": "Renderer backend", "tooltip": "Choose the renderer's backend. REQUIRES GAME RESTART", "config": "rendererbackend", "list": [ { "value": "gl", "label": "OpenGL", "tooltip": "Default OpenGL backend with GLSL. REQUIRES GAME RESTART" }, { "value": "glarb", "label": "OpenGL ARB", "tooltip": "Legacy OpenGL backend with ARB shaders. REQUIRES GAME RESTART" } ] }, { "type": "boolean", "label": "Fog", "tooltip": "Enable fog.", "dependencies": [{ "config": "rendererbackend", "op": "!=", "value": "glarb" }], "config": "fog" }, { "type": "boolean", "label": "Post-processing", "tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).", "config": "postproc" }, { "type": "dropdown", "label": "Antialiasing", "tooltip": "Reduce aliasing effect on edges.", "dependencies": ["postproc", { "config": "rendererbackend", "op": "!=", "value": "glarb" }], "config": "antialiasing", "list": [ { "value": "disabled", "label": "Disabled", "tooltip": "Do not use antialiasing." }, { "value": "fxaa", "label": "FXAA", "tooltip": "Fast, but simple antialiasing." }, { "value": "msaa2", "label": "MSAA (2×)", "tooltip": "Slow, but high-quality antialiasing, uses two samples per pixel. Supported for GL3.3+." }, { "value": "msaa4", "label": "MSAA (4×)", "tooltip": "Slow, but high-quality antialiasing, uses four samples per pixel. Supported for GL3.3+." }, { "value": "msaa8", "label": "MSAA (8×)", "tooltip": "Slow, but high-quality antialiasing, uses eight samples per pixel. Supported for GL3.3+." }, { "value": "msaa16", "label": "MSAA (16×)", "tooltip": "Slow, but high-quality antialiasing, uses sixteen samples per pixel. Supported for GL3.3+." } ] }, { "type": "dropdown", "label": "Sharpening", "tooltip": "Reduce blurry effects.", "dependencies": ["postproc", { "config": "rendererbackend", "op": "!=", "value": "glarb" }], "config": "sharpening", "list": [ { "value": "disabled", "label": "Disabled", "tooltip": "Do not use sharpening." }, { "value": "cas", "label": "FidelityFX CAS", "tooltip": "Contrast adaptive sharpening, a fast, contrast based sharpening pass." } ] }, { "type": "slider", "label": "Sharpness factor", "tooltip": "The sharpness of the choosen pass.", "dependencies": [ "postproc", { "config": "rendererbackend", "op": "!=", "value": "glarb" }, { "config": "sharpening", "op": "!=", "value": "disabled" } ], "config": "sharpness", "min": 0, "max": 1 }, { "type": "dropdown", "label": "Model quality", "tooltip": "Model quality setting.", "config": "max_actor_quality", "list": [ { "value": 100, "label": "Low", "tooltip": "Simpler models for better performance." }, { "value": 150, "label": "Medium", "tooltip": "Average quality and average performance." }, { "value": 200, "label": "High", "tooltip": "High quality models." } ] }, { "type": "dropdown", "label": "Model appearance randomization", "tooltip": "Randomize the appearance of entities. Disabling gives a small performance improvement.", "config": "variant_diversity", "list": [ { "value": "none", "label": "None", "tooltip": "Entities will all look the same." }, { "value": "limited", "label": "Limited", "tooltip": "Entities will be less diverse." }, { "value": "full", "label": "Normal", "tooltip": "Entities appearance is randomized normally." } ] }, { "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" }, { "type": "dropdown", "label": "Quality", "tooltip": "Shadow map resolution. High values can crash the game when using a graphics card with low memory!", "dependencies": ["shadows"], "config": "shadowquality", "list": [ { "value": -1, "label": "Low" }, { "value": 0, "label": "Medium" }, { "value": 1, "label": "High" }, { "value": 2, "label": "Very High" } ] }, { "type": "boolean", "label": "Filtering", "tooltip": "Smooth shadows.", "dependencies": ["shadows"], "config": "shadowpcf" }, { "type": "slider", "label": "Cutoff distance", "tooltip": "Hides shadows beyond a certain distance from a camera.", "dependencies": ["shadows"], "config": "shadowscutoffdistance", "min": 100, "max": 1500 }, { "type": "boolean", "label": "Cover whole map", "tooltip": "When ON shadows cover the whole map and shadows cutoff distance is ignored. Useful for making screenshots of a whole map.", "dependencies": ["shadows"], "config": "shadowscovermap" }, { "type": "boolean", "label": "Water effects", "tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.", "config": "watereffects" }, { "type": "boolean", "label": "High-quality water effects", "tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.", "dependencies": ["watereffects"], "config": "waterfancyeffects" }, { "type": "boolean", "label": "Water reflections", "tooltip": "Allow water to reflect a mirror image.", "dependencies": ["watereffects"], "config": "waterreflection" }, { "type": "boolean", "label": "Water refraction", "tooltip": "Use a real water refraction map and not transparency.", "dependencies": ["watereffects"], "config": "waterrefraction" }, { "type": "boolean", "label": "Real water depth", "tooltip": "Use actual water depth in rendering calculations.", "dependencies": ["watereffects", "waterrefraction"], "config": "waterrealdepth" } ] }, { "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": "New player notification in game setup", "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", "tooltip": "Players joining the match will be assigned if there is a free slot." }, { "value": "buddies", "label": "Buddies", "tooltip": "Players joining the match will only be assigned if they are a buddy of the host and if there is a free slot." }, { "value": "disabled", "label": "Disabled", "tooltip": "Players only receive a slot when the host assigns them explicitly." } ] } ] }, { "label": "Networking / 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" }, { "type": "boolean", "label": "Network warnings", "tooltip": "Show which player has a bad connection in multiplayer games.", "config": "overlay.netwarnings" }, { "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": "number", "label": "Max lag for observers", "tooltip": "When hosting, pause the game if observers are lagging more than this many turns. If set to -1, observers are ignored.", "config": "network.observermaxlag", "min": -1, "max": 10000 }, { "type": "boolean", "label": "(Observer) Speed up when lagging.", "tooltip": "When observing a game, automatically speed up if you start lagging, to catch up with the live match.", "config": "network.autocatchup" } ] }, { "label": "Game Session", "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": "slider", "label": "Flare display duration", "tooltip": "How long the flare markers on the minimap are displayed in seconds.", "config": "gui.session.flarelifetime", "min": 0, "max": 60 }, { "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. (It can also be toggled with the hotkey during a game).", "config": "gui.session.attackrange" }, { "type": "boolean", "label": "Aura range visualization", "tooltip": "Display the range of auras of selected units and structures. (It can also be toggled with the hotkey during a game).", "config": "gui.session.aurasrange" }, { "type": "boolean", "label": "Heal range visualization", "tooltip": "Display the healing range of selected units. (It can also be toggled with the hotkey during a game).", "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 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" }, { "type": "dropdown", "label": "Snap to edges", "tooltip": "This option allows to align new structures with nearby structures.", "config": "gui.session.snaptoedges", "list": [ { "value": "disabled", "label": "Hotkey to enable snapping", "tooltip": "New structures are aligned with nearby structures while pressing the hotkey." }, { "value": "enabled", "label": "Hotkey to disable snapping", "tooltip": "New structures are aligned with nearby structures unless the hotkey is pressed." } ] }, { "type": "dropdown", "label": "Control group membership", "tooltip": "Decide whether units can be part of multiple control groups.", "config": "gui.session.disjointcontrolgroups", "list": [ { "value": "true", "label": "Single", "tooltip": "When adding a Unit or Structure to a control group, they are removed from other control groups. Use this choice if you want control groups to refer to distinct armies." }, { "value": "false", "label": "Multiple", "tooltip": "Units and Structures can be part of multiple control groups. This is useful to keep control groups for distinct armies and a control group for the entire army simultaneously." } ] }, { "type": "dropdown", "label": "Formation control", "tooltip": "Decide whether formations are enabled for all orders or only 'Walk' and 'Patrol'.", "config": "gui.session.formationwalkonly", "list": [ { "value": "true", "label": "Walk/Patrol Only", "tooltip": "Other orders will disband existing formations." }, { "value": "false", "label": "No override", "tooltip": "Units in formations stay in formations." } ] + }, + { + "type": "boolean", + "label": "Battalion-style formations", + "tooltip": "Whether formations are selected as a whole.", + "config": "gui.session.selectformationasone" } ] } ] Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 26075) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 26076) @@ -1,1716 +1,1716 @@ 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; const ACTION_OCCUPY_TURRET = 5; const ACTION_CALLTOARMS = 6; 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; const INPUT_FLARE = 12; 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; /** * Store the last time the flare functionality was used to prevent overusage. */ var g_LastFlareTime; /** * The duration in ms for which we disable flaring after each flare to prevent overusage. */ const g_FlareCooldown = 3000; // Same double-click behaviour for hotkey presses. const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { let cursorSet = false; let tooltipSet = false; let informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); if (inputState == INPUT_FLARE || inputState == INPUT_NORMAL && Engine.HotkeyIsPressed("session.flare") && !g_IsObserver) { Engine.SetCursor("action-flare"); cursorSet = true; } else if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) || g_MiniMapPanel.isMouseOverMiniMap()) { let action = determineAction(mouseX, mouseY, g_MiniMapPanel.isMouseOverMiniMap()); 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; let 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) { let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); placementSupport.tooltipError = !result.success; placementSupport.tooltipMessage = ""; if (!result.success) { if (result.message && result.parameters) { let message = result.message; if (result.translateMessage) if (result.pluralMessage) message = translatePlural(result.message, result.pluralMessage, result.pluralCount); else message = translate(message); let parameters = result.parameters; if (result.translateParameters) translateObjectKeys(parameters, result.translateParameters); placementSupport.tooltipMessage = sprintf(message, parameters); } return false; } if (placementSupport.attack && placementSupport.attack.Ranged) { const cmd = { "x": placementSupport.position.x, "z": placementSupport.position.z, "range": placementSupport.attack.Ranged.maxRange, "yOrigin": placementSupport.attack.Ranged.yOrigin }; const averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); const 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" && placementSupport.wallSet && placementSupport.position) { 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) { let selection = g_Selection.toList(); if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } let entState = GetEntityState(selection[0]); if (!entState) return undefined; if (!selection.every(ownsEntity) && !(g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll)) return undefined; let target; if (!fromMiniMap) { let 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 // thus the most specific should appear first. if (preSelectedAction != ACTION_NONE) { for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].preSelectedActionCheck) { let r = g_UnitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].hotkeyActionCheck) { let r = g_UnitActions[action].hotkeyActionCheck(target, selection); if (r) return r; } for (let action of g_UnitActionsSortedKeys) if (g_UnitActions[action].actionCheck) { let 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 isAttackMovePressed() { return Engine.HotkeyIsPressed("session.attackmove") || Engine.HotkeyIsPressed("session.attackmoveUnit"); } function isSnapToEdgesEnabled() { let config = Engine.ConfigDB_GetValue("user", "gui.session.snaptoedges"); let hotkeyPressed = Engine.HotkeyIsPressed("session.snaptoedges"); return hotkeyPressed == (config == "disabled"); } function tryPlaceBuilding(queued, pushFront) { if (placementSupport.mode !== "building") { error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'"); return false; } if (!updateBuildingPlacementPreview()) { Engine.GuiInterfaceCall("PlaySound", { "name": "invalid_building_placement", "entity": g_Selection.getFirstSelected() }); return false; } let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection({ "type": "construct", "target": placementSupport }) || 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, "pushFront": pushFront, "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); if (!queued || !g_Selection.size()) placementSupport.Reset(); else placementSupport.RandomizeActorSeed(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'"); return false; } let 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; let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection({ "type": "construct", "target": placementSupport }) || g_Selection.toList(); let cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, "formation": g_AutoFormation.getNull() }; // 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). let 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 => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit"); }, "isDefensive": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Defensive"); }, "isMilitary": entity => { let entState = GetEntityState(entity); return entState && g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isNonMilitary": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && !g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isIdle": entity => { let 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) { let filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; 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]; let preferredEnts = []; for (let 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 cursor 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; } 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: let bandbox = Engine.GetGUIObjectByName("bandbox"); switch (ev.type) { case "mousemotion": { let rect = updateBandbox(bandbox, ev, false); let ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); let preferredEntities = getPreferredEntities(ents); g_Selection.setHighlightList(preferredEntities); return false; } case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { let rect = updateBandbox(bandbox, ev, true); let ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); g_Selection.setHighlightList([]); 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; } 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 queued, let the player continue placing another of the same building. let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront"))) { if (queued && g_Selection.size()) 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 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; let result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { let 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) { let 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; } if (ev.button == SDL_BUTTON_RIGHT) { 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 cursor. placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); else // If the cursor is near the center, snap back to the default orientation. placementSupport.SetDefaultAngle(); let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": isSnapToEdgesEnabled() && 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 queued, let the player continue placing another of the same structure. let queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued, Engine.HotkeyIsPressed("session.pushorderfront"))) { if (queued && g_Selection.size()) 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 == "hotkeypress"); updateAdditionalHighlight(); } else if (ev.hotkey == "session.highlightguarded") { g_ShowGuarded = (ev.type == "hotkeypress"); updateAdditionalHighlight(); } if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) clickedEntity = INVALID_ENTITY; // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": let ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (Engine.HotkeyIsPressed("session.flare") && controlsPlayer(g_ViewedPlayer)) { triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); return true; } 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 "hotkeypress": 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) { let sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { let sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now; prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousemotion": let 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) { let action = determineAction(ev.x, ev.y); if (!action) break; if (!Engine.HotkeyIsPressed("session.queue") && !Engine.HotkeyIsPressed("session.orderone")) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; } return doAction(action, ev); } if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } default: // Slight hack: If selection is empty, reset the input state. if (!g_Selection.size()) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta) { inputState = INPUT_BANDBOXING; return false; } let 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 (Engine.GetFollowedEntity() != clickedEntity) Engine.CameraFollow(0); let ents = []; if (ev.clicks == 1) ents = [clickedEntity]; else { let showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); let matchRank = true; let templateToMatch; if (ev.clicks == 2) { 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); } 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 (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 { if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost })) { placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } if (isSnapToEdgesEnabled()) { // We need to reset the angle before the snapping to edges, // because we want to get the angle near to the default one. placementSupport.SetDefaultAngle(); } let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "snapToEdges": isSnapToEdgesEnabled() && 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") { let validPlacement = updateBuildingPlacementPreview(); if (validPlacement !== false) inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (isSnapToEdgesEnabled()) { let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "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": let 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; case INPUT_FLARE: if (ev.type == "mousebuttondown") { if (ev.button == SDL_BUTTON_LEFT && controlsPlayer(g_ViewedPlayer)) { triggerFlareAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { inputState = INPUT_NORMAL; return true; } } } return false; } function doAction(action, ev) { if (!controlsPlayer(g_ViewedPlayer)) return false; return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action); } function popOneFromSelection(action) { // Pick the first unit that can do this order. let unit = action.firstAbleEntity || g_Selection.find(entity => ["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method => g_UnitActions[action.type][method] && g_UnitActions[action.type][method](action.target || undefined, [entity]) )); if (unit) { - g_Selection.removeList([unit], true); + g_Selection.removeList([unit], false); return [unit]; } return null; } 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]); const selection = g_Selection.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": isAttackMovePressed() ? "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"), "pushFront": Engine.HotkeyIsPressed("session.pushorderfront"), "formation": NULL_FORMATION, }); // 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 triggerFlareAction(target) { let now = Date.now(); if (g_LastFlareTime && now < g_LastFlareTime + g_FlareCooldown) return; g_LastFlareTime = now; displayFlare(target, Engine.GetPlayerID()); Engine.PlayUISound(g_FlareSound, false); Engine.PostNetworkCommand({ "type": "map-flare", "target": target }); } function handleUnitAction(target, action) { if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute) { error("Invalid action.type " + action.type); return false; } let selection = Engine.HotkeyIsPressed("session.orderone") && popOneFromSelection(action) || g_Selection.toList(); // If the session.queue hotkey is down, add the order to the unit's order queue instead // of running it immediately. If the pushorderfront hotkey is down, execute the order // immidiately and continue the rest of the queue afterwards. return g_UnitActions[action.type].execute( target, action, selection, Engine.HotkeyIsPressed("session.queue"), Engine.HotkeyIsPressed("session.pushorderfront")); } function getEntityLimitAndCount(playerState, entType) { let ret = { "entLimit": undefined, "entCount": undefined, "entLimitChangers": undefined, "canBeAddedCount": undefined, "matchLimit": undefined, "matchCount": undefined, "type": undefined }; if (!playerState.entityLimits) return ret; let template = GetTemplateData(entType); let entCategory; let matchLimit; if (template.trainingRestrictions) { entCategory = template.trainingRestrictions.category; matchLimit = template.trainingRestrictions.matchLimit; ret.type = "training"; } else if (template.buildRestrictions) { entCategory = template.buildRestrictions.category; matchLimit = template.buildRestrictions.matchLimit; ret.type = "build"; } 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); } if (matchLimit) { ret.matchLimit = matchLimit; ret.matchCount = playerState.matchEntityCounts[entType] || 0; ret.canBeAddedCount = Math.min(Math.max(ret.entLimit - ret.entCount, 0), Math.max(ret.matchLimit - ret.matchCount, 0)); } return ret; } /** * Called by GUI when user clicks construction button. * @param {string} 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 cursor 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 structure. // Gives the impression that somehow the hovered-over entity has something to do with the structure you're building. placementSupport.Reset(); let 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) 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 => { const state = GetEntityState(entity); return state?.trainer?.entities?.indexOf(trainEntType) != -1 && (!state.upgrade || !state.upgrade.isUpgrading); }); } 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. * @param {number} position - The position of the template to train. */ 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]; if (!entToTrain) return; addTrainingToQueue(selection, entToTrain, playerState); } // 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 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 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; } else if (!decrement) flushTrainingBatch(); } 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 { let buildingsForTraining = appropriateBuildings; if (canBeAddedCount !== undefined) buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); Engine.PostNetworkCommand({ "type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } } /** * 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 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 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 structure. if (g_BatchTrainingEntityAllowedCount !== undefined && g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) { // 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, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); // 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, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } else Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings, "template": g_BatchTrainingType, "count": batchedSize, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": let toSelect = []; g_Groups.update(); for (let 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(getEntityOrHolder(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) { let append = Engine.HotkeyIsPressed("selection.add"); let 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; let 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; } let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (!idleUnits.length) { // TODO: display a message to indicate no more idle units, or something Engine.GuiInterfaceCall("PlaySoundForPlayer", { "name": "no_idle_unit" }); resetIdleUnit(); return; } if (!append) g_Selection.reset(); g_Selection.addList(idleUnits); if (selectall) return; lastIdleUnit = idleUnits[0]; let entityState = GetEntityState(lastIdleUnit); if (entityState.position) Engine.CameraMoveTo(entityState.position.x, entityState.position.z); // Move the idle class index to the first class an idle unit was found for. let 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/selection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 26075) +++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 26076) @@ -1,578 +1,589 @@ // Limits selection size var g_MaxSelectionSize = 200; // Alpha value of hovered/mouseover/highlighted selection overlays // (should probably be greater than always visible alpha value, // see CCmpSelectable) var g_HighlightedAlpha = 0.75; function _setHighlight(ents, alpha, selected) { if (ents.length) Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities": ents, "alpha": alpha, "selected": selected }); } function _setStatusBars(ents, enabled) { if (!ents.length) return; Engine.GuiInterfaceCall("SetStatusBars", { "entities": ents, "enabled": enabled, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } function _setMotionOverlay(ents, enabled) { if (ents.length) Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities": ents, "enabled": enabled }); } function _playSound(ent) { Engine.GuiInterfaceCall("PlaySound", { "name": "select", "entity": ent }); } /** * EntityGroups class for managing grouped entities */ function EntityGroups() { this.groups = {}; this.ents = {}; } EntityGroups.prototype.reset = function() { this.groups = {}; this.ents = {}; }; EntityGroups.prototype.add = function(ents) { for (let ent of ents) { if (this.ents[ent]) continue; var entState = GetEntityState(ent); // When this function is called during group rebuild, deleted // entities will not yet have been removed, so entities might // still be present in the group despite not existing. if (!entState) continue; var templateName = entState.template; var key = GetTemplateData(templateName).selectionGroupName || templateName; // Group the ents by player and template if (entState.player !== undefined) key = "p" + entState.player + "&" + key; if (this.groups[key]) this.groups[key] += 1; else this.groups[key] = 1; this.ents[ent] = key; } }; EntityGroups.prototype.removeEnt = function(ent) { var key = this.ents[ent]; // Remove the entity delete this.ents[ent]; --this.groups[key]; // Remove the entire group if (this.groups[key] == 0) delete this.groups[key]; }; EntityGroups.prototype.rebuildGroup = function(renamed) { var oldGroup = this.ents; this.reset(); var toAdd = []; for (var ent in oldGroup) toAdd.push(renamed[ent] ? renamed[ent] : +ent); this.add(toAdd); }; EntityGroups.prototype.getCount = function(key) { return this.groups[key]; }; EntityGroups.prototype.getTotalCount = function() { let totalCount = 0; for (let key in this.groups) totalCount += this.groups[key]; return totalCount; }; EntityGroups.prototype.getKeys = function() { // Preserve order even when shuffling units around // Can be optimized by moving the sorting elsewhere return Object.keys(this.groups).sort(); }; EntityGroups.prototype.getEntsByKey = function(key) { var ents = []; for (var ent in this.ents) if (this.ents[ent] == key) ents.push(+ent); return ents; }; /** * get a list of entities grouped by a key */ EntityGroups.prototype.getEntsGrouped = function() { return this.getKeys().map(key => ({ "ents": this.getEntsByKey(key), "key": key })); }; /** * Gets all ents in every group except ones of the specified group */ EntityGroups.prototype.getEntsByKeyInverse = function(key) { var ents = []; for (var ent in this.ents) if (this.ents[ent] != key) ents.push(+ent); return ents; }; /** * EntitySelection class for managing the entity selection list and the primary selection */ function EntitySelection() { // Private properties: this.selected = new Set(); // For mouseover-highlighted entity IDs in these. this.highlighted = new Set(); this.motionDebugOverlay = false; // Public properties: this.dirty = false; // set whenever the selection has changed this.groups = new EntityGroups(); + + this.UpdateFormationSelectionBehaviour(); + registerConfigChangeHandler(changes => { + if (changes.has("gui.session.selectformationasone")) + this.UpdateFormationSelectionBehaviour(); + }); +} + +EntitySelection.prototype.UpdateFormationSelectionBehaviour = function() +{ + this.SelectFormationAsOne = Engine.ConfigDB_GetValue("user", "gui.session.selectformationasone") == "true"; } /** * Deselect everything but entities of the chosen type. */ EntitySelection.prototype.makePrimarySelection = function(key) { const ents = this.groups.getEntsByKey(key); this.reset(); this.addList(ents, false, false, false); }; /** * Deselect entities of the chosen type. */ EntitySelection.prototype.removeGroupFromSelection = function(key) { this.removeList(this.groups.getEntsByKey(key)); }; /** * Get a list of the template names */ EntitySelection.prototype.getTemplateNames = function() { const templateNames = []; for (const ent of this.selected) { const entState = GetEntityState(ent); if (entState) templateNames.push(entState.template); } return templateNames; }; /** * Update the selection to take care of changes (like units that have been killed). */ EntitySelection.prototype.update = function() { this.checkRenamedEntities(); const controlsAll = g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll; const removeOwnerChanges = !g_IsObserver && !controlsAll && this.selected.size > 1; let changed = false; for (const ent of this.selected) { const entState = GetEntityState(ent); if (!entState) { this.selected.delete(ent); this.groups.removeEnt(ent); changed = true; continue; } // Remove non-visible units (e.g. moved back into fog-of-war) // At the next update, mirages will be renamed to the real // entity they replace, so just ignore them now // Futhermore, when multiple selection, remove units which have changed ownership if (entState.visibility == "hidden" && !entState.mirage || removeOwnerChanges && entState.player != g_ViewedPlayer) { // Disable any highlighting of the disappeared unit _setHighlight([ent], 0, false); _setStatusBars([ent], false); _setMotionOverlay([ent], false); this.selected.delete(ent); this.groups.removeEnt(ent); changed = true; continue; } } if (changed) this.onChange(); }; /** * Update selection if some selected entities were renamed * (in case of unit promotion or finishing building structure) */ EntitySelection.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for (let renamedEntity of renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; // Reconstruct the selection if at least one entity has been renamed. for (let renamedEntity of renamedEntities) if (this.selected.has(renamedEntity.entity)) { this.rebuildSelection(renamedLookup); return; } } }; /** * Add entities to selection. Play selection sound unless quiet is true */ EntitySelection.prototype.addList = function(ents, quiet, force = false, addFormationMembers = true) { // If someone else's player is the sole selected unit, don't allow adding to the selection. const firstEntState = this.selected.size == 1 && GetEntityState(this.getFirstSelected()); if (firstEntState && firstEntState.player != g_ViewedPlayer && !force) return; const added = []; for (const ent of addFormationMembers ? this.addFormationMembers(ents) : ents) { if (this.selected.size >= g_MaxSelectionSize) break; if (this.selected.has(ent)) continue; const entState = GetEntityState(ent); if (!entState) continue; let isUnowned = g_ViewedPlayer != -1 && entState.player != g_ViewedPlayer || g_ViewedPlayer == -1 && entState.player == 0; // Don't add unowned entities to the list, unless a single entity was selected if (isUnowned && (ents.length > 1 || this.selected.size) && !force) continue; added.push(ent); this.selected.add(ent); } _setHighlight(added, 1, true); _setStatusBars(added, true); _setMotionOverlay(added, this.motionDebugOverlay); if (added.length) { // Play the sound if the entity is controllable by us or Gaia-owned. var owner = GetEntityState(added[0]).player; if (!quiet && (controlsPlayer(owner) || g_IsObserver || owner == 0)) _playSound(added[0]); } this.groups.add(this.toList()); // Create Selection Groups this.onChange(); }; /** * @param {number[]} ents - The entities to remove. - * @param {boolean} dontAddFormationMembers - If true we need to exclude adding formation members. + * @param {boolean} addFormationMembers - If true we need to add formation members. */ -EntitySelection.prototype.removeList = function(ents, dontAddFormationMembers = false) +EntitySelection.prototype.removeList = function(ents, addFormationMembers = true) { const removed = []; - for (const ent of dontAddFormationMembers ? ents : this.addFormationMembers(ents)) + for (const ent of addFormationMembers ? this.addFormationMembers(ents) : ents) if (this.selected.has(ent)) { this.groups.removeEnt(ent); removed.push(ent); this.selected.delete(ent); } _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setMotionOverlay(removed, false); this.onChange(); }; EntitySelection.prototype.reset = function() { _setHighlight(this.toList(), 0, false); _setStatusBars(this.toList(), false); _setMotionOverlay(this.toList(), false); this.selected.clear(); this.groups.reset(); this.onChange(); }; EntitySelection.prototype.rebuildSelection = function(renamed) { const toAdd = []; for (const ent of this.selected) toAdd.push(renamed[ent] || ent); this.reset(); this.addList(toAdd, true); // don't play selection sounds }; EntitySelection.prototype.getFirstSelected = function() { for (const ent of this.selected) return ent; return undefined; }; /** * TODO: This array should not be recreated every call */ EntitySelection.prototype.toList = function() { return Array.from(this.selected); }; /** * @return {number} - The number of entities selected. */ EntitySelection.prototype.size = function() { return this.selected.size; }; EntitySelection.prototype.find = function(condition) { for (const ent of this.selected) if (condition(ent)) return ent; return null; }; /** * @param {function} condition - A function. * @return {number[]} - The entities passing the condition. */ EntitySelection.prototype.filter = function(condition) { const result = []; for (const ent of this.selected) if (condition(ent)) result.push(ent); return result; }; EntitySelection.prototype.setHighlightList = function(entities) { const highlighted = new Set(); const ents = this.addFormationMembers(entities); for (const ent of ents) highlighted.add(ent); const removed = []; const added = []; // Remove highlighting for the old units that are no longer highlighted // (excluding ones that are actively selected too). for (const ent of this.highlighted) if (!highlighted.has(ent) && !this.selected.has(ent)) removed.push(ent); // Add new highlighting for units that aren't already highlighted. for (const ent of ents) if (!this.highlighted.has(ent) && !this.selected.has(ent)) added.push(ent); _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setHighlight(added, g_HighlightedAlpha, true); _setStatusBars(added, true); this.highlighted = highlighted; }; EntitySelection.prototype.SetMotionDebugOverlay = function(enabled) { this.motionDebugOverlay = enabled; _setMotionOverlay(this.toList(), enabled); }; EntitySelection.prototype.onChange = function() { this.dirty = true; if (this.isSelection) onSelectionChange(); }; EntitySelection.prototype.selectAndMoveTo = function(entityID) { let entState = GetEntityState(entityID); if (!entState || !entState.position) return; this.reset(); this.addList([entityID]); Engine.CameraMoveTo(entState.position.x, entState.position.z); } /** * Adds the formation members of a selected entities to the selection. * @param {number[]} entities - The entity IDs of selected entities. * @return {number[]} - Some more entity IDs if part of a formation was selected. */ EntitySelection.prototype.addFormationMembers = function(entities) { - if (!entities.length || Engine.HotkeyIsPressed("selection.singleselection")) + if (!entities.length || !this.SelectFormationAsOne || Engine.HotkeyIsPressed("selection.singleselection")) return entities; const result = new Set(entities); for (const entity of entities) { const entState = GetEntityState(+entity); if (entState?.unitAI?.formation) for (const member of GetEntityState(+entState.unitAI.formation).formation.members) result.add(member); } return result; }; /** * Cache some quantities which depends only on selection */ var g_Selection = new EntitySelection(); g_Selection.isSelection = true; var g_canMoveIntoFormation = {}; var g_allBuildableEntities; var g_allTrainableEntities; // Reset cached quantities function onSelectionChange() { g_canMoveIntoFormation = {}; g_allBuildableEntities = undefined; g_allTrainableEntities = undefined; } /** * EntityGroupsContainer class for managing grouped entities */ function EntityGroupsContainer() { this.groups = []; for (var i = 0; i < 10; ++i) this.groups[i] = new EntityGroups(); } /** * Add entities to a group. * @param {string} groupName - The number of the group to add the entities to. * @param {number[]} ents - The entities to add to the group. */ EntityGroupsContainer.prototype.addEntities = function(groupName, ents) { if (Engine.ConfigDB_GetValue("user", "gui.session.disjointcontrolgroups") == "true") for (let ent of ents) for (let group of this.groups) if (ent in group.ents) group.removeEnt(ent); this.groups[groupName].add(ents); }; EntityGroupsContainer.prototype.update = function() { this.checkRenamedEntities(); for (let group of this.groups) for (var ent in group.ents) { var entState = GetEntityState(+ent); // Remove deleted units if (!entState) group.removeEnt(ent); } }; /** * Update control group if some entities in the group were renamed * (in case of unit promotion or finishing building structure) */ EntityGroupsContainer.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for (let renamedEntity of renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; for (let group of this.groups) for (let renamedEntity of renamedEntities) // Reconstruct the group if at least one entity has been renamed. if (renamedEntity.entity in group.ents) { group.rebuildGroup(renamedLookup); break; } } }; var g_Groups = new EntityGroupsContainer(); Index: ps/trunk/binaries/data/mods/public/gui/text/tips/formations.txt =================================================================== --- ps/trunk/binaries/data/mods/public/gui/text/tips/formations.txt (revision 26075) +++ ps/trunk/binaries/data/mods/public/gui/text/tips/formations.txt (revision 26076) @@ -1,3 +1,3 @@ FORMATIONS Arrange your soldiers in formations to keep them organized during battles. -Formations are selected as a whole by default. +Formations are selected as a whole by default, but that can be changed in the game settings.