Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 25938) +++ ps/trunk/binaries/data/config/default.cfg (revision 25939) @@ -1,578 +1,579 @@ ; 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 = 4 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" ; Specify the render path. This can be one of: ; default Automatically select one of the below, depending on system capabilities ; fixed Only use OpenGL fixed function pipeline ; shader Use vertex/fragment shaders for transform and lighting where possible ; Using 'fixed' instead of 'default' may work around some graphics-related problems, ; but will reduce performance and features when a modern graphics card is available. renderpath = default ;;;;; EXPERIMENTAL ;;;;; ; Prefer GLSL shaders over ARB shaders. Allows fancier graphical effects. preferglsl = false ; Experimental probably-non-working GPU skinning support; requires preferglsl; 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 = "" ; Select only one entity of a 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) [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/hotkeys/spec/selection.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/hotkeys/spec/selection.json (revision 25938) +++ ps/trunk/binaries/data/mods/public/gui/hotkeys/spec/selection.json (revision 25939) @@ -1,180 +1,184 @@ { "categories": { "selection": { "name": "Selecting Units", "desc": "Hotkeys relating to modifying unit selection." } }, "mapped_hotkeys": { "selection": { "selection.cancel": { "name": "Cancel selection", "desc": "Un-select all units and cancel building placement." }, "selection.add": { "name": "Add to selection", "desc": "Add units to selection." }, "selection.militaryonly": { "name": "Select military only", "desc": "Add only military units to the selection." }, "selection.nonmilitaryonly": { "name": "Select non-military only", "desc": "Add only non-military units to the selection." }, "selection.idleonly": { "name": "Select only idle units", "desc": "Select only idle units." }, "selection.woundedonly": { "name": "Select only wounded units", "desc": "Select only wounded units." }, "selection.remove": { "name": "Remove units from selection", "desc": "Remove units from selection." }, "selection.idleworker": { "name": "Select next idle worker", "desc": "Select next idle worker." }, "selection.idlewarrior": { "name": "Select next idle warrior", "desc": "Select next idle warrior." }, "selection.idleunit": { "name": "Select next idle unit", "desc": "Select next idle unit." }, "selection.idlebuilder": { "name": "Select next idle builder", "desc": "Select next idle builder." }, "selection.offscreen": { "name": "Include offscreen", "desc": "Include offscreen units in selection." }, + "selection.singleselection": { + "name": "Single selection", + "desc": "Select only one entity of a formation." + }, "selection.group.save.0": { "name": "Set Control Group 0", "desc": "Save current selection as Control Group 0." }, "selection.group.save.1": { "name": "Set Control Group 1", "desc": "Save current selection as Control Group 1." }, "selection.group.save.2": { "name": "Set Control Group 2", "desc": "Save current selection as Control Group 2." }, "selection.group.save.3": { "name": "Set Control Group 3", "desc": "Save current selection as Control Group 3." }, "selection.group.save.4": { "name": "Set Control Group 4", "desc": "Save current selection as Control Group 4." }, "selection.group.save.5": { "name": "Set Control Group 5", "desc": "Save current selection as Control Group 5." }, "selection.group.save.6": { "name": "Set Control Group 6", "desc": "Save current selection as Control Group 6." }, "selection.group.save.7": { "name": "Set Control Group 7", "desc": "Save current selection as Control Group 7." }, "selection.group.save.8": { "name": "Set Control Group 8", "desc": "Save current selection as Control Group 8." }, "selection.group.save.9": { "name": "Set Control Group 9", "desc": "Save current selection as Control Group 9." }, "selection.group.select.0": { "name": "Select Control Group 0", "desc": "Change the current selection to Control Group 0." }, "selection.group.select.1": { "name": "Select Control Group 1", "desc": "Change the current selection to Control Group 1." }, "selection.group.select.2": { "name": "Select Control Group 2", "desc": "Change the current selection to Control Group 2." }, "selection.group.select.3": { "name": "Select Control Group 3", "desc": "Change the current selection to Control Group 3." }, "selection.group.select.4": { "name": "Select Control Group 4", "desc": "Change the current selection to Control Group 4." }, "selection.group.select.5": { "name": "Select Control Group 5", "desc": "Change the current selection to Control Group 5." }, "selection.group.select.6": { "name": "Select Control Group 6", "desc": "Change the current selection to Control Group 6." }, "selection.group.select.7": { "name": "Select Control Group 7", "desc": "Change the current selection to Control Group 7." }, "selection.group.select.8": { "name": "Select Control Group 8", "desc": "Change the current selection to Control Group 8." }, "selection.group.select.9": { "name": "Select Control Group 9", "desc": "Change the current selection to Control Group 9." }, "selection.group.add.0": { "name": "Add Control Group 0", "desc": "Add Control Group 0 to the current selection." }, "selection.group.add.1": { "name": "Add Control Group 1", "desc": "Add Control Group 1 to the current selection." }, "selection.group.add.2": { "name": "Add Control Group 2", "desc": "Add Control Group 2 to the current selection." }, "selection.group.add.3": { "name": "Add Control Group 3", "desc": "Add Control Group 3 to the current selection." }, "selection.group.add.4": { "name": "Add Control Group 4", "desc": "Add Control Group 4 to the current selection." }, "selection.group.add.5": { "name": "Add Control Group 5", "desc": "Add Control Group 5 to the current selection." }, "selection.group.add.6": { "name": "Add Control Group 6", "desc": "Add Control Group 6 to the current selection." }, "selection.group.add.7": { "name": "Add Control Group 7", "desc": "Add Control Group 7 to the current selection." }, "selection.group.add.8": { "name": "Add Control Group 8", "desc": "Add Control Group 8 to the current selection." }, "selection.group.add.9": { "name": "Add Control Group 9", "desc": "Add Control Group 9 to the current selection." } } } } Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25938) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 25939) @@ -1,1712 +1,1712 @@ 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) { let cmd = { "x": placementSupport.position.x, "z": placementSupport.position.z, "range": placementSupport.attack.Ranged.maxRange, "elevationBonus": placementSupport.attack.Ranged.elevationBonus }; let averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); let 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]); + g_Selection.removeList([unit], true); 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 => { let state = GetEntityState(entity); return state && state.production && state.production.entities.length && state.production.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 }); } } /** * 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 }); // Train remainer in one more structure. let remainer = g_BatchTrainingEntityAllowedCount % batchedSize; if (remainer) Engine.PostNetworkCommand({ "type": "train", "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], "template": g_BatchTrainingType, "count": remainer }); } else Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings, "template": g_BatchTrainingType, "count": batchedSize }); } function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": 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 25938) +++ ps/trunk/binaries/data/mods/public/gui/session/selection.js (revision 25939) @@ -1,546 +1,573 @@ // Limits selection size var g_MaxSelectionSize = 200; // Alpha value of hovered/mouseover/highlighted selection overlays // (should probably be greater than always visible alpha value, // see CCmpSelectable) var g_HighlightedAlpha = 0.75; function _setHighlight(ents, alpha, selected) { if (ents.length) Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities": ents, "alpha": alpha, "selected": selected }); } function _setStatusBars(ents, enabled) { if (!ents.length) return; Engine.GuiInterfaceCall("SetStatusBars", { "entities": ents, "enabled": enabled, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } function _setMotionOverlay(ents, enabled) { if (ents.length) Engine.GuiInterfaceCall("SetMotionDebugOverlay", { "entities": ents, "enabled": enabled }); } function _playSound(ent) { Engine.GuiInterfaceCall("PlaySound", { "name": "select", "entity": ent }); } /** * EntityGroups class for managing grouped entities */ function EntityGroups() { this.groups = {}; this.ents = {}; } EntityGroups.prototype.reset = function() { this.groups = {}; this.ents = {}; }; EntityGroups.prototype.add = function(ents) { for (let ent of ents) { if (this.ents[ent]) continue; var entState = GetEntityState(ent); // When this function is called during group rebuild, deleted // entities will not yet have been removed, so entities might // still be present in the group despite not existing. if (!entState) continue; var templateName = entState.template; var key = GetTemplateData(templateName).selectionGroupName || templateName; // Group the ents by player and template if (entState.player !== undefined) key = "p" + entState.player + "&" + key; if (this.groups[key]) this.groups[key] += 1; else this.groups[key] = 1; this.ents[ent] = key; } }; EntityGroups.prototype.removeEnt = function(ent) { var key = this.ents[ent]; // Remove the entity delete this.ents[ent]; --this.groups[key]; // Remove the entire group if (this.groups[key] == 0) delete this.groups[key]; }; EntityGroups.prototype.rebuildGroup = function(renamed) { var oldGroup = this.ents; this.reset(); var toAdd = []; for (var ent in oldGroup) toAdd.push(renamed[ent] ? renamed[ent] : +ent); this.add(toAdd); }; EntityGroups.prototype.getCount = function(key) { return this.groups[key]; }; EntityGroups.prototype.getTotalCount = function() { let totalCount = 0; for (let key in this.groups) totalCount += this.groups[key]; return totalCount; }; EntityGroups.prototype.getKeys = function() { // Preserve order even when shuffling units around // Can be optimized by moving the sorting elsewhere return Object.keys(this.groups).sort(); }; EntityGroups.prototype.getEntsByKey = function(key) { var ents = []; for (var ent in this.ents) if (this.ents[ent] == key) ents.push(+ent); return ents; }; /** * get a list of entities grouped by a key */ EntityGroups.prototype.getEntsGrouped = function() { return this.getKeys().map(key => ({ "ents": this.getEntsByKey(key), "key": key })); }; /** * Gets all ents in every group except ones of the specified group */ EntityGroups.prototype.getEntsByKeyInverse = function(key) { var ents = []; for (var ent in this.ents) if (this.ents[ent] != key) ents.push(+ent); return ents; }; /** * EntitySelection class for managing the entity selection list and the primary selection */ function EntitySelection() { // Private properties: this.selected = new Set(); // For mouseover-highlighted entity IDs in these. this.highlighted = new Set(); this.motionDebugOverlay = false; // Public properties: this.dirty = false; // set whenever the selection has changed this.groups = new EntityGroups(); } /** * Deselect everything but entities of the chosen type if inverse is true otherwise deselect just the chosen entity */ EntitySelection.prototype.makePrimarySelection = function(key, inverse) { let ents = inverse ? this.groups.getEntsByKeyInverse(key) : this.groups.getEntsByKey(key); this.reset(); this.addList(ents); }; /** * Get a list of the template names */ EntitySelection.prototype.getTemplateNames = function() { const templateNames = []; for (const ent of this.selected) { const entState = GetEntityState(ent); if (entState) templateNames.push(entState.template); } return templateNames; }; /** * Update the selection to take care of changes (like units that have been killed). */ EntitySelection.prototype.update = function() { this.checkRenamedEntities(); const controlsAll = g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll; const removeOwnerChanges = !g_IsObserver && !controlsAll && this.selected.size > 1; let changed = false; for (const ent of this.selected) { const entState = GetEntityState(ent); if (!entState) { this.selected.delete(ent); this.groups.removeEnt(ent); changed = true; continue; } // Remove non-visible units (e.g. moved back into fog-of-war) // At the next update, mirages will be renamed to the real // entity they replace, so just ignore them now // Futhermore, when multiple selection, remove units which have changed ownership if (entState.visibility == "hidden" && !entState.mirage || removeOwnerChanges && entState.player != g_ViewedPlayer) { // Disable any highlighting of the disappeared unit _setHighlight([ent], 0, false); _setStatusBars([ent], false); _setMotionOverlay([ent], false); this.selected.delete(ent); this.groups.removeEnt(ent); changed = true; continue; } } if (changed) this.onChange(); }; /** * Update selection if some selected entities were renamed * (in case of unit promotion or finishing building structure) */ EntitySelection.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for (let renamedEntity of renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; // Reconstruct the selection if at least one entity has been renamed. for (let renamedEntity of renamedEntities) if (this.selected.has(renamedEntity.entity)) { this.rebuildSelection(renamedLookup); return; } } }; /** * Add entities to selection. Play selection sound unless quiet is true */ EntitySelection.prototype.addList = function(ents, quiet, force = false) { // If someone else's player is the sole selected unit, don't allow adding to the selection. const firstEntState = this.selected.size == 1 && GetEntityState(this.getFirstSelected()); if (firstEntState && firstEntState.player != g_ViewedPlayer && !force) return; - let added = []; + const added = []; - for (const ent of ents) + for (const ent of this.addFormationMembers(ents)) { if (this.selected.size >= g_MaxSelectionSize) break; if (this.selected.has(ent)) continue; const entState = GetEntityState(ent); if (!entState) continue; let isUnowned = g_ViewedPlayer != -1 && entState.player != g_ViewedPlayer || g_ViewedPlayer == -1 && entState.player == 0; // Don't add unowned entities to the list, unless a single entity was selected if (isUnowned && (ents.length > 1 || this.selected.size) && !force) continue; added.push(ent); this.selected.add(ent); } _setHighlight(added, 1, true); _setStatusBars(added, true); _setMotionOverlay(added, this.motionDebugOverlay); if (added.length) { // Play the sound if the entity is controllable by us or Gaia-owned. var owner = GetEntityState(added[0]).player; if (!quiet && (controlsPlayer(owner) || g_IsObserver || owner == 0)) _playSound(added[0]); } this.groups.add(this.toList()); // Create Selection Groups this.onChange(); }; -EntitySelection.prototype.removeList = function(ents) +/** + * @param {number[]} ents - The entities to remove. + * @param {boolean} dontAddFormationMembers - If true we need to exclude adding formation members. + */ +EntitySelection.prototype.removeList = function(ents, dontAddFormationMembers = false) { - var removed = []; + const removed = []; - for (let ent of ents) + for (const ent of dontAddFormationMembers ? ents : this.addFormationMembers(ents)) if (this.selected.has(ent)) { this.groups.removeEnt(ent); removed.push(ent); this.selected.delete(ent); } _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setMotionOverlay(removed, false); this.onChange(); }; EntitySelection.prototype.reset = function() { _setHighlight(this.toList(), 0, false); _setStatusBars(this.toList(), false); _setMotionOverlay(this.toList(), false); this.selected.clear(); this.groups.reset(); this.onChange(); }; EntitySelection.prototype.rebuildSelection = function(renamed) { const toAdd = []; for (const ent of this.selected) toAdd.push(renamed[ent] || ent); this.reset(); this.addList(toAdd, true); // don't play selection sounds }; EntitySelection.prototype.getFirstSelected = function() { for (const ent of this.selected) return ent; return undefined; }; /** * TODO: This array should not be recreated every call */ EntitySelection.prototype.toList = function() { return Array.from(this.selected); }; /** * @return {number} - The number of entities selected. */ EntitySelection.prototype.size = function() { return this.selected.size; }; EntitySelection.prototype.find = function(condition) { for (const ent of this.selected) if (condition(ent)) return ent; return null; }; /** * @param {function} condition - A function. * @return {number[]} - The entities passing the condition. */ EntitySelection.prototype.filter = function(condition) { const result = []; for (const ent of this.selected) if (condition(ent)) result.push(ent); return result; }; -EntitySelection.prototype.setHighlightList = function(ents) +EntitySelection.prototype.setHighlightList = function(entities) { const highlighted = new Set(); + const ents = this.addFormationMembers(entities); for (const ent of ents) highlighted.add(ent); const removed = []; const added = []; // Remove highlighting for the old units that are no longer highlighted // (excluding ones that are actively selected too). for (const ent of this.highlighted) if (!highlighted.has(ent) && !this.selected.has(ent)) removed.push(ent); // Add new highlighting for units that aren't already highlighted. for (const ent of ents) if (!this.highlighted.has(ent) && !this.selected.has(ent)) added.push(ent); _setHighlight(removed, 0, false); _setStatusBars(removed, false); _setHighlight(added, g_HighlightedAlpha, true); _setStatusBars(added, true); this.highlighted = highlighted; }; EntitySelection.prototype.SetMotionDebugOverlay = function(enabled) { this.motionDebugOverlay = enabled; _setMotionOverlay(this.toList(), enabled); }; EntitySelection.prototype.onChange = function() { this.dirty = true; if (this.isSelection) onSelectionChange(); }; EntitySelection.prototype.selectAndMoveTo = function(entityID) { let entState = GetEntityState(entityID); if (!entState || !entState.position) return; this.reset(); this.addList([entityID]); Engine.CameraMoveTo(entState.position.x, entState.position.z); } /** + * Adds the formation members of a selected entities to the selection. + * @param {number[]} entities - The entity IDs of selected entities. + * @return {number[]} - Some more entity IDs if part of a formation was selected. + */ +EntitySelection.prototype.addFormationMembers = function(entities) +{ + if (!entities.length || Engine.HotkeyIsPressed("selection.singleselection")) + return entities; + + const result = new Set(entities); + for (const entity of entities) + { + const entState = GetEntityState(+entity); + if (entState?.unitAI?.formation) + for (const member of GetEntityState(+entState.unitAI.formation).formation.members) + result.add(member); + } + + return result; +}; + +/** * Cache some quantities which depends only on selection */ var g_Selection = new EntitySelection(); g_Selection.isSelection = true; var g_canMoveIntoFormation = {}; var g_allBuildableEntities; var g_allTrainableEntities; // Reset cached quantities function onSelectionChange() { g_canMoveIntoFormation = {}; g_allBuildableEntities = undefined; g_allTrainableEntities = undefined; } /** * EntityGroupsContainer class for managing grouped entities */ function EntityGroupsContainer() { this.groups = []; for (var i = 0; i < 10; ++i) this.groups[i] = new EntityGroups(); } /** * Add entities to a group. * @param {string} groupName - The number of the group to add the entities to. * @param {number[]} ents - The entities to add to the group. */ EntityGroupsContainer.prototype.addEntities = function(groupName, ents) { if (Engine.ConfigDB_GetValue("user", "gui.session.disjointcontrolgroups") == "true") for (let ent of ents) for (let group of this.groups) if (ent in group.ents) group.removeEnt(ent); this.groups[groupName].add(ents); }; EntityGroupsContainer.prototype.update = function() { this.checkRenamedEntities(); for (let group of this.groups) for (var ent in group.ents) { var entState = GetEntityState(+ent); // Remove deleted units if (!entState) group.removeEnt(ent); } }; /** * Update control group if some entities in the group were renamed * (in case of unit promotion or finishing building structure) */ EntityGroupsContainer.prototype.checkRenamedEntities = function() { var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities"); if (renamedEntities.length > 0) { var renamedLookup = {}; for (let renamedEntity of renamedEntities) renamedLookup[renamedEntity.entity] = renamedEntity.newentity; for (let group of this.groups) for (let renamedEntity of renamedEntities) // Reconstruct the group if at least one entity has been renamed. if (renamedEntity.entity in group.ents) { group.rebuildGroup(renamedLookup); break; } } }; var g_Groups = new EntityGroupsContainer(); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25938) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 25939) @@ -1,2146 +1,2153 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronized for the biggest part, // so most of the attributes shouldn't be serialized. // Return an object with a small selection of deterministic data. return { "timeNotifications": this.timeNotifications, "timeNotificationID": this.timeNotificationID }; }; GuiInterface.prototype.Deserialize = function(data) { this.Init(); this.timeNotifications = data.timeNotifications; this.timeNotificationID = data.timeNotificationID; }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; this.templateModified = {}; this.selectionDirty = {}; this.obstructionSnap = new ObstructionSnap(); }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function() { let ret = { "players": [] }; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits); // Work out which phase we are in. let phase = ""; let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } let allies = []; let mutualAllies = []; let neutrals = []; let enemies = []; for (let j = 0; j < numPlayers; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } ret.players.push({ "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "color": cmpPlayer.GetColor(), "controlsAll": cmpPlayer.CanControlAllUnits(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "resourceGatherers": cmpPlayer.GetResourceGatherers(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), "hasSharedLos": cmpPlayer.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": cmpPlayer.CanBarter(), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer) }); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = cmpTerrain.GetMapSize(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function() { let ret = this.GetSimulationState(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; /** * Returns the gamesettings that were chosen at the time the match started. */ GuiInterface.prototype.GetInitAttributes = function() { return InitAttributes; }; /** * This data will be stored in the replay metadata file after a match has been finished recording. */ GuiInterface.prototype.GetReplayMetadata = function() { let extendedSimState = this.GetExtendedSimulationState(); return { "timeElapsed": extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "mapSettings": InitAttributes.settings }; }; /** * Called when the game ends if the current game is part of a campaign run. */ GuiInterface.prototype.GetCampaignGameEndData = function(player) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (Trigger.prototype.OnCampaignGameEnd) return Trigger.prototype.OnCampaignGameEnd(); return {}; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function() { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({ "entity": entity, "newentity": mirage }); }; /** * Get common entity info, often used in the gui. */ GuiInterface.prototype.GetEntityState = function(player, ent) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); if (!ent) return null; // All units must have a template; if not then it's a nonexistent entity id. let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; let ret = { "id": ent, "player": INVALID_PLAYER, "template": template }; let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable(), "hasSomeFormation": cmpIdentity.HasSomeFormation(), "formations": cmpIdentity.GetFormationsList(), "controllable": cmpIdentity.IsControllable() }; + const cmpFormation = Engine.QueryInterface(ent, IID_Formation); + if (cmpFormation) + ret.formation = { + "members": cmpFormation.GetMembers() + }; + let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) ret.position = cmpPosition.GetPosition(); let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured(); ret.needsHeal = !cmpHealth.IsUnhealable(); } let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; let cmpMarket = QueryMiragedInterface(ent, IID_Market); if (cmpMarket) ret.market = { "land": cmpMarket.HasType("land"), "naval": cmpMarket.HasType("naval") }; let cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress() }; let cmpPopulation = Engine.QueryInterface(ent, IID_Population); if (cmpPopulation) ret.population = { "bonus": cmpPopulation.GetPopBonus() }; let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades": cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo(), "isUpgrading": cmpUpgrade.IsUpgrading() }; let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver); if (cmpStatusEffects) ret.statusEffects = cmpStatusEffects.GetActiveStatuses(); let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(), "queue": cmpProductionQueue.GetQueue(), "autoqueue": cmpProductionQueue.IsAutoQueueing() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) ret.foundation = { "numBuilders": cmpFoundation.GetNumBuilders(), "buildTime": cmpFoundation.GetBuildTime() }; let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders(), "buildTime": cmpRepairable.GetBuildTime() }; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) ret.player = cmpOwnership.GetOwner(); let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "occupiedSlots": cmpGarrisonHolder.OccupiedSlots() }; let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); if (cmpTurretHolder) ret.turretHolder = { "turretPoints": cmpTurretHolder.GetTurretPoints() }; let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (cmpTurretable) ret.turretable = { "ejectable": cmpTurretable.IsEjectable(), "holder": cmpTurretable.HolderID() }; let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) ret.garrisonable = { "holder": cmpGarrisonable.HolderID(), "size": cmpGarrisonable.UnitSize() }; let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "selectableStances": cmpUnitAI.GetSelectableStances(), - "isIdle": cmpUnitAI.IsIdle() + "isIdle": cmpUnitAI.IsIdle(), + "formation": cmpUnitAI.GetFormationController() }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) ret.guard = { "entities": cmpGuard.GetEntities() }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } let cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) ret.gate = { "locked": cmpGate.IsLocked() }; let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) ret.alertRaiser = true; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); let cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (let type of types) { ret.attack[type] = {}; Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type)); ret.attack[type].attackName = cmpAttack.GetAttackName(type); ret.attack[type].splash = cmpAttack.GetSplashData(type); if (ret.attack[type].splash) Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true)); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { // Not a ranged attack, set some defaults. ret.attack[type].elevationBonus = 0; ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } ret.attack[type].elevationBonus = range.elevationBonus; if (cmpPosition && cmpPosition.IsInWorld()) // For units, take the range in front of it, no spread, so angle = 0, // else, take the average elevation around it: angle = 2 * pi. ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI); else // Not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } let cmpResistance = QueryMiragedInterface(ent, IID_Resistance); if (cmpResistance) ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity"); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "numGatherers": cmpResourceSupply.GetNumGatherers() }; let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes(), "sharable": cmpResourceDropsite.IsSharable(), "shared": cmpResourceDropsite.IsShared() }; let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "health": cmpHeal.GetHealth(), "range": cmpHeal.GetRange().max, "interval": cmpHeal.GetInterval(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses() }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { ret.loot = cmpLoot.GetResources(); ret.loot.xp = cmpLoot.GetXp(); } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) ret.resourceTrickle = { "interval": cmpResourceTrickle.GetInterval(), "rates": cmpResourceTrickle.GetRates() }; let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure); if (cmpTreasure) ret.treasure = { "collectTime": cmpTreasure.CollectionTime(), "resources": cmpTreasure.Resources() }; let cmpTreasureCollector = Engine.QueryInterface(ent, IID_TreasureCollector); if (cmpTreasureCollector) ret.treasureCollector = true; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier() }; let cmpUpkeep = Engine.QueryInterface(ent, IID_Upkeep); if (cmpUpkeep) ret.upkeep = { "interval": cmpUpkeep.GetInterval(), "rates": cmpUpkeep.GetRates() }; return ret; }; GuiInterface.prototype.GetMultipleEntityStates = function(player, ents) { return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) })); }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); let rot = { "x": 0, "y": 0, "z": 0 }; let pos = { "x": cmd.x, "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z), "z": cmd.z }; let elevationBonus = cmd.elevationBonus || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, data) { let templateName = data.templateName; let owner = data.player !== undefined ? data.player : player; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) return GetTemplateDataHelper(template, owner, aurasTemplate); let auraNames = template.Auras._string.split(/\s+/); for (let name of auraNames) { let auraTemplate = AuraTemplates.Get(name); if (!auraTemplate) error("Template " + templateName + " has undefined aura " + name); else aurasTemplate[name] = auraTemplate; } return GetTemplateDataHelper(template, owner, aurasTemplate); }; GuiInterface.prototype.IsTechnologyResearched = function(player, data) { if (!data.tech) return true; let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(data.tech); }; /** * Checks whether the requirements for this technology have been met. */ GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) { let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(data.tech); }; /** * Returns technologies that are being actively researched, along with * which entity is researching them and how far along the research is. */ GuiInterface.prototype.GetStartedResearch = function(player) { let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return {}; let ret = {}; for (let tech of cmpTechnologyManager.GetStartedTechs()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); if (cmpProductionQueue) { ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress; ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining; } else { ret[tech].progress = 0; ret[tech].timeRemaining = 0; } } return ret; }; /** * Returns the battle state of the player. */ GuiInterface.prototype.GetBattleState = function(player) { let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (!cmpBattleDetection) return false; return cmpBattleDetection.GetState(); }; /** * Returns a list of ongoing attacks against the player. */ GuiInterface.prototype.GetIncomingAttacks = function(player) { let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); if (!cmpAttackDetection) return []; return cmpAttackDetection.GetIncomingAttacks(); }; /** * Used to show a red square over GUI elements you can't yet afford. */ GuiInterface.prototype.GetNeededResources = function(player, data) { let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player); return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {}; }; /** * State of the templateData (player dependent): true when some template values have been modified * and need to be reloaded by the gui. */ GuiInterface.prototype.OnTemplateModification = function(msg) { this.templateModified[msg.player] = true; this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.IsTemplateModified = function(player) { return this.templateModified[player] || false; }; GuiInterface.prototype.ResetTemplateModified = function() { this.templateModified = {}; }; /** * Some changes may require an update to the selection panel, * which is cached for efficiency. Inform the GUI it needs reloading. */ GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg) { this.selectionDirty[msg.player] = true; }; GuiInterface.prototype.SetSelectionDirty = function(player) { this.selectionDirty[player] = true; }; GuiInterface.prototype.IsSelectionDirty = function(player) { return this.selectionDirty[player] || false; }; GuiInterface.prototype.ResetSelectionDirty = function() { this.selectionDirty = {}; }; /** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) * so they should allways be added and deleted in a deterministic way. */ GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); notification.endTime = duration + cmpTimer.GetTime(); notification.id = ++this.timeNotificationID; // Let all players and observers receive the notification by default. if (!notification.players) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } this.timeNotifications.push(notification); this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime); cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID); }; GuiInterface.prototype.GetTimeNotifications = function(player) { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // Filter on players and time, since the delete timer might be executed with a delay. return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time); }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") this.AddTimeNotification(notification); else this.notifications.push(notification); }; GuiInterface.prototype.GetNotifications = function() { let n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { let cmpPlayer = QueryPlayerIDInterface(wantedPlayer); if (!cmpPlayer) return []; return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return {}; return { "name": template.Formation.FormationName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Formation.Icon }; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { return data.ents.some(ent => { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate; }); }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance) return true; } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; for (let ent of cmd.entities) { let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (let building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data) { let updateEntityColor = (iids, entities) => { for (let ent of entities) for (let iid of iids) { let cmp = Engine.QueryInterface(ent, iid); if (cmp) cmp.UpdateColor(); } }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 1; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i, IID_Player); if (!cmpPlayer) continue; cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors); if (data.displayDiplomacyColors) cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]); updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ? [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] : [IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer], cmpRangeManager.GetEntitiesByPlayer(i)); } updateEntityColor([IID_Selectable, IID_StatusBars], data.selected); Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors(); }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { // Cache of owner -> color map let playerColors = {}; for (let ent of cmd.entities) { let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color. let owner = INVALID_PLAYER; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); let color = playerColors[owner]; if (!color) { color = { "r": 1, "g": 1, "b": 1 }; let cmpPlayer = QueryPlayerIDInterface(owner); if (cmpPlayer) color = cmpPlayer.GetDisplayedColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(this.entsWithAuraAndStatusBars); }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { let affectedEnts = new Set(); for (let ent of cmd.entities) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (!cmpStatusBars) continue; cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; for (let name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; for (let e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); else this.entsWithAuraAndStatusBars.delete(ent); } } for (let ent of affectedEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RegenerateSprites(); } }; GuiInterface.prototype.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player); }; GuiInterface.prototype.GetNonGaiaEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them. for (let ent of this.entsRallyPointsDisplayed) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities. for (let ent of cmd.entities) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // Entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location). let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner. let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position. let pos; if (cmd.x && cmd.z) pos = cmd; else // May return undefined if no rally point is set. pos = cmpRallyPoint.GetPositions()[0]; if (pos) { // Only update the position if we changed it (cmd.queued is set). // Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z. if ("queued" in cmd) { if (cmd.queued == true) cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z)); else cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z)); } else if (!cmpRallyPointRenderer.IsSet()) // Rebuild the renderer when not set (when reading saved game or in case of building update). for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z)); cmpRallyPointRenderer.SetDisplayed(true); // Remember which entities have their rally points displayed so we can hide them again. this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(cmd.owner); let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": localisation info (optional) * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { let result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [] }; if (!this.placementEntity || this.placementEntity[0] != cmd.template) { if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; let pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // Set it to a red shade if this is an invalid location. let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { let wallSet = cmd.wallSet; // Did the start position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let start = { "pos": cmd.start, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // Did the end position snap to anything? // If we snapped, was it to an entity? If yes, hold that entity's ID. let end = { "pos": cmd.end, "angle": 0, "snapped": false, "snappedEnt": INVALID_ENTITY }; // -------------------------------------------------------------------------------- // Do some entity cache management and check for snapping. if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // We're clearing the preview, clear the entity cache and bail. for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // Keep template data around. } return false; } for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) { let pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before. for (let type in wallSet.templates) { if (type == "curves") continue; let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, { "templateName": tpl }), }; if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } // Prevent division by zero errors further on if the start and end positions are the same. if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { // Value of 0.5 was determined through trial and error. let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; let startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { let endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // Clear the single-building preview entity (we'll be rolling our own). this.SetBuildingPlacementPreview(player, { "template": "" }); // -------------------------------------------------------------------------------- // Calculate wall placement and position preview entities. let result = { "pieces": [], "cost": { "population": 0, "time": 0 } }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) // See helpers/Walls.js. previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group. let startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true // Preview only, must not appear in the result. }); } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle }); } if (end.pos) { // Analogous to the starting side case above. if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || []; previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup()); } // If we're snapping to a foundation, add an extra preview tower and also set it to the same control group. let endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true }); } } else previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle }); } let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. let allPiecesValid = true; // Number of entities that are required to build the entire wall, regardless of validity. let numRequiredPieces = 0; for (let i = 0; i < previewEntities.length; ++i) { let entInfo = previewEntities[i]; let ent = null; let tpl = entInfo.template; let tplData = this.placementWallEntities[tpl].templateData; let entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else ent = entPool.entities[entPool.numUsed]; if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // Move piece to right location. // TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities. let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces. if (tpl === wallSet.templates.tower) { let terrainGroundPrev = null; let terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { let targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. let primaryControlGroup = ent; let secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); let validPlacement = false; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region. // TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta. let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden"; if (visible) { let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement. validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success; // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) ++numRequiredPieces; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: We should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. for (let res of Resources.GetCodes().concat(["population", "time"])) result.cost[res] += tplData.cost[res]; } let canAfford = true; let cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) canAfford = false; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } ++entPool.numUsed; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest. // (TODO: Break unlikely ties by choosing the lowest entity ID.) let minDist2 = -1; let minDistEntitySnapData = null; let radius2 = data.snapRadius * data.snapRadius; for (let ent of data.snapEntities) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = { "x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent }; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (data.snapToEdges) { let position = this.obstructionSnap.getPosition(data, template); if (position) return position; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySoundForPlayer = function(player, data) { let playerEntityID = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(player); let cmpSound = Engine.QueryInterface(playerEntityID, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(data.name); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, player); }; GuiInterface.prototype.PlaySound = function(player, data) { if (!data.entity) return; PlaySound(data.name, data.entity); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. let bucket = filtered.bucket; if (bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle()) return { "idle": false }; let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned()) return { "idle": false }; const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable); if (cmpTurretable && cmpTurretable.IsTurreted()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if (!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; let cmpMarket = QueryMiragedInterface(data.firstMarket, IID_Market); return cmpMarket && cmpMarket.CalculateTraderGain(data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; let firstMarket = cmpEntityTrader.GetFirstMarket(); let secondMarket = cmpEntityTrader.GetSecondMarket(); let result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGoods().amount; } else if (data.target === secondMarket) result = { "type": "is second", "gain": cmpEntityTrader.GetGoods().amount, }; else if (!firstMarket) result = { "type": "set first" }; else if (!secondMarket) result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; else result = { "type": "set first" }; return result; }; GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); if (!cmpProductionQueue) return 0; return cmpProductionQueue.GetBatchTime(data.batchSize); }; GuiInterface.prototype.IsMapRevealed = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for (let ent of data.entities) { let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of traders) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { let holder = cmpUnitAI.order.data.target; let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return []; return cmpPlayer.GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; /** * List the GuiInterface functions that can be safely called by GUI scripts. * (GUI scripts are non-deterministic and untrusted, so these functions must be * appropriately careful. They are called with a first argument "player", which is * trusted and indicates the player associated with the current client; no data should * be returned unless this player is meant to be able to see it.) */ let exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetInitAttributes": 1, "GetReplayMetadata": 1, "GetCampaignGameEndData": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetMultipleEntityStates": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNotifications": 1, "GetTimeNotifications": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "UpdateDisplayedPlayerColors": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "PlaySoundForPlayer": 1, "FindIdleUnits": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "EnableVisualRangeOverlayType": 1, "SetRangeOverlays": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, "IsTemplateModified": 1, "ResetTemplateModified": 1, "IsSelectionDirty": 1, "ResetSelectionDirty": 1 }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); throw new Error("Invalid GuiInterface Call name \"" + name + "\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 25938) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 25939) @@ -1,616 +1,617 @@ Engine.LoadHelperScript("ObstructionSnap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/AlertRaiser.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Barter.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); +Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Gate.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Market.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/Population.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/ResourceDropsite.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/ResourceTrickle.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Trader.js"); Engine.LoadComponentScript("interfaces/TurretHolder.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/Treasure.js"); Engine.LoadComponentScript("interfaces/TreasureCollector.js"); Engine.LoadComponentScript("interfaces/Turretable.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("interfaces/Upkeep.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("GuiInterface.js"); Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetNames": () => ({ "food": "Food", "metal": "Metal", "stone": "Stone", "wood": "Wood" }), "GetResource": resource => ({ "aiAnalysisInfluenceGroup": resource == "food" ? "ignore" : resource == "wood" ? "abundant" : "sparse" }) }; var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); AddMock(SYSTEM_ENTITY, IID_Barter, { "GetPrices": function() { return { "buy": { "food": 150 }, "sell": { "food": 25 } }; } }); AddMock(SYSTEM_ENTITY, IID_EndGameManager, { "GetVictoryConditions": () => ["conquest", "wonder"], "GetAlliedVictory": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": function() { return 2; }, "GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; }, "GetMaxWorldPopulation": function() {} }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "GetLosVisibility": function(ent, player) { return "visible"; }, "GetLosCircular": function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetCurrentTemplateName": function(ent) { return "example"; }, "GetTemplate": function(name) { return ""; } }); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetTime": function() { return 0; }, "SetTimeout": function(ent, iid, funcname, time, data) { return 0; } }); AddMock(100, IID_Player, { "GetName": function() { return "Player 1"; }, "GetCiv": function() { return "gaia"; }, "GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; }, "CanControlAllUnits": function() { return false; }, "GetPopulationCount": function() { return 10; }, "GetPopulationLimit": function() { return 20; }, "GetMaxPopulation": function() { return 200; }, "GetResourceCounts": function() { return { "food": 100 }; }, "GetResourceGatherers": function() { return { "food": 1 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, "GetTeam": function() { return -1; }, "GetLockTeams": function() { return false; }, "GetCheatsEnabled": function() { return false; }, "GetDiplomacy": function() { return [-1, 1]; }, "IsAlly": function() { return false; }, "IsMutualAlly": function() { return false; }, "IsNeutral": function() { return false; }, "IsEnemy": function() { return true; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, "GetSpyCostMultiplier": function() { return 1; }, "HasSharedDropsites": function() { return false; }, "HasSharedLos": function() { return false; } }); AddMock(100, IID_EntityLimits, { "GetLimits": function() { return { "Foo": 10 }; }, "GetCounts": function() { return { "Foo": 5 }; }, "GetLimitChangers": function() { return { "Foo": {} }; }, "GetMatchCounts": function() { return { "Bar": 0 }; } }); AddMock(100, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(100, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); AddMock(101, IID_Player, { "GetName": function() { return "Player 2"; }, "GetCiv": function() { return "mace"; }, "GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; }, "CanControlAllUnits": function() { return true; }, "GetPopulationCount": function() { return 40; }, "GetPopulationLimit": function() { return 30; }, "GetMaxPopulation": function() { return 300; }, "GetResourceCounts": function() { return { "food": 200 }; }, "GetResourceGatherers": function() { return { "food": 3 }; }, "GetPanelEntities": function() { return []; }, "IsTrainingBlocked": function() { return false; }, "GetState": function() { return "active"; }, "GetTeam": function() { return -1; }, "GetLockTeams": function() {return false; }, "GetCheatsEnabled": function() { return false; }, "GetDiplomacy": function() { return [-1, 1]; }, "IsAlly": function() { return true; }, "IsMutualAlly": function() {return false; }, "IsNeutral": function() { return false; }, "IsEnemy": function() { return false; }, "GetDisabledTemplates": function() { return {}; }, "GetDisabledTechnologies": function() { return {}; }, "CanBarter": function() { return false; }, "GetSpyCostMultiplier": function() { return 1; }, "HasSharedDropsites": function() { return false; }, "HasSharedLos": function() { return false; } }); AddMock(101, IID_EntityLimits, { "GetLimits": function() { return { "Bar": 20 }; }, "GetCounts": function() { return { "Bar": 0 }; }, "GetLimitChangers": function() { return { "Bar": {} }; }, "GetMatchCounts": function() { return { "Foo": 0 }; } }); AddMock(101, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(101, IID_StatisticsTracker, { "GetBasicStatistics": function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, "GetSequences": function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, "IncreaseTrainedUnitsCounter": function() { return 1; }, "IncreaseConstructedBuildingsCounter": function() { return 1; }, "IncreaseBuiltCivCentresCounter": function() { return 1; } }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r": 1, "g": 1, "b": 1, "a": 1 }, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "resourceGatherers": { "food": 1 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } }, { "name": "Player 2", "civ": "mace", "color": { "r": 1, "g": 0, "b": 0, "a": 1 }, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "resourceGatherers": { "food": 3 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } } ], "circularMap": false, "timeElapsed": 0, "victoryConditions": ["conquest", "wonder"], "alliedVictory": false, "maxWorldPopulation": undefined }); AddMock(10, IID_Builder, { "GetEntitiesList": function() { return ["test1", "test2"]; }, }); AddMock(10, IID_Health, { "GetHitpoints": function() { return 50; }, "GetMaxHitpoints": function() { return 60; }, "IsRepairable": function() { return false; }, "IsUnhealable": function() { return false; } }); AddMock(10, IID_Identity, { "GetClassesList": function() { return ["class1", "class2"]; }, "GetRank": function() { return "foo"; }, "GetSelectionGroupName": function() { return "Selection Group Name"; }, "HasClass": function() { return true; }, "IsUndeletable": function() { return false; }, "IsControllable": function() { return true; }, "HasSomeFormation": function() { return false; }, "GetFormationsList": function() { return []; }, }); AddMock(10, IID_Position, { "GetTurretParent": function() { return INVALID_ENTITY; }, "GetPosition": function() { return { "x": 1, "y": 2, "z": 3 }; }, "IsInWorld": function() { return true; } }); AddMock(10, IID_ResourceTrickle, { "GetInterval": () => 1250, "GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 }) }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), { "id": 10, "player": INVALID_PLAYER, "template": "example", "identity": { "rank": "foo", "classes": ["class1", "class2"], "selectionGroupName": "Selection Group Name", "canDelete": true, "hasSomeFormation": false, "formations": [], "controllable": true, }, "position": { "x": 1, "y": 2, "z": 3 }, "hitpoints": 50, "maxHitpoints": 60, "needsRepair": false, "needsHeal": true, "builder": true, "visibility": "visible", "isBarterMarket": true, "resourceTrickle": { "interval": 1250, "rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 } } });