Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 23329) +++ ps/trunk/binaries/data/config/default.cfg (revision 23330) @@ -1,519 +1,521 @@ ; 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 ; 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 shadowsonwater = false shadows = true shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -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 vsync = false particles = true fog = true silhouettes = true showsky = true nos3tc = false noautomipmap = true novbo = false noframebufferobject = false ; Disable hardware cursors nohwcursor = false ; Linux only: Set the driconf force_s3tc_enable option at startup, ; for compressed texture support force_s3tc_enable = true ; 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 ; 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 = 30 ; Throttle FPS in menus only. [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 = "Ctrl+Break", "Super+Q" ; Exit to desktop cancel = Escape ; Close or cancel the current dialog box/popup confirm = Return ; Confirm the current command pause = Pause ; 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 showsky = "Alt+Z" ; Toggle sky ; > 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 session.devcommands.toggle = "Alt+D" ; Toggle developer commands panel timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter ceasefirecounter.toggle = unused ; Toggle ceasefire counter session.showstatusbars = Tab ; Toggle display of status bars session.highlightguarding = PgDn ; Toggle highlight of guarding units session.highlightguarded = PgUp ; Toggle highlight of guarded units session.toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures session.toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures session.togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units session.diplomacycolors = "Alt+X" ; Toggle diplomacy colors ; > 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 = unused ; Focus the camera on the rally point of the selected building zoom.in = Plus, Equals, 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] 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 cancel = Esc ; Un-select all units and cancel building placement idleworker = Period ; Select next idle worker idlewarrior = ForwardSlash ; Select next idle warrior idleunit = BackSlash ; Select next idle unit offscreen = Alt ; Include offscreen units in selection [hotkey.selection.group.add] 0 = "Shift+0" 1 = "Shift+1" 2 = "Shift+2" 3 = "Shift+3" 4 = "Shift+4" 5 = "Shift+5" 6 = "Shift+6" 7 = "Shift+7" 8 = "Shift+8" 9 = "Shift+9" [hotkey.selection.group.save] 0 = "Ctrl+0" 1 = "Ctrl+1" 2 = "Ctrl+2" 3 = "Ctrl+3" 4 = "Ctrl+4" 5 = "Ctrl+5" 6 = "Ctrl+6" 7 = "Ctrl+7" 8 = "Ctrl+8" 9 = "Ctrl+9" [hotkey.selection.group.select] 0 = 0 1 = 1 2 = 2 3 = 3 4 = 4 5 = 5 6 = 6 7 = 7 8 = 8 9 = 9 [hotkey.session] kill = Delete ; 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 move = unused ; 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 (should contain the attackmove keys) garrison = Ctrl ; Modifier to garrison when clicking on building 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 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 [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 ; 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 = Space ; If timewarp mode enabled, speed up the game rewind = 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 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) +snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges [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 [joystick.camera] pan.x = 0 pan.y = 1 rotate.x = 3 rotate.y = 2 zoom.in = 5 zoom.out = 4 [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 = "arena24" ; 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 = "wfgbot24" ; Name of the server-side XMPP-account that manage games echelon = "echelon24" ; 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 = "RWTsHxQMrRq4xwHisyBa2rNQfAedcINzbTT83jeX4/ZcfVxqLfWB4y8w" ; 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 gamestarttimeout = 60000 ; Don't disconnect clients timing out in the loading screen and rejoin process before exceeding this timeout. [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 [sound] mastergain = 0.9 musicgain = 0.2 ambientgain = 0.6 actiongain = 0.7 uigain = 0.7 [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/globalscripts/vector.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/vector.js (revision 23329) +++ ps/trunk/binaries/data/mods/public/globalscripts/vector.js (revision 23330) @@ -1,431 +1,443 @@ ///////////////////////////////////////////////////////////////////// // Vector2D // // Class for representing and manipulating 2D vectors // ///////////////////////////////////////////////////////////////////// // TODO: Type errors if v not instanceof Vector classes // TODO: Possibly implement in C++ function Vector2D(x = 0, y = 0) { this.set(x, y); } Vector2D.prototype.clone = function() { return new Vector2D(this.x, this.y); }; // Mutating 2D functions // // These functions modify the current object, // and always return this object to allow chaining Vector2D.prototype.set = function(x, y) { this.x = x; this.y = y; return this; }; +Vector2D.prototype.setFrom = function(v) +{ + this.x = v.x; + this.y = v.y; + return this; +}; + Vector2D.prototype.add = function(v) { this.x += v.x; this.y += v.y; return this; }; Vector2D.prototype.sub = function(v) { this.x -= v.x; this.y -= v.y; return this; }; Vector2D.prototype.mult = function(f) { this.x *= f; this.y *= f; return this; }; Vector2D.prototype.div = function(f) { this.x /= f; this.y /= f; return this; }; Vector2D.prototype.normalize = function() { let magnitude = this.length(); if (!magnitude) return this; return this.div(magnitude); }; /** * Rotate a radians anti-clockwise */ Vector2D.prototype.rotate = function(angle) { let sin = Math.sin(angle); let cos = Math.cos(angle); return this.set( this.x * cos + this.y * sin, -this.x * sin + this.y * cos); }; /** * Rotate radians anti-clockwise around the specified rotation center. */ Vector2D.prototype.rotateAround = function(angle, center) { return this.sub(center).rotate(angle).add(center); }; /** * Convert to integer coordinates. */ Vector2D.prototype.round = function() { return this.set(Math.round(this.x), Math.round(this.y)); }; Vector2D.prototype.floor = function() { return this.set(Math.floor(this.x), Math.floor(this.y)); }; Vector2D.prototype.toFixed = function(digits) { return this.set(this.x.toFixed(digits), this.y.toFixed(digits)); }; // Numeric 2D info functions (non-mutating) // // These methods serve to get numeric info on the vector, they don't modify the vector /** * Returns a vector that forms a right angle with this one. */ Vector2D.prototype.perpendicular = function() { return new Vector2D(-this.y, this.x); }; /** * Computes the scalar product of the two vectors. * Geometrically, this is the product of the length of the two vectors and the cosine of the angle between them. * If the vectors are orthogonal, the product is zero. */ Vector2D.prototype.dot = function(v) { return this.x * v.x + this.y * v.y; }; /** * Computes the non-zero coordinate of the cross product of the two vectors. * Geometrically, the cross of the vectors is a 3D vector perpendicular to the two 2D vectors. * The returned number corresponds to the area of the parallelogram with the vectors for sides. */ Vector2D.prototype.cross = function(v) { return this.x * v.y - this.y * v.x; }; Vector2D.prototype.lengthSquared = function() { return this.dot(this); }; Vector2D.prototype.length = function() { return Math.sqrt(this.lengthSquared()); }; /** * Compare this length to the length of v. * @return 0 if the lengths are equal * @return 1 if this is longer than v * @return -1 if this is shorter than v * @return NaN if the vectors aren't comparable */ Vector2D.prototype.compareLength = function(v) { return Math.sign(this.lengthSquared() - v.lengthSquared()); }; Vector2D.prototype.distanceToSquared = function(v) { return Math.euclidDistance2DSquared(this.x, this.y, v.x, v.y); }; Vector2D.prototype.distanceTo = function(v) { return Math.euclidDistance2D(this.x, this.y, v.x, v.y); }; /** * Returns the angle going from this position to v. * Angles are between -PI and PI. E.g., north is 0, east is PI/2. */ Vector2D.prototype.angleTo = function(v) { return Math.atan2(v.x - this.x, v.y - this.y); }; // Static 2D functions // // Static functions that return a new vector object. // Note that object creation is slow in JS, so use them only when necessary Vector2D.from3D = function(v) { return new Vector2D(v.x, v.z); }; Vector2D.add = function(v1, v2) { return new Vector2D(v1.x + v2.x, v1.y + v2.y); }; Vector2D.sub = function(v1, v2) { return new Vector2D(v1.x - v2.x, v1.y - v2.y); }; Vector2D.isEqualTo = function(v1, v2) { return v1.x == v2.x && v1.y == v2.y; }; Vector2D.mult = function(v, f) { return new Vector2D(v.x * f, v.y * f); }; Vector2D.div = function(v, f) { return new Vector2D(v.x / f, v.y / f); }; Vector2D.min = function(v1, v2) { return new Vector2D(Math.min(v1.x, v2.x), Math.min(v1.y, v2.y)); }; Vector2D.max = function(v1, v2) { return new Vector2D(Math.max(v1.x, v2.x), Math.max(v1.y, v2.y)); }; Vector2D.average = function(vectorList) { return Vector2D.sum(vectorList).div(vectorList.length); }; Vector2D.sum = function(vectorList) { // Do not use for...of nor array functions for performance let sum = new Vector2D(); for (let i = 0; i < vectorList.length; ++i) sum.add(vectorList[i]); return sum; }; +Vector2D.dot = function(v1, v2) +{ + return v1.x * v2.x + v1.y * v2.y; +}; + ///////////////////////////////////////////////////////////////////// // Vector3D // // Class for representing and manipulating 3D vectors // ///////////////////////////////////////////////////////////////////// function Vector3D(x = 0, y = 0, z = 0) { this.set(x, y, z); } Vector3D.prototype.clone = function() { return new Vector3D(this.x, this.y, this.z); }; // Mutating 3D functions // // These functions modify the current object, // and always return this object to allow chaining Vector3D.prototype.set = function(x, y, z) { this.x = x; this.y = y; this.z = z; return this; }; Vector3D.prototype.add = function(v) { this.x += v.x; this.y += v.y; this.z += v.z; return this; }; Vector3D.prototype.sub = function(v) { this.x -= v.x; this.y -= v.y; this.z -= v.z; return this; }; Vector3D.prototype.mult = function(f) { this.x *= f; this.y *= f; this.z *= f; return this; }; Vector3D.prototype.div = function(f) { this.x /= f; this.y /= f; this.z /= f; return this; }; Vector3D.prototype.normalize = function() { let magnitude = this.length(); if (!magnitude) return this; return this.div(magnitude); }; /** * Convert to integer coordinates. */ Vector3D.prototype.round = function() { return this.set(Math.round(this.x), Math.round(this.y), Math.round(this.z)); }; Vector3D.prototype.floor = function() { return this.set(Math.floor(this.x), Math.floor(this.y), Math.floor(this.z)); }; Vector3D.prototype.toFixed = function(digits) { return this.set(this.x.toFixed(digits), this.y.toFixed(digits), this.z.toFixed(digits)); }; // Numeric 3D info functions (non-mutating) // // These methods serve to get numeric info on the vector, they don't modify the vector Vector3D.prototype.dot = function(v) { return this.x * v.x + this.y * v.y + this.z * v.z; }; /** * Returns a vector perpendicular to the two given vectors. * The length of the returned vector corresponds to the area of the parallelogram with the vectors for sides. */ Vector3D.prototype.cross = function(v) { return new Vector3D( this.y * v.z - this.z * v.y, this.z * v.x - this.x * v.z, this.x * v.y - this.y * v.x); }; Vector3D.prototype.lengthSquared = function() { return this.dot(this); }; Vector3D.prototype.length = function() { return Math.sqrt(this.lengthSquared()); }; /** * Compare this length to the length of v, * @return 0 if the lengths are equal * @return 1 if this is longer than v * @return -1 if this is shorter than v * @return NaN if the vectors aren't comparable */ Vector3D.prototype.compareLength = function(v) { return Math.sign(this.lengthSquared() - v.lengthSquared()); }; Vector3D.prototype.distanceToSquared = function(v) { return Math.euclidDistance3DSquared(this.x, this.y, this.z, v.x, v.y, v.z); }; Vector3D.prototype.distanceTo = function(v) { return Math.euclidDistance3D(this.x, this.y, this.z, v.x, v.y, v.z); }; Vector3D.prototype.horizDistanceToSquared = function(v) { return Math.euclidDistance2DSquared(this.x, this.z, v.x, v.z); }; Vector3D.prototype.horizDistanceTo = function(v) { return Math.sqrt(this.horizDistanceToSquared(v)); }; /** * Returns the angle going from this position to v. */ Vector3D.prototype.horizAngleTo = function(v) { return Math.atan2(v.x - this.x, v.z - this.z); }; // Static 3D functions // // Static functions that return a new vector object. // Note that object creation is slow in JS, so use them only when really necessary Vector3D.add = function(v1, v2) { return new Vector3D(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); }; Vector3D.sub = function(v1, v2) { return new Vector3D(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); }; Vector3D.isEqualTo = function(v1, v2) { return v1.x == v2.x && v1.y == v2.y && v1.z == v2.z; }; Vector3D.mult = function(v, f) { return new Vector3D(v.x * f, v.y * f, v.z * f); }; Vector3D.div = function(v, f) { return new Vector3D(v.x / f, v.y / f, v.z / f); }; Index: ps/trunk/binaries/data/mods/public/gui/manual/intro.txt =================================================================== --- ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 23329) +++ ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 23330) @@ -1,157 +1,158 @@ [font="sans-bold-18"]0 A.D. in-game manual[font="sans-14"] Thank you for installing 0 A.D.! This page will give a brief overview of the features available in this incomplete, under-development, alpha version of the game. [font="sans-bold-16"]Graphics settings[font="sans-14"] You can switch between fullscreen and windowed mode by pressing Alt + Enter. In windowed mode, you can resize the window. If the game runs too slowly, you can change some settings in the options window: try disabling the "HQ Water Effects" and "Shadows" options. [font="sans-bold-16"]Playing the game[font="sans-14"] The controls and gameplay should be familiar to players of traditional real-time strategy (RTS) games. There are currently a lot of missing features and poorly-balanced stats – you will probably have to wait until a beta release for it to work well. Basic controls: • Left-click to select units • Left-click-and-drag to select groups of units • Right-click to order units to the target • Arrow keys or WASD keys to move the camera • Ctrl + arrow keys, or shift + mouse wheel, to rotate the camera • Mouse wheel, or "="/"+" and "-"/"−" keys, to zoom [font="sans-bold-16"]Modes[font="sans-14"] The main menu gives access to two game modes: • [font="sans-bold-14"]Single-player[font="sans-14"] – play a sandbox game or against one or more computer players (AI). The default AI (Petra) is under development and may not always be up to date on the new features, but you can play the game with or against it nonetheless. • [font="sans-bold-14"]Multiplayer[font="sans-14"] – play against human opponents over the internet. To set up a multiplayer game, one player must select the "Host game" option. The game uses UDP port 20595, so the host must configure their NAT/firewall/etc to allow this. Other players can then select "Join game" and enter the host's IP address. [font="sans-bold-16"]Game setup[font="sans-14"] In a multiplayer game, only the host can alter the game setup options. First, select what type of map you want to play: • [font="sans-bold-14"]Random map[font="sans-14"] – A map created automatically from a script • [font="sans-bold-14"]Scenario map[font="sans-14"] – A map created by map designers and with fixed civilizations • [font="sans-bold-14"]Skirmish map[font="sans-14"] – A map created by map designers but where the civilizations can be chosen Then the maps can be further filtered. The default maps are generally playable maps. Naval maps are maps where not all opponents can be reached over land and demo maps are maps used for testing purposes (generally not useful to play on). The "All Maps" filter shows all maps together in one list. Finally change the settings. For random maps this includes the number of players, the size of a map, etc. For scenarios you can only select who controls which player (decides where you start on the map etc). The options are either a human player, an AI or no player at all (the opponent will just be idle). When you are ready to start, click the "Start game" button. [font="sans-bold-16"]Hotkeys:[font="sans-14"] [font="sans-bold-14"]Program-wide[font="sans-14"] Alt + F4 – Immediately close the game, without asking for confirmation Alt + Enter – Toggle between fullscreen and windowed ~ or F9 – Show/hide console Alt + F – Show/hide frame counter (FPS) F11 – Enable/disable real-time profiler (toggles through the displays of information) Shift + F11 – Save current profiler data to "logs/profile.txt" F2 – Take screenshot (in .png format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there) Shift + F2 – Take huge screenshot (6400×4800 pixels, in .bmp format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there) Tab, Alt + S – Switch to the next tab Shift + Tab, Alt + W – Switch to the previous tab [font="sans-bold-14"]In Game[font="sans-14"] Double Left Click \[on unit] – Select all of your units of the same kind on the screen (even if they're different ranks) Triple Left Click \[on unit] – Select all of your units of the same kind and the same rank on the screen Alt + Double Left Click \[on unit] – Select all your units of the same kind on the entire map (even if they have different ranks) Alt + Triple Left Click \[on unit] – Select all your units of the same kind and rank on the entire map Shift + F5 – Quicksave Shift + F8 – Quickload F10 – Open/close menu F12 – Show/hide time elapsed counter Esc – Close all dialogs (chat, menu) or clear selected units Enter/Return – Open/send chat T – Send team chat L – Chat with the previously selected private chat partner Pause – Pause/resume the game Delete – Delete currently selected unit(s)/building(s), ask for confirmation Shift + Delete – Immediately delete currently selected unit(s)/building(s), without asking for confirmation / (Slash) – Select idle fighter Shift + / (Slash) – Add idle fighter to selection Alt + / (Slash) – Select all idle fighters . (Period) – Select idle worker (including citizen-soldiers) Shift + . (Period) – Add idle worker to selection (including citizen-soldiers) Alt + . (Period) – Select all idle workers (including citizen-soldiers) \\ (Backslash) – Select idle unit Shift + \\ (Backslash) – Add idle unit to selection Alt + \\ (Backslash) – Select all idle units H – Stop (halt) the currently selected units Y – The unit will go back to work U – Unload the garrisoned units of the selected buildings Ctrl + 1 (and so on up to Ctrl + 0) – Create control group 1 (to 0) from the selected unit(s)/building(s) 1 (and so on up to 0) – Select the units/buildings in control group 1 (to 0) Shift + 1 (to 0) – Add control group 1 (to 0) to the selected units/buildings Ctrl + F5 (and so on up to F8) – Mark the current camera position, for jumping back to later F5, F6, F7, and F8 – Move the camera to a marked position. Jump back to the last location if the camera is already over the marked position Z, X, C, V, B, N, M – With training buildings selected. Add the 1st, 2nd, … unit shown to the training queue for all the selected buildings PageUp with units selected – Highlights the units/buildings guarded by the selection PageDown with units/buildings selected – Highlights the units guarding the selection Tab – See all status bars (which would also show the building progress) Ctrl + Tab – Toggle summary window Alt + L – Show the multiplayer lobby in a dialog window Ctrl + H – Toggle in-game diplomacy panel Ctrl + B – Toggle in-game barter and trade panel Ctrl + O – Toggle in-game objectives panel Ctrl + P – Toggle in-game tutorial panel Alt + Shift + T – Toggle structure tree panel Alt + Shift + H – Toggle civilization info panel [font="sans-bold-14"]Modify mouse action[font="sans-14"] Ctrl + Right Click on building – Garrison J + Right Click on building – Repair P + Right Click – Patrol Shift + Right Click – Queue the move/build/gather/etc order Alt + Right Click – Order one unit from the current selection to move/build/gather/etc and unselect it. Used to quickly dispatch units with specific tasks Shift + Left Click when training units – Add units in batches (the batch size is 5 by default and can be changed in the options) Shift + Left Click or Left Drag over unit on map – Add unit to selection Ctrl + Left Click or Left Drag over unit on map – Remove unit from selection Alt + Left Drag over units on map – Only select military units Alt + Y + Left Drag over units on map – Only select non-military units I + Left Drag over units on map – Only select idle units O + Left Drag over units on map – Only select wounded units Ctrl + Left Click on unit/group icon with multiple units selected – Deselect Right Click with a building(s) selected – sets a rally point for units created/ungarrisoned from that building Ctrl + Right Click with unit(s) selected: • If the cursor is over an own or allied structure – Garrison • If the cursor is over an enemy unit or building – Attack (instead of capture or gather) • Otherwise – Attack move (by default all enemy units and structures along the way are targeted) Ctrl + Q + Right Click with unit(s) selected – Attack move, only units along the way are targeted + Ctrl + Mouse Move near structures – Align the new structure with an existing nearby structure [font="sans-bold-14"]Overlays[font="sans-14"] Alt + G – Hide/show the GUI Alt + D – Show/hide developer overlay (with developer options) Alt + Shift + W – Toggle wireframe mode (press once to get wireframes overlaid over the textured models, twice to get just the wireframes colored by the textures, thrice to get back to normal textured mode) Alt + Shift + S – Toggle unit silhouettes (might give a small performance boost) Alt + Z – Toggle sky Alt + X – Toggle diplomacy colors Alt + C – Toggle attack range visualizations of selected units and structures Alt + V – Toggle aura range visualizations of selected units and structures Alt + B – Toggle heal range visualizations of selected units [font="sans-bold-14"]Camera manipulation[font="sans-14"] W or ↑ (Up) – Pan screen up S or ↓ (Down) – Pan screen down A or ← (Left) – Pan screen left D or → (Right) – Pan screen right Ctrl + W or Ctrl + ↑ (Up) – Rotate camera to look upward Ctrl + S or Ctrl + ↓ (Down) – Rotate camera to look downward Ctrl + A or Ctrl + ← (Left) – Rotate camera clockwise around terrain Ctrl + D or Ctrl + → (Right) – Rotate camera counter-clockwise around terrain Q – Rotate camera clockwise around terrain E – Rotate camera counter-clockwise around terrain Shift + Mouse Wheel Rotate Up – Rotate camera clockwise around terrain Shift + Mouse Wheel Rotate Down – Rotate camera counter-clockwise around terrain F – Follow the selected unit (move the camera to stop following the unit) R – Reset camera zoom/orientation = (Equals) or + (Plus) – Zoom in (keep pressed for continuous zoom) - (Hyphen) or − (Minus) – Zoom out (keep pressed for continuous zoom) Middle Mouse Button – Keep pressed and move the mouse to pan [font="sans-bold-14"]During Building Placement[font="sans-14"] \[ (Left Bracket) – Rotate building 15 degrees counter-clockwise ] (Right Bracket) – Rotate building 15 degrees clockwise Left Drag – Rotate building using mouse (foundation will be placed on mouse release) [font="sans-bold-14"]When loading a saved game[font="sans-14"] Esc – Cancel Delete – Delete the selected saved game, ask for confirmation Shift + Delete – Immediately delete the selected saved game, without asking for confirmation Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 23329) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 23330) @@ -1,1629 +1,1655 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; const ACTION_GUARD = 3; const ACTION_PATROL = 4; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; const INPUT_SELECTING = 1; const INPUT_BANDBOXING = 2; const INPUT_BUILDING_PLACEMENT = 3; const INPUT_BUILDING_CLICK = 4; const INPUT_BUILDING_DRAG = 5; const INPUT_BATCHTRAINING = 6; const INPUT_PRESELECTEDACTION = 7; const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; const INPUT_UNIT_POSITION_START = 10; const INPUT_UNIT_POSITION = 11; var inputState = INPUT_NORMAL; const INVALID_ENTITY = 0; var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; /** * Containing the ingame position which span the line. */ var g_FreehandSelection_InputLine = []; /** * Minimum squared distance when a mouse move is called a drag. */ const g_FreehandSelection_ResolutionInputLineSquared = 1; /** * Minimum length a dragged line should have to use the freehand selection. */ const g_FreehandSelection_MinLengthOfLine = 8; /** * To start the freehandSelection function you need a minimum number of units. * Minimum must be 2, for better performance you could set it higher. */ const g_FreehandSelection_MinNumberOfUnits = 2; /** * Number of pixels the mouse can move before the action is considered a drag. */ const g_MaxDragDelta = 4; /** * Used for remembering mouse coordinates at start of drag operations. */ var g_DragStart; /** * Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. * If any mousedown or mouseup of a sequence of clicks lands on a unit, * that unit will be selected, which makes it easier to click on moving units. */ var clickedEntity = INVALID_ENTITY; // Same double-click behaviour for hotkey presses const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { var cursorSet = false; var tooltipSet = false; var informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION)) { let action = determineAction(mouseX, mouseY); if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); cursorSet = true; } if (action.tooltip) { tooltipSet = true; informationTooltip.caption = action.tooltip; informationTooltip.hidden = false; } } } if (!cursorSet) Engine.ResetCursor(); if (!tooltipSet) informationTooltip.hidden = true; var placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); if (placementSupport.tooltipMessage) placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip"; placementTooltip.caption = placementSupport.tooltipMessage || ""; placementTooltip.hidden = !placementSupport.tooltipMessage; } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to. // See onSimulationUpdate in session.js. if (placementSupport.mode === "building") { if (placementSupport.template && placementSupport.position) { var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); // Show placement info tooltip if invalid position placementSupport.tooltipError = !result.success; placementSupport.tooltipMessage = ""; if (!result.success) { if (result.message && result.parameters) { var message = result.message; if (result.translateMessage) if (result.pluralMessage) message = translatePlural(result.message, result.pluralMessage, result.pluralCount); else message = translate(message); var parameters = result.parameters; if (result.translateParameters) translateObjectKeys(parameters, result.translateParameters); placementSupport.tooltipMessage = sprintf(message, parameters); } return false; } if (placementSupport.attack && placementSupport.attack.Ranged) { // building can be placed here, and has an attack // show the range advantage in the tooltip var cmd = { "x": placementSupport.position.x, "z": placementSupport.position.z, "range": placementSupport.attack.Ranged.maxRange, "elevationBonus": placementSupport.attack.Ranged.elevationBonus, }; var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); var range = Math.round(cmd.range); placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); } return true; } } else if (placementSupport.mode === "wall") { if (placementSupport.wallSet && placementSupport.position) { // Fetch an updated list of snapping candidate entities placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities( placementSupport.wallSet.templates.tower, placementSupport.wallSnapEntitiesIncludeOffscreen, true, // require exact template match true // include foundations ); return Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": placementSupport.wallSet, "start": placementSupport.position, "end": placementSupport.wallEndPosition, "snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment }); } } return false; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMinimap) { var selection = g_Selection.toList(); // No action if there's no selection if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return undefined; // If the selection isn't friendly units, no action if (!selection.every(ownsEntity) && !(g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll)) return undefined; var target = undefined; if (!fromMinimap) { var ent = Engine.PickEntityAtPoint(x, y); if (ent != INVALID_ENTITY) target = ent; } // decide between the following ordered actions // if two actions are possible, the first one is taken // so the most specific should appear first var actions = Object.keys(g_UnitActions).slice(); actions.sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness); var actionInfo = undefined; if (preSelectedAction != ACTION_NONE) { for (var action of actions) if (g_UnitActions[action].preSelectedActionCheck) { var r = g_UnitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } for (var action of actions) if (g_UnitActions[action].hotkeyActionCheck) { var r = g_UnitActions[action].hotkeyActionCheck(target, selection); if (r) return r; } for (var action of actions) if (g_UnitActions[action].actionCheck) { var r = g_UnitActions[action].actionCheck(target, selection); if (r) return r; } return { "type": "none", "cursor": "", "target": target }; } function ownsEntity(ent) { let entState = GetEntityState(ent); return entState && entState.player == g_ViewedPlayer; } function tryPlaceBuilding(queued) { if (placementSupport.mode !== "building") { error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'"); return false; } if (!updateBuildingPlacementPreview()) { // invalid location - don't build it // TODO: play a sound? return false; } var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); if (!queued) placementSupport.Reset(); else placementSupport.RandomizeActorSeed(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'"); return false; } var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) { error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo)); return false; } if (!wallPlacementInfo) return false; var selection = g_Selection.toList(); var cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, }; // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed // (this is somewhat non-ideal and hardcode-ish) var hasWallSegment = false; for (let piece of cmd.pieces) { if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( { hasWallSegment = true; break; } } if (hasWallSegment) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); } return true; } /** * Updates the bandbox object with new positions and visibility. * @returns {array} The coordinates of the vertices of the bandbox. */ function updateBandbox(bandbox, ev, hidden) { let scale = +Engine.ConfigDB_GetValue("user", "gui.scale"); let vMin = Vector2D.min(g_DragStart, ev); let vMax = Vector2D.max(g_DragStart, ev); bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale); bandbox.hidden = hidden; return [vMin.x, vMin.y, vMax.x, vMax.y]; } // Define some useful unit filters for getPreferredEntities var unitFilters = { "isUnit": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Unit"); }, "isDefensive": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Defensive"); }, "isMilitary": entity => { var entState = GetEntityState(entity); return entState && g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isNonMilitary": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && !g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isIdle": entity => { var entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.unitAI && entState.unitAI.isIdle && !hasClass(entState, "Domestic"); }, "isWounded": entity => { let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && entState.maxHitpoints && 100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold"); }, "isAnything": entity => { return true; } }; // Choose, inside a list of entities, which ones will be selected. // We may use several entity filters, until one returns at least one element. function getPreferredEntities(ents) { // Default filters var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; // Handle hotkeys if (Engine.HotkeyIsPressed("selection.militaryonly")) filters = [unitFilters.isMilitary]; if (Engine.HotkeyIsPressed("selection.nonmilitaryonly")) filters = [unitFilters.isNonMilitary]; if (Engine.HotkeyIsPressed("selection.idleonly")) filters = [unitFilters.isIdle]; if (Engine.HotkeyIsPressed("selection.woundedonly")) filters = [unitFilters.isWounded]; var preferredEnts = []; for (var i = 0; i < filters.length; ++i) { preferredEnts = ents.filter(filters[i]); if (preferredEnts.length) break; } return preferredEnts; } function handleInputBeforeGui(ev, hoveredObject) { if (GetSimState().cinemaPlaying) return false; // Capture mouse position so we can use it for displaying cursors, // and key states switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } // Remember whether the mouse is over a GUI object or not mouseIsOverObject = (hoveredObject != null); // Close the menu when interacting with the game world if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) g_Menu.close(); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: var bandbox = Engine.GetGUIObjectByName("bandbox"); switch (ev.type) { case "mousemotion": var rect = updateBandbox(bandbox, ev, false); var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); var preferredEntities = getPreferredEntities(ents); g_Selection.setHighlightList(preferredEntities); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var rect = updateBandbox(bandbox, ev, true); // Get list of entities limited to preferred entities var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); // Remove the bandbox hover highlighting g_Selection.setHighlightList([]); // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION: switch (ev.type) { case "mousemotion": return positionUnitsFreehandSelectionMouseMove(ev); case "mousebuttonup": return positionUnitsFreehandSelectionMouseUp(ev); } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_CLICK: // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. switch (ev.type) { case "mousebuttonup": if (ev.button === SDL_BUTTON_LEFT) { inputState = INPUT_BUILDING_WALL_PATHING; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_PATHING: // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the // user to continue building walls. switch (ev.type) { case "mousemotion": placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!) // the ending point and the starting point to snap to. // // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers // in them. Might be useful to query only for entities within a certain range around the starting point and ending // points. placementSupport.wallSnapEntitiesIncludeOffscreen = true; var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost }); placementSupport.tooltipMessage = [ getEntityCostTooltip(result), getNeededResourcesTooltip(neededResources) ].filter(tip => tip).join("\n"); } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceWall(queued)) { if (queued) { // continue building, just set a new starting position where we left off placementSupport.position = placementSupport.wallEndPosition; placementSupport.wallEndPosition = undefined; inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.Reset(); inputState = INPUT_NORMAL; } } else placementSupport.tooltipMessage = translate("Cannot build wall here!"); updateBuildingPlacementPreview(); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // reset to normal input mode placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_DRAG: switch (ev.type) { case "mousemotion": let maxDragDelta = 16; if (g_DragStart.distanceTo(ev) >= maxDragDelta) { // Rotate in the direction of the mouse placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); } else { // If the mouse is near the center, snap back to the default orientation placementSupport.SetDefaultAngle(); } - var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { - "template": placementSupport.template, - "x": placementSupport.position.x, - "z": placementSupport.position.z - }); + let snapToEdges = Engine.HotkeyIsPressed("session.snaptoedges"); + let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { + "template": placementSupport.template, + "x": placementSupport.position.x, + "z": placementSupport.position.z, + "angle": placementSupport.angle, + "snapToEdges": snapToEdges && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( + placementSupport.position.x, placementSupport.position.z) + }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } updateBuildingPlacementPreview(); break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BATCHTRAINING: if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") { flushTrainingBatch(); inputState = INPUT_NORMAL; } break; } return false; } function handleInputAfterGui(ev) { if (GetSimState().cinemaPlaying) return false; if (ev.hotkey === undefined) ev.hotkey = null; if (ev.hotkey == "session.highlightguarding") { g_ShowGuarding = (ev.type == "hotkeydown"); updateAdditionalHighlight(); } else if (ev.hotkey == "session.highlightguarded") { g_ShowGuarded = (ev.type == "hotkeydown"); updateAdditionalHighlight(); } if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) clickedEntity = INVALID_ENTITY; // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_SELECTING; // If a single click occured, reset the clickedEntity. // Also set it if we're double/triple clicking and missed the unit earlier. if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { if (!controlsPlayer(g_ViewedPlayer)) break; g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_UNIT_POSITION_START; } break; case "hotkeydown": if (ev.hotkey.indexOf("selection.group.") == 0) { let now = Date.now(); if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { var sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { var sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now; prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { var action = determineAction(ev.x, ev.y); if (!action) break; if (!Engine.HotkeyIsPressed("session.queue")) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; } return doAction(action, ev); } else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } // else default: // Slight hack: If selection is empty, reset the input state if (g_Selection.toList().length == 0) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to bandbox mode if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta) { inputState = INPUT_BANDBOXING; return false; } var ent = Engine.PickEntityAtPoint(ev.x, ev.y); if (ent != INVALID_ENTITY) g_Selection.setHighlightList([ent]); else g_Selection.setHighlightList([]); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { if (clickedEntity == INVALID_ENTITY) clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); // Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event. if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity)) { clickedEntity = INVALID_ENTITY; if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { g_Selection.reset(); resetIdleUnit(); } inputState = INPUT_NORMAL; return true; } // If camera following and we select different unit, stop if (Engine.GetFollowedEntity() != clickedEntity) Engine.CameraFollow(0); var ents = []; if (ev.clicks == 1) ents = [clickedEntity]; else { // Double click or triple click has occurred var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); var matchRank = true; var templateToMatch; // Check for double click or triple click if (ev.clicks == 2) { // Select similar units regardless of rank templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName; if (templateToMatch) matchRank = false; else // No selection group name defined, so fall back to exact match templateToMatch = GetEntityState(clickedEntity).template; } else // Triple click // Select units matching exact template name (same rank) templateToMatch = GetEntityState(clickedEntity).template; // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); } // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) g_Selection.addList(ents); else if (Engine.HotkeyIsPressed("selection.remove")) g_Selection.removeList(ents); else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_UNIT_POSITION_START: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to unit position mode if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta)) { inputState = INPUT_UNIT_POSITION; return false; } break; case "mousebuttonup": inputState = INPUT_NORMAL; if (ev.button == SDL_BUTTON_RIGHT) { let action = determineAction(ev.x, ev.y); if (action) return doAction(action, ev); } break; } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (placementSupport.mode === "wall") { // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities // itself happens in the call to updateBuildingPlacementPreview below). placementSupport.wallSnapEntitiesIncludeOffscreen = false; } else { // cancel if not enough resources if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost })) { placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } - var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { - "template": placementSupport.template, - "x": placementSupport.position.x, - "z": placementSupport.position.z, - }); + let snapToEdges = Engine.HotkeyIsPressed("session.snaptoedges"); + let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { + "template": placementSupport.template, + "x": placementSupport.position.x, + "z": placementSupport.position.z, + "snapToEdges": snapToEdges && Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( + placementSupport.position.x, placementSupport.position.z) + }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } updateBuildingPlacementPreview(); // includes an update of the snap entity candidates return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { if (placementSupport.mode === "wall") { var validPlacement = updateBuildingPlacementPreview(); if (validPlacement !== false) inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); + + let snapToEdges = Engine.HotkeyIsPressed("session.snaptoedges"); + if (snapToEdges) + { + let snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { + "template": placementSupport.template, + "x": placementSupport.position.x, + "z": placementSupport.position.z, + "snapToEdges": Engine.GetEdgesOfStaticObstructionsOnScreenNearTo( + placementSupport.position.x, placementSupport.position.z) + }); + if (snapData) + { + placementSupport.angle = snapData.angle; + placementSupport.position.x = snapData.x; + placementSupport.position.z = snapData.z; + } + } + g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_BUILDING_CLICK; } return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; case "hotkeydown": var rotation_step = Math.PI / 12; // 24 clicks make a full rotation switch (ev.hotkey) { case "session.rotate.cw": placementSupport.angle += rotation_step; updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementSupport.angle -= rotation_step; updateBuildingPlacementPreview(); break; } break; } break; } return false; } function doAction(action, ev) { if (!controlsPlayer(g_ViewedPlayer)) return false; return handleUnitAction(Engine.GetTerrainAtScreenPoint(ev.x, ev.y), action); } function positionUnitsFreehandSelectionMouseMove(ev) { // Converting the input line into a List of points. // For better performance the points must have a minimum distance to each other. let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); if (!g_FreehandSelection_InputLine.length || target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= g_FreehandSelection_ResolutionInputLineSquared) g_FreehandSelection_InputLine.push(target); return false; } function positionUnitsFreehandSelectionMouseUp(ev) { inputState = INPUT_NORMAL; let inputLine = g_FreehandSelection_InputLine; g_FreehandSelection_InputLine = []; if (ev.button != SDL_BUTTON_RIGHT) return true; let lengthOfLine = 0; for (let i = 1; i < inputLine.length; ++i) lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); let selection = g_Selection.toList().filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); // Checking the line for a minimum length to save performance. if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) { let action = determineAction(ev.x, ev.y); return !!action && doAction(action, ev); } // Even distribution of the units on the line. let p0 = inputLine[0]; let entityDistribution = [p0]; let distanceBetweenEnts = lengthOfLine / (selection.length - 1); let freeDist = -distanceBetweenEnts; for (let i = 1; i < inputLine.length; ++i) { let p1 = inputLine[i]; freeDist += inputLine[i - 1].distanceTo(p1); while (freeDist >= 0) { p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); entityDistribution.push(p0); freeDist -= distanceBetweenEnts; } } // Rounding errors can lead to missing or too many points. entityDistribution = entityDistribution.slice(0, selection.length); entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1])); if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) > Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) + Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0])) entityDistribution.reverse(); Engine.PostNetworkCommand({ "type": Engine.HotkeyIsPressed("session.attackmove") ? "attack-walk-custom" : "walk-custom", "entities": selection, "targetPositions": entityDistribution.map(pos => pos.toFixed(2)), "targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, "queued": Engine.HotkeyIsPressed("session.queue") }); // Add target markers with a minimum distance of 5 to each other. let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts); for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker) DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; } function handleUnitAction(target, action) { if (!g_UnitActions[action.type] || !g_UnitActions[action.type].execute) { error("Invalid action.type " + action.type); return false; } let selection = g_Selection.toList(); if (Engine.HotkeyIsPressed("session.orderone")) { // Pick the first unit that can do this order. let unit = selection.find(entity => ["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method => g_UnitActions[action.type][method] && g_UnitActions[action.type][method](action.target || undefined, [entity]) )); if (unit) { selection = [unit]; g_Selection.removeList(selection); } } // If the session.queue hotkey is down, add the order to the unit's order queue instead // of running it immediately return g_UnitActions[action.type].execute(target, action, selection, Engine.HotkeyIsPressed("session.queue")); } function getEntityLimitAndCount(playerState, entType) { let ret = { "entLimit": undefined, "entCount": undefined, "entLimitChangers": undefined, "canBeAddedCount": undefined }; if (!playerState.entityLimits) return ret; let template = GetTemplateData(entType); let entCategory = template.trainingRestrictions && template.trainingRestrictions.category || template.buildRestrictions && template.buildRestrictions.category; if (entCategory && playerState.entityLimits[entCategory] !== undefined) { ret.entLimit = playerState.entityLimits[entCategory] || 0; ret.entCount = playerState.entityCounts[entCategory] || 0; ret.entLimitChangers = playerState.entityLimitChangers[entCategory]; ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0); } return ret; } // Called by GUI when user clicks construction button // @param buildTemplate Template name of the entity the user wants to build function startBuildingPlacement(buildTemplate, playerState) { if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) return; // TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI // to start building a structure, then the highlight selection rings are kept during the construction of the building. // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing. placementSupport.Reset(); // find out if we're building a wall, and change the entity appropriately if so var templateData = GetTemplateData(buildTemplate); if (templateData.wallSet) { placementSupport.mode = "wall"; placementSupport.wallSet = templateData.wallSet; inputState = INPUT_BUILDING_PLACEMENT; } else { placementSupport.mode = "building"; placementSupport.template = buildTemplate; inputState = INPUT_BUILDING_PLACEMENT; } if (templateData.attack && templateData.attack.Ranged && templateData.attack.Ranged.maxRange) { // add attack information to display a good tooltip placementSupport.attack = templateData.attack; } } // Batch training: // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING // When the user releases shift, or clicks on a different training button, we create the batched units var g_BatchTrainingEntities; var g_BatchTrainingType; var g_NumberOfBatches; var g_BatchTrainingEntityAllowedCount; var g_BatchSize = getDefaultBatchTrainingSize(); function OnTrainMouseWheel(dir) { if (!Engine.HotkeyIsPressed("session.batchtrain")) return; g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio"); if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize)) g_BatchSize = 1; updateSelectionDetails(); } function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) { return entitiesToCheck.filter(entity => { let state = GetEntityState(entity); return state && state.production && state.production.entities.length && state.production.entities.indexOf(trainEntType) != -1; }); } function initBatchTrain() { registerConfigChangeHandler(changes => { if (changes.has("gui.session.batchtrainingsize")) updateDefaultBatchSize(); }); } function getDefaultBatchTrainingSize() { let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize"); return Number.isInteger(num) && num > 0 ? num : 5; } function getBatchTrainingSize() { return Math.max(Math.round(g_BatchSize), 1); } function updateDefaultBatchSize() { g_BatchSize = getDefaultBatchTrainingSize(); } // Add the unit shown at position to the training queue for all entities in the selection function addTrainingByPosition(position) { let playerState = GetSimState().players[Engine.GetPlayerID()]; let selection = g_Selection.toList(); if (!playerState || !selection.length) return; let trainableEnts = getAllTrainableEntitiesFromSelection(); let entToTrain = trainableEnts[position]; // When we have no building to train or the position is invalid if (!entToTrain) return; addTrainingToQueue(selection, entToTrain, playerState); return; } // Called by GUI when user clicks training button function addTrainingToQueue(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; let decrement = Engine.HotkeyIsPressed("selection.remove"); let template; if (!decrement) template = GetTemplateData(trainEntType); // Batch training only possible if we can train at least 2 units if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) { if (inputState == INPUT_BATCHTRAINING) { // Check if we are training in the same building(s) as the last batch // NOTE: We just check if the arrays are the same and if the order is the same // If the order changed, we have a new selection and we should create a new batch. // If we're already creating a batch of this unit (in the same building(s)), then just extend it // (if training limits allow) if (g_BatchTrainingEntities.length == selection.length && g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && g_BatchTrainingType == trainEntType) { if (decrement) { --g_NumberOfBatches; if (g_NumberOfBatches <= 0) inputState = INPUT_NORMAL; } else if (canBeAddedCount == undefined || canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) { if (Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) })) return; ++g_NumberOfBatches; } g_BatchTrainingEntityAllowedCount = canBeAddedCount; return; } // Otherwise start a new one else if (!decrement) flushTrainingBatch(); // fall through to create the new batch } // Don't start a new batch if decrementing or unable to afford it. if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, getBatchTrainingSize()) })) return; inputState = INPUT_BATCHTRAINING; g_BatchTrainingEntities = selection; g_BatchTrainingType = trainEntType; g_BatchTrainingEntityAllowedCount = canBeAddedCount; g_NumberOfBatches = 1; } else { // Non-batched - just create a single entity in each building // (but no more than entity limit allows) let buildingsForTraining = appropriateBuildings; if (canBeAddedCount !== undefined) buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); Engine.PostNetworkCommand({ "type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining }); } } /** * Returns the number of units that will be present in a batch if the user clicks * the training button depending on the batch training modifier hotkey */ function getTrainingStatus(selection, trainEntType, playerState) { let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); let nextBatchTrainingCount = 0; let canBeAddedCount; if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType) { nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize(); canBeAddedCount = g_BatchTrainingEntityAllowedCount; } else canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; // We need to calculate count after the next increment if it's possible if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && Engine.HotkeyIsPressed("session.batchtrain")) nextBatchTrainingCount += getBatchTrainingSize(); nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); // If training limits don't allow us to train batchTrainingCount in each appropriate building // train as many full batches as we can and remainer in one more building. let buildingsCountToTrainFullBatch = appropriateBuildings.length; let remainderToTrain = 0; if (canBeAddedCount !== undefined && canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) { buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); remainderToTrain = canBeAddedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; } function flushTrainingBatch() { let batchedSize = g_NumberOfBatches * getBatchTrainingSize(); let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); // If training limits don't allow us to train batchedSize in each appropriate building if (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 building let remainer = g_BatchTrainingEntityAllowedCount % batchedSize; if (remainer) Engine.PostNetworkCommand({ "type": "train", "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], "template": g_BatchTrainingType, "count": remainer }); } else Engine.PostNetworkCommand({ "type": "train", "entities": appropriateBuildings, "template": g_BatchTrainingType, "count": batchedSize }); } function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": var toSelect = []; g_Groups.update(); for (var ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) { let entState = GetEntityState(toSelect[0]); let position = entState.position; if (position && entState.visibility != "hidden") Engine.CameraMoveTo(position.x, position.z); } break; case "save": case "breakUp": g_Groups.groups[groupId].reset(); if (action == "save") g_Groups.addEntities(groupId, g_Selection.toList()); updateGroups(); break; } } var lastIdleUnit = 0; var currIdleClassIndex = 0; var lastIdleClasses = []; function resetIdleUnit() { lastIdleUnit = 0; currIdleClassIndex = 0; lastIdleClasses = []; } function findIdleUnit(classes) { var append = Engine.HotkeyIsPressed("selection.add"); var selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i])) resetIdleUnit(); lastIdleClasses = classes; var data = { "viewedPlayer": g_ViewedPlayer, "excludeUnits": append ? g_Selection.toList() : [], // If the current idle class index is not 0, put the class at that index first. "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) }; if (!selectall) { data.limit = 1; data.prevUnit = lastIdleUnit; } var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (!idleUnits.length) { // TODO: display a message or play a sound to indicate no more idle units, or something // Reset for next cycle resetIdleUnit(); return; } if (!append) g_Selection.reset(); g_Selection.addList(idleUnits); if (selectall) return; lastIdleUnit = idleUnits[0]; var entityState = GetEntityState(lastIdleUnit); var position = entityState.position; if (position) Engine.CameraMoveTo(position.x, position.z); // Move the idle class index to the first class an idle unit was found for. var indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; } function clearSelection() { if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING) { inputState = INPUT_NORMAL; placementSupport.Reset(); } else g_Selection.reset(); preSelectedAction = ACTION_NONE; } Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 23329) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 23330) @@ -1,2009 +1,2017 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised 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.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 numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits); // Work out what 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"; } // store player ally/neutral/enemy data as arrays 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(), "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, "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": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i) }); } 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(); // Add timeElapsed let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); // Add ceasefire info 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; } // Add cinema path info let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); // Add the game type and allied victory let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); // Add basic statistics to each player 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() { // Get basic simulation info let ret = this.GetSimulationState(); // Add statistics to each player 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 }; }; 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); // 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(), "visibleClasses": cmpIdentity.GetVisibleClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable() }; 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(), }; var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades": cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo() }; 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() }; 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(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable); 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(), }; 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].splash = cmpAttack.GetSplashDamage(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 (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) { // For units, take the range in front of it, no spread. So angle = 0 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); } else if(cmpPosition && cmpPosition.IsInWorld()) { // For buildings, take the average elevation around it. So angle = 2*pi ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI); } else { // not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } } let cmpArmour = Engine.QueryInterface(ent, IID_Resistance); if (cmpArmour) ret.armour = cmpArmour.GetArmourStrengths("Damage"); 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("BarterMarket")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "hp": cmpHeal.GetHP(), "range": cmpHeal.GetRange().max, "rate": cmpHeal.GetRate(), "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.GetTimer(), "rates": cmpResourceTrickle.GetRates() }; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier() }; 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.owner ? data.owner : 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) aurasTemplate[name] = AuraTemplates.Get(name); return GetTemplateDataHelper(template, owner, aurasTemplate); }; GuiInterface.prototype.IsTechnologyResearched = function(player, data) { if (!data.tech) return true; let cmpTechnologyManager = QueryPlayerIDInterface(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 || 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) { return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks(); }; // Used to show a red square over GUI elements you can't yet afford. GuiInterface.prototype.GetNeededResources = function(player, data) { return QueryPlayerIDInterface(data.player || player).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; }; GuiInterface.prototype.IsTemplateModified = function(player) { return this.templateModified[player] || false; }; GuiInterface.prototype.ResetTemplateModified = function() { this.templateModified = {}; }; /** * 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) { return QueryPlayerIDInterface(wantedPlayer).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) { let playerColors = {}; // cache of owner -> color map 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 pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set if (pos) { // Only update the position if we changed it (cmd.queued is set) if ("queued" in cmd) if (cmd.queued == true) cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z else cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z // rebuild the renderer when not set (when reading saved game or in case of building update) else if (!cmpRallyPointRenderer.IsSet()) for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': 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 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": [], }; // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; // Move the preview into the right location 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); // Check whether building placement is valid 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': ..., * 'populationBonus': ..., * } * } * * @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; let start = { "pos": cmd.start, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; let end = { "pos": cmd.end, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; // -------------------------------------------------------------------------------- // 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; } // Move all existing cached entities outside of the world and reset their use count 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 }), }; // ensure that the loaded template data contains a wallPiece component 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) { let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error 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, "populationBonus": 0, "time": 0 }, }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js // 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 > 0 && 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 > 0 ? 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 > 0 ? 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; let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity 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) { // allocate new entity ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else // reuse an existing one 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); // check whether this wall piece can be validly positioned here 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", "populationBonus", "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.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. var 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() || cmpUnitAI.IsGarrisoned()) 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; return CalculateTraderGain(data.firstMarket, 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 { // Else both markets are not null and target is different from them 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) { return QueryPlayerIDInterface(player).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, "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, "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 }; 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 23329) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 23330) @@ -1,591 +1,592 @@ +Engine.LoadHelperScript("ObstructionSnap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Attack.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/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/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/Timer.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/Upgrade.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 } }; }, PlayerHasMarket: function () { return false; } }); 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; } }); 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 }; }, 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 {}; }, 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": {}}; } }); 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 }; }, 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 {}; }, 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": {}}; } }); 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 }, 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}, 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 }, 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}, 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 }); 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 }, "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}, "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 }, "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}, "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 }); 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"]; }, GetVisibleClassesList: function() { return ["class3", "class4"]; }, GetRank: function() { return "foo"; }, GetSelectionGroupName: function() { return "Selection Group Name"; }, HasClass: function() { return true; }, IsUndeletable: function() { return false; } }); 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, { "GetTimer": () => 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"], "visibleClasses": ["class3", "class4"], "selectionGroupName": "Selection Group Name", "canDelete": true }, "position": {x:1, y:2, z:3}, "hitpoints": 50, "maxHitpoints": 60, "needsRepair": false, "needsHeal": true, "builder": true, "canGarrison": false, "visibility": "visible", "isBarterMarket":true, "resourceTrickle": { "interval": 1250, "rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 } } }); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/ObstructionSnap.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/ObstructionSnap.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/ObstructionSnap.js (revision 23330) @@ -0,0 +1,154 @@ +/** + * The class allows the player to position structures so that they are aligned + * with nearby structures. + */ +class ObstructionSnap +{ + getValidEdges(allEdges, position, maxSide) + { + let edges = []; + let dir1 = new Vector2D(); + let dir2 = new Vector2D(); + for (let edge of allEdges) + { + let signedDistance = Vector2D.dot(edge.normal, position) - + Vector2D.dot(edge.normal, edge.begin); + // Negative signed distance means that the template position + // lays behind the edge. + if (signedDistance < -this.MinimalDistanceToSnap - maxSide || + signedDistance > this.MinimalDistanceToSnap + maxSide) + continue; + dir1.setFrom(edge.begin).sub(edge.end).normalize(); + dir2.setFrom(dir1).mult(-1); + let offsetDistance = Math.max( + Vector2D.dot(dir1, position) - Vector2D.dot(dir1, edge.begin), + Vector2D.dot(dir2, position) - Vector2D.dot(dir2, edge.end)); + if (offsetDistance > this.MinimalDistanceToSnap + maxSide) + continue; + // If a projection of the template position on the edge is + // lying inside the edge then obviously we don't need to + // account the offset distance. + if (offsetDistance < 0) + offsetDistance = 0; + edge.signedDistance = signedDistance; + edge.offsetDistance = offsetDistance; + edges.push(edge); + } + return edges; + } + + // We need a small padding to avoid unnecessary collisions + // because of loss of accuracy. + getPadding(edge) + { + const snapPadding = 0.05; + // We don't need to padding for edges with normals directed inside + // its entity, as we try to snap from an internal side of the edge. + return edge.order == "ccw" ? 0 : snapPadding; + } + + // Pick a base edge, it will be the first axis and fix the angle. + // We can't just pick an edge by signed distance, because we might have + // a case when one segment is closer by signed distance than another + // one but much farther by actual (euclid) distance. + compareEdges(a, b) + { + const behindA = a.signedDistance < -this.EPS; + const behindB = b.signedDistance < -this.EPS; + const scoreA = Math.abs(a.signedDistance) + a.offsetDistance; + const scoreB = Math.abs(b.signedDistance) + b.offsetDistance; + if (Math.abs(scoreA - scoreB) < this.EPS) + { + if (behindA != behindB) + return behindA - behindB; + if (!behindA) + return a.offsetDistance - b.offsetDistance; + return -a.signedDistance - -b.signedDistance; + } + return scoreA - scoreB; + } + + getPosition(data, template) + { + if (!data.snapToEdges || !template.Obstruction || !template.Obstruction.Static) + return undefined; + + let width = template.Obstruction.Static["@depth"] / 2; + let depth = template.Obstruction.Static["@width"] / 2; + const maxSide = Math.max(width, depth); + let templatePos = Vector2D.from3D(data); + let templateAngle = data.angle || 0; + + let edges = this.getValidEdges(data.snapToEdges, templatePos, maxSide); + if (!edges.length) + return undefined; + + let baseEdge = edges[0]; + for (let edge of edges) + if (this.compareEdges(edge, baseEdge) < 0) + baseEdge = edge; + // Now we have the normal, we need to determine an angle, + // which side will be snapped first. + for (let dir = 0; dir < 4; ++dir) + { + const angleCandidate = baseEdge.angle + dir * Math.PI / 2; + // We need to find a minimal angle difference. + let difference = Math.abs(angleCandidate - templateAngle); + difference = Math.min(difference, Math.PI * 2 - difference); + if (difference < Math.PI / 4 + this.EPS) + { + // We need to swap sides for orthogonal cases. + if (dir % 2 == 0) + [width, depth] = [depth, width]; + templateAngle = angleCandidate; + break; + } + } + + let distance = Vector2D.dot(baseEdge.normal, templatePos) - Vector2D.dot(baseEdge.normal, baseEdge.begin); + templatePos.sub(Vector2D.mult(baseEdge.normal, distance - width - this.getPadding(baseEdge))); + edges = this.getValidEdges(data.snapToEdges, templatePos, maxSide); + if (edges.length > 1) + { + let pairedEdges = []; + for (let edge of edges) + { + // We have to place a rectangle, so the angle between + // edges should be 90 degrees. + if (Math.abs(Vector2D.dot(baseEdge.normal, edge.normal)) > this.EPS) + continue; + let newEdge = { + "begin": edge.end, + "end": edge.begin, + "normal": Vector2D.mult(edge.normal, -1), + "signedDistance": -edge.signedDistance, + "offsetDistance": edge.offsetDistance, + "order": "ccw", + }; + pairedEdges.push(edge); + pairedEdges.push(newEdge); + } + pairedEdges.sort(this.compareEdges.bind(this)); + if (pairedEdges.length) + { + let secondEdge = pairedEdges[0]; + for (let edge of pairedEdges) + if (this.compareEdges(edge, secondEdge) < 0) + secondEdge = edge; + let distance = Vector2D.dot(secondEdge.normal, templatePos) - Vector2D.dot(secondEdge.normal, secondEdge.begin); + templatePos.sub(Vector2D.mult(secondEdge.normal, distance - depth - this.getPadding(secondEdge))); + } + } + return { + "x": templatePos.x, + "z": templatePos.y, + "angle": templateAngle + }; + } +} + +ObstructionSnap.prototype.MinimalDistanceToSnap = 5; + +ObstructionSnap.prototype.EPS = 1e-3; + +Engine.RegisterGlobal("ObstructionSnap", ObstructionSnap); Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/ObstructionSnap.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/scriptinterface/ScriptConversions.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptConversions.cpp (revision 23329) +++ ps/trunk/source/scriptinterface/ScriptConversions.cpp (revision 23330) @@ -1,383 +1,384 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptConversions.h" #include "graphics/Entity.h" #include "maths/Vector2D.h" #include "ps/utf16string.h" #include "ps/CStr.h" #define FAIL(msg) STMT(JS_ReportError(cx, msg); return false) // Implicit type conversions often hide bugs, so warn about them #define WARN_IF_NOT(c, v) STMT(if (!(c)) { JS_ReportWarning(cx, "Script value conversion check failed: %s (got type %s)", #c, InformalValueTypeName(v)); }) // TODO: SpiderMonkey: Follow upstream progresses about JS_InformalValueTypeName in the API // https://bugzilla.mozilla.org/show_bug.cgi?id=1285917 static const char* InformalValueTypeName(const JS::Value& v) { if (v.isObject()) return "object"; if (v.isString()) return "string"; if (v.isSymbol()) return "symbol"; if (v.isNumber()) return "number"; if (v.isBoolean()) return "boolean"; if (v.isNull()) return "null"; if (v.isUndefined()) return "undefined"; return "value"; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, bool& out) { JSAutoRequest rq(cx); WARN_IF_NOT(v.isBoolean(), v); out = JS::ToBoolean(v); return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, float& out) { JSAutoRequest rq(cx); double tmp; WARN_IF_NOT(v.isNumber(), v); if (!JS::ToNumber(cx, v, &tmp)) return false; out = tmp; return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, double& out) { JSAutoRequest rq(cx); WARN_IF_NOT(v.isNumber(), v); if (!JS::ToNumber(cx, v, &out)) return false; return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, i32& out) { JSAutoRequest rq(cx); WARN_IF_NOT(v.isNumber(), v); if (!JS::ToInt32(cx, v, &out)) return false; return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, u32& out) { JSAutoRequest rq(cx); WARN_IF_NOT(v.isNumber(), v); if (!JS::ToUint32(cx, v, &out)) return false; return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, u16& out) { JSAutoRequest rq(cx); WARN_IF_NOT(v.isNumber(), v); if (!JS::ToUint16(cx, v, &out)) return false; return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, u8& out) { JSAutoRequest rq(cx); u16 tmp; WARN_IF_NOT(v.isNumber(), v); if (!JS::ToUint16(cx, v, &tmp)) return false; out = (u8)tmp; return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, std::wstring& out) { JSAutoRequest rq(cx); WARN_IF_NOT(v.isString() || v.isNumber(), v); // allow implicit number conversions JS::RootedString str(cx, JS::ToString(cx, v)); if (!str) FAIL("Argument must be convertible to a string"); if (JS_StringHasLatin1Chars(str)) { size_t length; JS::AutoCheckCannotGC nogc; const JS::Latin1Char* ch = JS_GetLatin1StringCharsAndLength(cx, nogc, str, &length); if (!ch) FAIL("JS_GetLatin1StringCharsAndLength failed"); out.assign(ch, ch + length); } else { size_t length; JS::AutoCheckCannotGC nogc; const char16_t* ch = JS_GetTwoByteStringCharsAndLength(cx, nogc, str, &length); if (!ch) FAIL("JS_GetTwoByteStringsCharsAndLength failed"); // out of memory out.assign(ch, ch + length); } return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, Path& out) { std::wstring string; if (!FromJSVal(cx, v, string)) return false; out = string; return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, std::string& out) { JSAutoRequest rq(cx); WARN_IF_NOT(v.isString() || v.isNumber(), v); // allow implicit number conversions JS::RootedString str(cx, JS::ToString(cx, v)); if (!str) FAIL("Argument must be convertible to a string"); char* ch = JS_EncodeString(cx, str); // chops off high byte of each char16_t if (!ch) FAIL("JS_EncodeString failed"); // out of memory out.assign(ch, ch + JS_GetStringLength(str)); JS_free(cx, ch); return true; } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CStr8& out) { return ScriptInterface::FromJSVal(cx, v, static_cast(out)); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CStrW& out) { return ScriptInterface::FromJSVal(cx, v, static_cast(out)); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, Entity& out) { JSAutoRequest rq(cx); if (!v.isObject()) FAIL("Argument must be an object"); JS::RootedObject obj(cx, &v.toObject()); JS::RootedValue templateName(cx); JS::RootedValue id(cx); JS::RootedValue player(cx); JS::RootedValue position(cx); JS::RootedValue rotation(cx); // TODO: Report type errors if (!JS_GetProperty(cx, obj, "player", &player) || !FromJSVal(cx, player, out.playerID)) FAIL("Failed to read Entity.player property"); if (!JS_GetProperty(cx, obj, "templateName", &templateName) || !FromJSVal(cx, templateName, out.templateName)) FAIL("Failed to read Entity.templateName property"); if (!JS_GetProperty(cx, obj, "id", &id) || !FromJSVal(cx, id, out.entityID)) FAIL("Failed to read Entity.id property"); if (!JS_GetProperty(cx, obj, "position", &position) || !FromJSVal(cx, position, out.position)) FAIL("Failed to read Entity.position property"); if (!JS_GetProperty(cx, obj, "rotation", &rotation) || !FromJSVal(cx, rotation, out.rotation)) FAIL("Failed to read Entity.rotation property"); return true; } //////////////////////////////////////////////////////////////// // Primitive types: template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const bool& val) { ret.setBoolean(val); } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const float& val) { ret.set(JS::NumberValue(val)); } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const double& val) { ret.set(JS::NumberValue(val)); } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const i32& val) { ret.set(JS::NumberValue(val)); } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const u16& val) { ret.set(JS::NumberValue(val)); } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const u8& val) { ret.set(JS::NumberValue(val)); } template<> void ScriptInterface::ToJSVal(JSContext* UNUSED(cx), JS::MutableHandleValue ret, const u32& val) { ret.set(JS::NumberValue(val)); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const std::wstring& val) { JSAutoRequest rq(cx); utf16string utf16(val.begin(), val.end()); JS::RootedString str(cx, JS_NewUCStringCopyN(cx, reinterpret_cast (utf16.c_str()), utf16.length())); if (str) ret.setString(str); else ret.setUndefined(); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const Path& val) { ToJSVal(cx, ret, val.string()); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const std::string& val) { JSAutoRequest rq(cx); JS::RootedString str(cx, JS_NewStringCopyN(cx, val.c_str(), val.length())); if (str) ret.setString(str); else ret.setUndefined(); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const wchar_t* const& val) { ToJSVal(cx, ret, std::wstring(val)); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const char* const& val) { JSAutoRequest rq(cx); JS::RootedString str(cx, JS_NewStringCopyZ(cx, val)); if (str) ret.setString(str); else ret.setUndefined(); } #define TOJSVAL_CHAR(N) \ template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const wchar_t (&val)[N]) \ { \ ToJSVal(cx, ret, static_cast(val)); \ } \ template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const char (&val)[N]) \ { \ ToJSVal(cx, ret, static_cast(val)); \ } +TOJSVAL_CHAR(3) TOJSVAL_CHAR(5) TOJSVAL_CHAR(6) TOJSVAL_CHAR(7) TOJSVAL_CHAR(8) TOJSVAL_CHAR(9) TOJSVAL_CHAR(10) TOJSVAL_CHAR(11) TOJSVAL_CHAR(12) TOJSVAL_CHAR(13) TOJSVAL_CHAR(14) TOJSVAL_CHAR(15) TOJSVAL_CHAR(16) TOJSVAL_CHAR(17) TOJSVAL_CHAR(18) TOJSVAL_CHAR(19) TOJSVAL_CHAR(20) TOJSVAL_CHAR(24) TOJSVAL_CHAR(29) TOJSVAL_CHAR(33) TOJSVAL_CHAR(35) TOJSVAL_CHAR(256) #undef TOJSVAL_CHAR template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CStrW& val) { ToJSVal(cx, ret, static_cast(val)); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CStr8& val) { ToJSVal(cx, ret, static_cast(val)); } //////////////////////////////////////////////////////////////// // Compound types // Instantiate various vector types: JSVAL_VECTOR(int) JSVAL_VECTOR(u32) JSVAL_VECTOR(u16) JSVAL_VECTOR(std::string) JSVAL_VECTOR(std::wstring) JSVAL_VECTOR(std::vector) JSVAL_VECTOR(CStr8) JSVAL_VECTOR(std::vector) JSVAL_VECTOR(std::vector) class IComponent; template<> void ScriptInterface::ToJSVal >(JSContext* cx, JS::MutableHandleValue ret, const std::vector& val) { ToJSVal_vector(cx, ret, val); } template<> bool ScriptInterface::FromJSVal >(JSContext* cx, JS::HandleValue v, std::vector& out) { return FromJSVal_vector(cx, v, out); } template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const CVector2D& val) { std::vector vec = {val.X, val.Y}; ToJSVal_vector(cx, ret, vec); } template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, CVector2D& out) { std::vector vec; if (!FromJSVal_vector(cx, v, vec)) return false; if (vec.size() != 2) return false; out.X = vec[0]; out.Y = vec[1]; return true; } #undef FAIL #undef WARN_IF_NOT Index: ps/trunk/source/simulation2/components/CCmpObstruction.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpObstruction.cpp (revision 23329) +++ ps/trunk/source/simulation2/components/CCmpObstruction.cpp (revision 23330) @@ -1,837 +1,842 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpObstruction.h" #include "ps/CLogger.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpUnitMotion.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/serialization/SerializeTemplates.h" /** * Obstruction implementation. This keeps the ICmpPathfinder's model of the world updated when the * entities move and die, with shapes derived from ICmpFootprint. */ class CCmpObstruction : public ICmpObstruction { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_PositionChanged); componentManager.SubscribeToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(Obstruction) typedef ICmpObstructionManager::tag_t tag_t; typedef ICmpObstructionManager::flags_t flags_t; // Template state: EObstructionType m_Type; entity_pos_t m_Size0; // radius or width entity_pos_t m_Size1; // radius or depth flags_t m_TemplateFlags; entity_pos_t m_Clearance; typedef struct { entity_pos_t dx, dz; entity_angle_t da; entity_pos_t size0, size1; flags_t flags; } Shape; std::vector m_Shapes; // Dynamic state: /// Whether the obstruction is actively obstructing or just an inactive placeholder. bool m_Active; /// Whether the entity associated with this obstruction is currently moving. Only applicable for /// UNIT-type obstructions. bool m_Moving; /// Whether an obstruction's control group should be kept consistent and /// used to set control groups for entities that collide with it. bool m_ControlPersist; /** * Primary control group identifier. Indicates to which control group this entity's shape belongs. * Typically used in combination with obstruction test filters to have member shapes ignore each * other during obstruction tests. Defaults to the entity's ID. Must never be set to INVALID_ENTITY. */ entity_id_t m_ControlGroup; /** * Optional secondary control group identifier. Similar to m_ControlGroup; if set to a valid value, * then this field identifies an additional, secondary control group to which this entity's shape * belongs. Set to INVALID_ENTITY to not assign any secondary group. Defaults to INVALID_ENTITY. * * These are only necessary in case it is not sufficient for an entity to belong to only one control * group. Otherwise, they can be ignored. */ entity_id_t m_ControlGroup2; /// Identifier of this entity's obstruction shape, as registered in the obstruction manager. Contains /// structure, but should be treated as opaque here. tag_t m_Tag; std::vector m_ClusterTags; /// Set of flags affecting the behaviour of this entity's obstruction shape. flags_t m_Flags; static std::string GetSchema() { return "" "Causes this entity to obstruct the motion of other units." "" "" "" "" "1.5" "" "" "" "" "1.5" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "1.5" "" "" "" "" "1.5" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { // The minimum obstruction size is the navcell size * sqrt(2) // This is enforced in the schema as a minimum of 1.5 fixed minObstruction = (Pathfinding::NAVCELL_SIZE.Square() * 2).Sqrt(); m_TemplateFlags = 0; if (paramNode.GetChild("BlockMovement").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_MOVEMENT; if (paramNode.GetChild("BlockPathfinding").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_PATHFINDING; if (paramNode.GetChild("BlockFoundation").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_FOUNDATION; if (paramNode.GetChild("BlockConstruction").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION; if (paramNode.GetChild("DeleteUponConstruction").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_DELETE_UPON_CONSTRUCTION; m_Flags = m_TemplateFlags; if (paramNode.GetChild("DisableBlockMovement").ToBool()) m_Flags &= (flags_t)(~ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); if (paramNode.GetChild("DisableBlockPathfinding").ToBool()) m_Flags &= (flags_t)(~ICmpObstructionManager::FLAG_BLOCK_PATHFINDING); if (paramNode.GetChild("Unit").IsOk()) { m_Type = UNIT; CmpPtr cmpUnitMotion(GetEntityHandle()); if (cmpUnitMotion) m_Clearance = cmpUnitMotion->GetUnitClearance(); } else if (paramNode.GetChild("Static").IsOk()) { m_Type = STATIC; m_Size0 = paramNode.GetChild("Static").GetChild("@width").ToFixed(); m_Size1 = paramNode.GetChild("Static").GetChild("@depth").ToFixed(); ENSURE(m_Size0 > minObstruction); ENSURE(m_Size1 > minObstruction); } else { m_Type = CLUSTER; CFixedVector2D max = CFixedVector2D(fixed::FromInt(0), fixed::FromInt(0)); CFixedVector2D min = CFixedVector2D(fixed::FromInt(0), fixed::FromInt(0)); const CParamNode::ChildrenMap& clusterMap = paramNode.GetChild("Obstructions").GetChildren(); for(CParamNode::ChildrenMap::const_iterator it = clusterMap.begin(); it != clusterMap.end(); ++it) { Shape b; b.size0 = it->second.GetChild("@width").ToFixed(); b.size1 = it->second.GetChild("@depth").ToFixed(); ENSURE(b.size0 > minObstruction); ENSURE(b.size1 > minObstruction); b.dx = it->second.GetChild("@x").ToFixed(); b.dz = it->second.GetChild("@z").ToFixed(); b.da = entity_angle_t::FromInt(0); b.flags = m_Flags; m_Shapes.push_back(b); max.X = std::max(max.X, b.dx + b.size0/2); max.Y = std::max(max.Y, b.dz + b.size1/2); min.X = std::min(min.X, b.dx - b.size0/2); min.Y = std::min(min.Y, b.dz - b.size1/2); } m_Size0 = fixed::FromInt(2).Multiply(std::max(max.X, -min.X)); m_Size1 = fixed::FromInt(2).Multiply(std::max(max.Y, -min.Y)); } m_Active = paramNode.GetChild("Active").ToBool(); m_ControlPersist = paramNode.GetChild("ControlPersist").IsOk(); m_Tag = tag_t(); if (m_Type == CLUSTER) m_ClusterTags.clear(); m_Moving = false; m_ControlGroup = GetEntityId(); m_ControlGroup2 = INVALID_ENTITY; } virtual void Deinit() { } struct SerializeTag { template void operator()(S& serialize, const char* UNUSED(name), tag_t& value) { serialize.NumberU32_Unbounded("tag", value.n); } }; template void SerializeCommon(S& serialize) { serialize.Bool("active", m_Active); serialize.Bool("moving", m_Moving); serialize.NumberU32_Unbounded("control group", m_ControlGroup); serialize.NumberU32_Unbounded("control group 2", m_ControlGroup2); serialize.NumberU32_Unbounded("tag", m_Tag.n); serialize.NumberU8_Unbounded("flags", m_Flags); if (m_Type == CLUSTER) SerializeVector()(serialize, "cluster tags", m_ClusterTags); if (m_Type == UNIT) serialize.NumberFixed_Unbounded("clearance", m_Clearance); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_PositionChanged: { if (!m_Active) break; const CMessagePositionChanged& data = static_cast (msg); if (!data.inWorld && !m_Tag.valid()) break; // nothing needs to change CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) break; // error if (data.inWorld && m_Tag.valid()) { cmpObstructionManager->MoveShape(m_Tag, data.x, data.z, data.a); if (m_Type == CLUSTER) { for (size_t i = 0; i < m_Shapes.size(); ++i) { Shape& b = m_Shapes[i]; fixed s, c; sincos_approx(data.a, s, c); cmpObstructionManager->MoveShape(m_ClusterTags[i], data.x + b.dx.Multiply(c) + b.dz.Multiply(s), data.z + b.dz.Multiply(c) - b.dx.Multiply(s), data.a + b.da); } } } else if (data.inWorld && !m_Tag.valid()) { // Need to create a new pathfinder shape: if (m_Type == STATIC) m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(), data.x, data.z, data.a, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2); else if (m_Type == UNIT) m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(), data.x, data.z, m_Clearance, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup); else AddClusterShapes(data.x, data.x, data.a); } else if (!data.inWorld && m_Tag.valid()) { cmpObstructionManager->RemoveShape(m_Tag); m_Tag = tag_t(); if(m_Type == CLUSTER) RemoveClusterShapes(); } break; } case MT_Destroy: { if (m_Tag.valid()) { CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) break; // error // Deactivate the obstruction in case PositionChanged messages are sent after this. m_Active = false; cmpObstructionManager->RemoveShape(m_Tag); m_Tag = tag_t(); if(m_Type == CLUSTER) RemoveClusterShapes(); } break; } } } virtual void SetActive(bool active) { if (active && !m_Active) { m_Active = true; // Construct the obstruction shape CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return; // error CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return; // error if (!cmpPosition->IsInWorld()) return; // don't need an obstruction // TODO: code duplication from message handlers CFixedVector2D pos = cmpPosition->GetPosition2D(); if (m_Type == STATIC) m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(), pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags, m_ControlGroup, m_ControlGroup2); else if (m_Type == UNIT) m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(), pos.X, pos.Y, m_Clearance, (flags_t)(m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0)), m_ControlGroup); else AddClusterShapes(pos.X, pos.Y, cmpPosition->GetRotation().Y); } else if (!active && m_Active) { m_Active = false; // Delete the obstruction shape // TODO: code duplication from message handlers if (m_Tag.valid()) { CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return; // error cmpObstructionManager->RemoveShape(m_Tag); m_Tag = tag_t(); if (m_Type == CLUSTER) RemoveClusterShapes(); } } // else we didn't change the active status } virtual void SetDisableBlockMovementPathfinding(bool movementDisabled, bool pathfindingDisabled, int32_t shape) { flags_t *flags = NULL; if (shape == -1) flags = &m_Flags; else if (m_Type == CLUSTER && shape < (int32_t)m_Shapes.size()) flags = &m_Shapes[shape].flags; else return; // error // Remove the blocking / pathfinding flags or // Add the blocking / pathfinding flags if the template had enabled them if (movementDisabled) *flags &= (flags_t)(~ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); else *flags |= (flags_t)(m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); if (pathfindingDisabled) *flags &= (flags_t)(~ICmpObstructionManager::FLAG_BLOCK_PATHFINDING); else *flags |= (flags_t)(m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_PATHFINDING); // Reset the shape with the new flags (kind of inefficiently - we // should have a ICmpObstructionManager::SetFlags function or something) if (m_Active) { SetActive(false); SetActive(true); } } virtual bool GetBlockMovementFlag() const { return (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) != 0; } virtual EObstructionType GetObstructionType() const { return m_Type; } virtual ICmpObstructionManager::tag_t GetObstruction() const { return m_Tag; } virtual bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) const { return GetObstructionSquare(out, true); } virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) const { return GetObstructionSquare(out, false); } virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out, bool previousPosition) const { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return false; // error CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return false; // error if (!cmpPosition->IsInWorld()) return false; // no obstruction square CFixedVector2D pos; if (previousPosition) pos = cmpPosition->GetPreviousPosition2D(); else pos = cmpPosition->GetPosition2D(); if (m_Type == UNIT) out = cmpObstructionManager->GetUnitShapeObstruction(pos.X, pos.Y, m_Clearance); else out = cmpObstructionManager->GetStaticShapeObstruction(pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1); return true; } virtual entity_pos_t GetUnitRadius() const { if (m_Type == UNIT) return m_Clearance; else return entity_pos_t::Zero(); } virtual entity_pos_t GetSize() const { if (m_Type == UNIT) return m_Clearance; else return CFixedVector2D(m_Size0 / 2, m_Size1 / 2).Length(); } + virtual CFixedVector2D GetStaticSize() const + { + return m_Type == STATIC ? CFixedVector2D(m_Size0, m_Size1) : CFixedVector2D(); + } + virtual void SetUnitClearance(const entity_pos_t& clearance) { if (m_Type == UNIT) m_Clearance = clearance; } virtual bool IsControlPersistent() const { return m_ControlPersist; } virtual bool CheckShorePlacement() const { ICmpObstructionManager::ObstructionSquare s; if (!GetObstructionSquare(s)) return false; CFixedVector2D front = CFixedVector2D(s.x, s.z) + s.v.Multiply(s.hh); CFixedVector2D back = CFixedVector2D(s.x, s.z) - s.v.Multiply(s.hh); CmpPtr cmpTerrain(GetSystemEntity()); CmpPtr cmpWaterManager(GetSystemEntity()); if (!cmpTerrain || !cmpWaterManager) return false; // Keep these constants in agreement with the pathfinder. return cmpWaterManager->GetWaterLevel(front.X, front.Y) - cmpTerrain->GetGroundLevel(front.X, front.Y) > fixed::FromInt(1) && cmpWaterManager->GetWaterLevel( back.X, back.Y) - cmpTerrain->GetGroundLevel( back.X, back.Y) < fixed::FromInt(2); } virtual EFoundationCheck CheckFoundation(const std::string& className) const { return CheckFoundation(className, false); } virtual EFoundationCheck CheckFoundation(const std::string& className, bool onlyCenterPoint) const { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return FOUNDATION_CHECK_FAIL_ERROR; // error if (!cmpPosition->IsInWorld()) return FOUNDATION_CHECK_FAIL_NO_OBSTRUCTION; // no obstruction CFixedVector2D pos = cmpPosition->GetPosition2D(); CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return FOUNDATION_CHECK_FAIL_ERROR; // error // required precondition to use SkipControlGroupsRequireFlagObstructionFilter if (m_ControlGroup == INVALID_ENTITY) { LOGERROR("[CmpObstruction] Cannot test for foundation obstructions; primary control group must be valid"); return FOUNDATION_CHECK_FAIL_ERROR; } // Get passability class pass_class_t passClass = cmpPathfinder->GetPassabilityClass(className); // Ignore collisions within the same control group, or with other non-foundation-blocking shapes. // Note that, since the control group for each entity defaults to the entity's ID, this is typically // equivalent to only ignoring the entity's own shape and other non-foundation-blocking shapes. SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION); if (m_Type == UNIT) return cmpPathfinder->CheckUnitPlacement(filter, pos.X, pos.Y, m_Clearance, passClass, onlyCenterPoint); else return cmpPathfinder->CheckBuildingPlacement(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, GetEntityId(), passClass, onlyCenterPoint); } virtual bool CheckDuplicateFoundation() const { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return false; // error if (!cmpPosition->IsInWorld()) return false; // no obstruction CFixedVector2D pos = cmpPosition->GetPosition2D(); CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return false; // error // required precondition to use SkipControlGroupsRequireFlagObstructionFilter if (m_ControlGroup == INVALID_ENTITY) { LOGERROR("[CmpObstruction] Cannot test for foundation obstructions; primary control group must be valid"); return false; } // Ignore collisions with entities unless they block foundations and match both control groups. SkipTagRequireControlGroupsAndFlagObstructionFilter filter(m_Tag, m_ControlGroup, m_ControlGroup2, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION); if (m_Type == UNIT) return !cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, m_Clearance, NULL); else return !cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, NULL ); } virtual std::vector GetEntitiesByFlags(flags_t flags) const { std::vector ret; CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return ret; // error // Ignore collisions within the same control group, or with other shapes that don't match the filter. // Note that, since the control group for each entity defaults to the entity's ID, this is typically // equivalent to only ignoring the entity's own shape and other shapes that don't match the filter. SkipControlGroupsRequireFlagObstructionFilter filter(false, m_ControlGroup, m_ControlGroup2, flags); ICmpObstructionManager::ObstructionSquare square; if (!GetObstructionSquare(square)) return ret; // error cmpObstructionManager->GetUnitsOnObstruction(square, ret, filter, false); cmpObstructionManager->GetStaticObstructionsOnObstruction(square, ret, filter); return ret; } virtual std::vector GetEntitiesBlockingConstruction() const { return GetEntitiesByFlags(ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION); } virtual std::vector GetEntitiesDeletedUponConstruction() const { return GetEntitiesByFlags(ICmpObstructionManager::FLAG_DELETE_UPON_CONSTRUCTION); } virtual void SetMovingFlag(bool enabled) { m_Moving = enabled; if (m_Tag.valid() && m_Type == UNIT) { CmpPtr cmpObstructionManager(GetSystemEntity()); if (cmpObstructionManager) cmpObstructionManager->SetUnitMovingFlag(m_Tag, m_Moving); } } virtual void SetControlGroup(entity_id_t group) { m_ControlGroup = group; UpdateControlGroups(); } virtual void SetControlGroup2(entity_id_t group2) { m_ControlGroup2 = group2; UpdateControlGroups(); } virtual entity_id_t GetControlGroup() const { return m_ControlGroup; } virtual entity_id_t GetControlGroup2() const { return m_ControlGroup2; } void UpdateControlGroups() { if (m_Tag.valid()) { CmpPtr cmpObstructionManager(GetSystemEntity()); if (cmpObstructionManager) { if (m_Type == UNIT) { cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup); } else if (m_Type == STATIC) { cmpObstructionManager->SetStaticControlGroup(m_Tag, m_ControlGroup, m_ControlGroup2); } else { cmpObstructionManager->SetStaticControlGroup(m_Tag, m_ControlGroup, m_ControlGroup2); for (size_t i = 0; i < m_ClusterTags.size(); ++i) { cmpObstructionManager->SetStaticControlGroup(m_ClusterTags[i], m_ControlGroup, m_ControlGroup2); } } } } } void ResolveFoundationCollisions() const { if (m_Type == UNIT) return; CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return; CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return; // error if (!cmpPosition->IsInWorld()) return; // no obstruction CFixedVector2D pos = cmpPosition->GetPosition2D(); // Ignore collisions within the same control group, or with other non-foundation-blocking shapes. // Note that, since the control group for each entity defaults to the entity's ID, this is typically // equivalent to only ignoring the entity's own shape and other non-foundation-blocking shapes. SkipControlGroupsRequireFlagObstructionFilter filter(m_ControlGroup, m_ControlGroup2, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION); std::vector collisions; if (cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, &collisions)) { std::vector persistentEnts, normalEnts; if (m_ControlPersist) persistentEnts.push_back(m_ControlGroup); else normalEnts.push_back(GetEntityId()); for (std::vector::iterator it = collisions.begin(); it != collisions.end(); ++it) { entity_id_t ent = *it; if (ent == INVALID_ENTITY) continue; CmpPtr cmpObstruction(GetSimContext(), ent); if (!cmpObstruction->IsControlPersistent()) normalEnts.push_back(ent); else persistentEnts.push_back(cmpObstruction->GetControlGroup()); } // The collision can't be resolved without usable persistent control groups. if (persistentEnts.empty()) return; // Attempt to replace colliding entities' control groups with a persistent one. for (std::vector::iterator it = normalEnts.begin(); it != normalEnts.end(); ++it) { entity_id_t ent = *it; CmpPtr cmpObstruction(GetSimContext(), ent); for (std::vector::iterator it = persistentEnts.begin(); it != persistentEnts.end(); ++it) { entity_id_t persistent = *it; entity_id_t group = cmpObstruction->GetControlGroup(); // Only clobber 'default' control groups. if (group == ent) cmpObstruction->SetControlGroup(persistent); else if (cmpObstruction->GetControlGroup2() == INVALID_ENTITY && group != persistent) cmpObstruction->SetControlGroup2(persistent); } } } } protected: inline void AddClusterShapes(entity_pos_t x, entity_pos_t z, entity_angle_t a) { CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return; // error flags_t flags = m_Flags; // Disable block movement and block pathfinding for the obstruction shape flags &= (flags_t)(~ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); flags &= (flags_t)(~ICmpObstructionManager::FLAG_BLOCK_PATHFINDING); m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(), x, z, a, m_Size0, m_Size1, flags, m_ControlGroup, m_ControlGroup2); fixed s, c; sincos_approx(a, s, c); for (size_t i = 0; i < m_Shapes.size(); ++i) { Shape& b = m_Shapes[i]; tag_t tag = cmpObstructionManager->AddStaticShape(GetEntityId(), x + b.dx.Multiply(c) + b.dz.Multiply(s), z + b.dz.Multiply(c) - b.dx.Multiply(s), a + b.da, b.size0, b.size1, b.flags, m_ControlGroup, m_ControlGroup2); m_ClusterTags.push_back(tag); } } inline void RemoveClusterShapes() { CmpPtr cmpObstructionManager(GetSystemEntity()); if (!cmpObstructionManager) return; // error for (size_t i = 0; i < m_ClusterTags.size(); ++i) { if (m_ClusterTags[i].valid()) { cmpObstructionManager->RemoveShape(m_ClusterTags[i]); } } m_ClusterTags.clear(); } }; REGISTER_COMPONENT_TYPE(Obstruction) Index: ps/trunk/source/simulation2/components/ICmpObstruction.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpObstruction.h (revision 23329) +++ ps/trunk/source/simulation2/components/ICmpObstruction.h (revision 23330) @@ -1,147 +1,149 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_ICMPOBSTRUCTION #define INCLUDED_ICMPOBSTRUCTION #include "simulation2/system/Interface.h" #include "simulation2/components/ICmpObstructionManager.h" /** * Flags an entity as obstructing movement for other units, * and handles the processing of collision queries. */ class ICmpObstruction : public IComponent { public: enum EFoundationCheck { FOUNDATION_CHECK_SUCCESS, FOUNDATION_CHECK_FAIL_ERROR, FOUNDATION_CHECK_FAIL_NO_OBSTRUCTION, FOUNDATION_CHECK_FAIL_OBSTRUCTS_FOUNDATION, FOUNDATION_CHECK_FAIL_TERRAIN_CLASS }; enum EObstructionType { STATIC, UNIT, CLUSTER }; virtual ICmpObstructionManager::tag_t GetObstruction() const = 0; /** * Gets the square corresponding to this obstruction shape. * @return true and updates @p out on success; * false on failure (e.g. object not in the world). */ virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) const = 0; /** * Same as the method above, but returns an obstruction shape for the previous turn */ virtual bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) const = 0; virtual entity_pos_t GetSize() const = 0; + virtual CFixedVector2D GetStaticSize() const = 0; + virtual entity_pos_t GetUnitRadius() const = 0; virtual EObstructionType GetObstructionType() const = 0; virtual void SetUnitClearance(const entity_pos_t& clearance) = 0; virtual bool IsControlPersistent() const = 0; /** * Test whether the front of the obstruction square is in the water and the back is on the shore. */ virtual bool CheckShorePlacement() const = 0; /** * Test whether this entity is colliding with any obstruction that are set to * block the creation of foundations. * @param ignoredEntities List of entities to ignore during the test. * @return FOUNDATION_CHECK_SUCCESS if check passes, else an EFoundationCheck * value describing the type of failure. */ virtual EFoundationCheck CheckFoundation(const std::string& className) const = 0; virtual EFoundationCheck CheckFoundation(const std::string& className, bool onlyCenterPoint) const = 0; /** * CheckFoundation wrapper for script calls, to return friendly strings instead of an EFoundationCheck. * @return "success" if check passes, else a string describing the type of failure. */ virtual std::string CheckFoundation_wrapper(const std::string& className, bool onlyCenterPoint) const; /** * Test whether this entity is colliding with any obstructions that share its * control groups and block the creation of foundations. * @return true if foundation is valid (not obstructed) */ virtual bool CheckDuplicateFoundation() const = 0; /** * Returns a list of entities that have an obstruction matching the given flag and intersect the current obstruction. * @return vector of blocking entities */ virtual std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t flags) const = 0; /** * Returns a list of entities that are blocking construction of a foundation. * @return vector of blocking entities */ virtual std::vector GetEntitiesBlockingConstruction() const = 0; /** * Returns a list of entities that shall be deleted when a construction on this obstruction starts, * for example sheep carcasses. */ virtual std::vector GetEntitiesDeletedUponConstruction() const = 0; /** * Detects collisions between foundation-blocking entities and * tries to fix them by setting control groups, if appropriate. */ virtual void ResolveFoundationCollisions() const = 0; virtual void SetActive(bool active) = 0; virtual void SetMovingFlag(bool enabled) = 0; virtual void SetDisableBlockMovementPathfinding(bool movementDisabled, bool pathfindingDisabled, int32_t shape) = 0; virtual bool GetBlockMovementFlag() const = 0; /** * Change the control group that the entity belongs to. * Control groups are used to let units ignore collisions with other units from * the same group. Default is the entity's own ID. */ virtual void SetControlGroup(entity_id_t group) = 0; /// See SetControlGroup. virtual entity_id_t GetControlGroup() const = 0; virtual void SetControlGroup2(entity_id_t group2) = 0; virtual entity_id_t GetControlGroup2() const = 0; DECLARE_INTERFACE_TYPE(Obstruction) }; #endif // INCLUDED_ICMPOBSTRUCTION Index: ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h (revision 23329) +++ ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h (revision 23330) @@ -1,592 +1,593 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpObstruction.h" class MockObstruction : public ICmpObstruction { public: DEFAULT_MOCK_COMPONENT() ICmpObstructionManager::ObstructionSquare obstruction; virtual ICmpObstructionManager::tag_t GetObstruction() const { return ICmpObstructionManager::tag_t(); } virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) const { out = obstruction; return true; } virtual bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare& UNUSED(out)) const { return true; } virtual entity_pos_t GetSize() const { return entity_pos_t::Zero(); } + virtual CFixedVector2D GetStaticSize() const { return CFixedVector2D(); } virtual entity_pos_t GetUnitRadius() const { return entity_pos_t::Zero(); } virtual EObstructionType GetObstructionType() const { return ICmpObstruction::STATIC; } virtual void SetUnitClearance(const entity_pos_t& UNUSED(clearance)) { } virtual bool IsControlPersistent() const { return true; } virtual bool CheckShorePlacement() const { return true; } virtual EFoundationCheck CheckFoundation(const std::string& UNUSED(className)) const { return EFoundationCheck(); } virtual EFoundationCheck CheckFoundation(const std::string& UNUSED(className), bool UNUSED(onlyCenterPoint)) const { return EFoundationCheck(); } virtual std::string CheckFoundation_wrapper(const std::string& UNUSED(className), bool UNUSED(onlyCenterPoint)) const { return std::string(); } virtual bool CheckDuplicateFoundation() const { return true; } virtual std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t UNUSED(flags)) const { return std::vector(); } virtual std::vector GetEntitiesBlockingConstruction() const { return std::vector(); } virtual std::vector GetEntitiesDeletedUponConstruction() const { return std::vector(); } virtual void ResolveFoundationCollisions() const { } virtual void SetActive(bool UNUSED(active)) { } virtual void SetMovingFlag(bool UNUSED(enabled)) { } virtual void SetDisableBlockMovementPathfinding(bool UNUSED(movementDisabled), bool UNUSED(pathfindingDisabled), int32_t UNUSED(shape)) { } virtual bool GetBlockMovementFlag() const { return true; } virtual void SetControlGroup(entity_id_t UNUSED(group)) { } virtual entity_id_t GetControlGroup() const { return INVALID_ENTITY; } virtual void SetControlGroup2(entity_id_t UNUSED(group2)) { } virtual entity_id_t GetControlGroup2() const { return INVALID_ENTITY; } }; class TestCmpObstructionManager : public CxxTest::TestSuite { typedef ICmpObstructionManager::tag_t tag_t; typedef ICmpObstructionManager::ObstructionSquare ObstructionSquare; // some variables for setting up a scene with 3 shapes entity_id_t ent1, ent2, ent3; // entity IDs entity_angle_t ent1a; // angles entity_pos_t ent1x, ent1z, ent1w, ent1h, // positions/dimensions ent2x, ent2z, ent2c, ent3x, ent3z, ent3c; entity_id_t ent1g1, ent1g2, ent2g, ent3g; // control groups tag_t shape1, shape2, shape3; ICmpObstructionManager* cmp; ComponentTestHelper* testHelper; public: void setUp() { CXeromyces::Startup(); CxxTest::setAbortTestOnFail(true); // set up a simple scene with some predefined obstruction shapes // (we can't position shapes on the origin because the world bounds must range // from 0 to X, so instead we'll offset things by, say, 10). ent1 = 1; ent1a = fixed::Zero(); ent1w = fixed::FromFloat(4); ent1h = fixed::FromFloat(2); ent1x = fixed::FromInt(10); ent1z = fixed::FromInt(10); ent1g1 = ent1; ent1g2 = INVALID_ENTITY; ent2 = 2; ent2c = fixed::FromFloat(1); ent2x = ent1x; ent2z = ent1z; ent2g = ent1g1; ent3 = 3; ent3c = fixed::FromFloat(3); ent3x = ent2x; ent3z = ent2z + ent2c + ent3c; // ensure it just touches the border of ent2 ent3g = ent3; testHelper = new ComponentTestHelper(g_ScriptRuntime); cmp = testHelper->Add(CID_ObstructionManager, "", SYSTEM_ENTITY); cmp->SetBounds(fixed::FromInt(0), fixed::FromInt(0), fixed::FromInt(1000), fixed::FromInt(1000)); shape1 = cmp->AddStaticShape(ent1, ent1x, ent1z, ent1a, ent1w, ent1h, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_MOVING, ent1g1, ent1g2); shape2 = cmp->AddUnitShape(ent2, ent2x, ent2z, ent2c, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent2g); shape3 = cmp->AddUnitShape(ent3, ent3x, ent3z, ent3c, ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent3g); } void tearDown() { delete testHelper; cmp = NULL; // not our responsibility to deallocate CXeromyces::Terminate(); } /** * Verifies the collision testing procedure. Collision-tests some simple shapes against the shapes registered in * the scene, and verifies the result of the test against the expected value. */ void test_simple_collisions() { std::vector out; NullObstructionFilter nullFilter; // Collision-test a simple shape nested inside shape3 against all shapes in the scene. Since the tested shape // overlaps only with shape 3, we should find only shape 3 in the result. cmp->TestUnitShape(nullFilter, ent3x, ent3z, fixed::FromInt(1), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestStaticShape(nullFilter, ent3x, ent3z, fixed::Zero(), fixed::FromInt(1), fixed::FromInt(1), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // Similarly, collision-test a simple shape nested inside both shape1 and shape2. Since the tested shape overlaps // only with shapes 1 and 2, those are the only ones we should find in the result. cmp->TestUnitShape(nullFilter, ent2x, ent2z, ent2c/2, &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); cmp->TestStaticShape(nullFilter, ent2x, ent2z, fixed::Zero(), ent2c, ent2c, &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); } /** * Verifies the behaviour of the null obstruction filter. Tests with this filter will be performed against all * registered shapes. */ void test_filter_null() { std::vector out; // Collision test a scene-covering shape against all shapes in the scene. We should find all registered shapes // in the result. NullObstructionFilter nullFilter; cmp->TestUnitShape(nullFilter, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(3U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(nullFilter, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(3U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); } /** * Verifies the behaviour of the StationaryOnlyObstructionFilter. Tests with this filter will be performed only * against non-moving (stationary) shapes. */ void test_filter_stationary_only() { std::vector out; // Collision test a scene-covering shape against all shapes in the scene, but skipping shapes that are moving, // i.e. shapes that have the MOVING flag. Since only shape 1 is flagged as moving, we should find // shapes 2 and 3 in each case. StationaryOnlyObstructionFilter ignoreMoving; cmp->TestUnitShape(ignoreMoving, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(ignoreMoving, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); } /** * Verifies the behaviour of the SkipTagObstructionFilter. Tests with this filter will be performed against * all registered shapes that do not have the specified tag set. */ void test_filter_skip_tag() { std::vector out; // Collision-test shape 2's obstruction shape against all shapes in the scene, but skipping tests against // shape 2. Since shape 2 overlaps only with shape 1, we should find only shape 1's entity ID in the result. SkipTagObstructionFilter ignoreShape2(shape2); cmp->TestUnitShape(ignoreShape2, ent2x, ent2z, ent2c/2, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent1, out[0]); out.clear(); cmp->TestStaticShape(ignoreShape2, ent2x, ent2z, fixed::Zero(), ent2c, ent2c, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent1, out[0]); out.clear(); } /** * Verifies the behaviour of the SkipTagFlagsObstructionFilter. Tests with this filter will be performed against * all registered shapes that do not have the specified tag set, and that have at least one of required flags set. */ void test_filter_skip_tag_require_flag() { std::vector out; // Collision-test a scene-covering shape against all shapes in the scene, but skipping tests against shape 1 // and requiring the BLOCK_MOVEMENT flag. Since shape 1 is being ignored and shape 2 does not have the required // flag, we should find only shape 3 in the results. SkipTagRequireFlagsObstructionFilter skipShape1RequireBlockMovement(shape1, ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); cmp->TestUnitShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestStaticShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // If we now do the same test, but require at least one of the entire set of available filters, we should find // all shapes that are not shape 1 and that have at least one flag set. Since all shapes in our testing scene // have at least one flag set, we should find shape 2 and shape 3 in the results. SkipTagRequireFlagsObstructionFilter skipShape1RequireAnyFlag(shape1, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); // And if we now do the same test yet again, but specify an empty set of flags, then it becomes impossible for // any shape to have at least one of the required flags, and we should hence find no shapes in the result. SkipTagRequireFlagsObstructionFilter skipShape1RejectAll(shape1, 0U); cmp->TestUnitShape(skipShape1RejectAll, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestStaticShape(skipShape1RejectAll, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); } /** * Verifies the behaviour of SkipControlGroupsRequireFlagObstructionFilter. Tests with this filter will be performed * against all registered shapes that are members of neither specified control groups, and that have at least one of * the specified flags set. */ void test_filter_skip_controlgroups_require_flag() { std::vector out; // Collision-test a shape that overlaps the entire scene, but ignoring shapes from shape1's control group // (which also includes shape 2), and requiring that either the BLOCK_FOUNDATION or the // BLOCK_CONSTRUCTION flag is set, or both. Since shape 1 and shape 2 both belong to shape 1's control // group, and shape 3 has the BLOCK_FOUNDATION flag (but not BLOCK_CONSTRUCTION), we should find only // shape 3 in the result. SkipControlGroupsRequireFlagObstructionFilter skipGroup1ReqFoundConstr(ent1g1, INVALID_ENTITY, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION); cmp->TestUnitShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestStaticShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // Perform the same test, but now also exclude shape 3's control group (in addition to shape 1's control // group). Despite shape 3 having at least one of the required flags set, it should now also be ignored, // yielding an empty result set. SkipControlGroupsRequireFlagObstructionFilter skipGroup1And3ReqFoundConstr(ent1g1, ent3g, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION); cmp->TestUnitShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestStaticShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); // Same test, but this time excluding only shape 3's control group, and requiring any of the available flags // to be set. Since both shape 1 and shape 2 have at least one flag set and are both in a different control // group, we should find them in the result. SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireAnyFlag(ent3g, INVALID_ENTITY, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); cmp->TestStaticShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); // Finally, the same test as the one directly above, now with an empty set of required flags. Since it now becomes // impossible for shape 1 and shape 2 to have at least one of the required flags set, and shape 3 is excluded by // virtue of the control group filtering, we should find an empty result. SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireNoFlags(ent3g, INVALID_ENTITY, 0U); cmp->TestUnitShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestStaticShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); // ------------------------------------------------------------------------------------ // In the tests up until this point, the shapes have all been filtered out based on their primary control group. // Now, to verify that shapes are also filtered out based on their secondary control groups, add a fourth shape // with arbitrarily-chosen dual control groups, and also change shape 1's secondary control group to another // arbitrarily-chosen control group. Then, do a scene-covering collision test while filtering out a combination // of shape 1's secondary control group, and one of shape 4's control groups. We should find neither ent1 nor ent4 // in the result. entity_id_t ent4 = 4, ent4g1 = 17, ent4g2 = 19, ent1g2_new = 18; // new secondary control group for entity 1 entity_pos_t ent4x = fixed::FromInt(4), ent4z = fixed::Zero(), ent4w = fixed::FromInt(1), ent4h = fixed::FromInt(1); entity_angle_t ent4a = fixed::FromDouble(M_PI/3); cmp->AddStaticShape(ent4, ent4x, ent4z, ent4a, ent4w, ent4h, ICmpObstructionManager::FLAG_BLOCK_PATHFINDING, ent4g1, ent4g2); cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2_new); // Exclude shape 1's and shape 4's secondary control groups from testing, and require any available flag to be set. // Since neither shape 2 nor shape 3 are part of those control groups and both have at least one available flag set, // the results should only those two shapes' entities. SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4SecRequireAny(ent1g2_new, ent4g2, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); // Same as the above, but now exclude shape 1's secondary and shape 4's primary control group, while still requiring // any available flag to be set. (Note that the test above used shape 4's secondary control group). Results should // remain the same. SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4PrimRequireAny(ent1g2_new, ent4g1, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2); // restore shape 1's original secondary control group } void test_adjacent_shapes() { std::vector out; NullObstructionFilter nullFilter; SkipTagObstructionFilter ignoreShape1(shape1); SkipTagObstructionFilter ignoreShape2(shape2); SkipTagObstructionFilter ignoreShape3(shape3); // Collision-test a shape that is perfectly adjacent to shape3. This should be counted as a hit according to // the code at the time of writing. entity_angle_t ent4a = fixed::FromDouble(M_PI); // rotated 180 degrees, should not affect collision test entity_pos_t ent4w = fixed::FromInt(2), ent4h = fixed::FromInt(1), ent4x = ent3x + ent3c + ent4w/2, // make ent4 adjacent to ent3 ent4z = ent3z; cmp->TestStaticShape(nullFilter, ent4x, ent4z, ent4a, ent4w, ent4h, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestUnitShape(nullFilter, ent4x, ent4z, ent4w/2, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // now do the same tests, but move the shape a little bit to the right so that it doesn't touch anymore cmp->TestStaticShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4a, ent4w, ent4h, &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestUnitShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4w/2, &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); } /** * Verifies that fetching the registered shapes from the obstruction manager yields the correct results. */ void test_get_obstruction() { ObstructionSquare obSquare1 = cmp->GetObstruction(shape1); ObstructionSquare obSquare2 = cmp->GetObstruction(shape2); ObstructionSquare obSquare3 = cmp->GetObstruction(shape3); TS_ASSERT_EQUALS(obSquare1.hh, ent1h/2); TS_ASSERT_EQUALS(obSquare1.hw, ent1w/2); TS_ASSERT_EQUALS(obSquare1.x, ent1x); TS_ASSERT_EQUALS(obSquare1.z, ent1z); TS_ASSERT_EQUALS(obSquare1.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0))); TS_ASSERT_EQUALS(obSquare1.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1))); TS_ASSERT_EQUALS(obSquare2.hh, ent2c); TS_ASSERT_EQUALS(obSquare2.hw, ent2c); TS_ASSERT_EQUALS(obSquare2.x, ent2x); TS_ASSERT_EQUALS(obSquare2.z, ent2z); TS_ASSERT_EQUALS(obSquare2.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0))); TS_ASSERT_EQUALS(obSquare2.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1))); TS_ASSERT_EQUALS(obSquare3.hh, ent3c); TS_ASSERT_EQUALS(obSquare3.hw, ent3c); TS_ASSERT_EQUALS(obSquare3.x, ent3x); TS_ASSERT_EQUALS(obSquare3.z, ent3z); TS_ASSERT_EQUALS(obSquare3.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0))); TS_ASSERT_EQUALS(obSquare3.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1))); } /** * Verifies the calculations of distances between shapes. */ void test_distance_to() { // Create two more entities to have non-zero distances entity_id_t ent4 = 4, ent4g1 = ent4, ent4g2 = INVALID_ENTITY, ent5 = 5, ent5g1 = ent5, ent5g2 = INVALID_ENTITY; entity_pos_t ent4a = fixed::Zero(), ent4w = fixed::FromInt(6), ent4h = fixed::Zero(), ent4x = ent1x, ent4z = fixed::FromInt(20), ent5a = fixed::Zero(), ent5w = fixed::FromInt(2), ent5h = fixed::FromInt(4), ent5x = fixed::FromInt(20), ent5z = ent1z; tag_t shape4 = cmp->AddStaticShape(ent4, ent4x, ent4z, ent4a, ent4w, ent4h, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_MOVING, ent4g1, ent4g2); tag_t shape5 = cmp->AddStaticShape(ent5, ent5x, ent5z, ent5a, ent5w, ent5h, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_MOVING, ent5g1, ent5g2); MockObstruction obstruction1, obstruction2, obstruction3, obstruction4, obstruction5; testHelper->AddMock(ent1, IID_Obstruction, obstruction1); testHelper->AddMock(ent2, IID_Obstruction, obstruction2); testHelper->AddMock(ent3, IID_Obstruction, obstruction3); testHelper->AddMock(ent4, IID_Obstruction, obstruction4); testHelper->AddMock(ent5, IID_Obstruction, obstruction5); obstruction1.obstruction = cmp->GetObstruction(shape1); obstruction2.obstruction = cmp->GetObstruction(shape2); obstruction3.obstruction = cmp->GetObstruction(shape3); obstruction4.obstruction = cmp->GetObstruction(shape4); obstruction5.obstruction = cmp->GetObstruction(shape5); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent1, ent2)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent2, ent1)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent2, ent3)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent3, ent2)); // Due to rounding errors we need to use some leeway TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(80)), cmp->MaxDistanceToTarget(ent2, ent3), fixed::FromFloat(0.0001)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(80)), cmp->MaxDistanceToTarget(ent3, ent2), fixed::FromFloat(0.0001)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent1, ent3)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent3, ent1)); TS_ASSERT_EQUALS(fixed::FromInt(6), cmp->DistanceToTarget(ent1, ent4)); TS_ASSERT_EQUALS(fixed::FromInt(6), cmp->DistanceToTarget(ent4, ent1)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(125) + 3), cmp->MaxDistanceToTarget(ent1, ent4), fixed::FromFloat(0.0001)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(125) + 3), cmp->MaxDistanceToTarget(ent4, ent1), fixed::FromFloat(0.0001)); TS_ASSERT_EQUALS(fixed::FromInt(7), cmp->DistanceToTarget(ent1, ent5)); TS_ASSERT_EQUALS(fixed::FromInt(7), cmp->DistanceToTarget(ent5, ent1)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(178)), cmp->MaxDistanceToTarget(ent1, ent5), fixed::FromFloat(0.0001)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(178)), cmp->MaxDistanceToTarget(ent5, ent1), fixed::FromFloat(0.0001)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent2, fixed::Zero(), fixed::FromInt(1), true)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent2, fixed::Zero(), fixed::FromInt(1), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent2, fixed::FromInt(1), fixed::FromInt(1), true)); TS_ASSERT(!cmp->IsInTargetRange(ent1, ent2, fixed::FromInt(1), fixed::FromInt(1), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::Zero(), fixed::FromInt(10), true)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::Zero(), fixed::FromInt(10), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(1), fixed::FromInt(10), true)); TS_ASSERT(!cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(1), fixed::FromInt(5), false)); TS_ASSERT(!cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(10), fixed::FromInt(10), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(10), fixed::FromInt(10), true)); } }; Index: ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp (revision 23329) +++ ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp (revision 23330) @@ -1,145 +1,222 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Simulation.h" #include "graphics/GameView.h" +#include "ps/ConfigDB.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/Entity.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpGuiInterface.h" +#include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/helpers/Selection.h" +#include #include JS::Value JSI_Simulation::GuiInterfaceCall(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue data) { if (!g_Game) return JS::UndefinedValue(); CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY); if (!cmpGuiInterface) return JS::UndefinedValue(); JSContext* cxSim = sim->GetScriptInterface().GetContext(); JSAutoRequest rqSim(cxSim); JS::RootedValue arg(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), data)); JS::RootedValue ret(cxSim); cmpGuiInterface->ScriptCall(g_Game->GetViewedPlayerID(), name, arg, &ret); return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(sim->GetScriptInterface(), ret); } void JSI_Simulation::PostNetworkCommand(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue cmd) { if (!g_Game) return; CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY); if (!cmpCommandQueue) return; JSContext* cxSim = sim->GetScriptInterface().GetContext(); JSAutoRequest rqSim(cxSim); JS::RootedValue cmd2(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), cmd)); cmpCommandQueue->PostNetworkCommand(cmd2); } void JSI_Simulation::DumpSimState(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { OsPath path = psLogDir()/"sim_dump.txt"; std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); g_Game->GetSimulation2()->DumpDebugState(file); } entity_id_t JSI_Simulation::PickEntityAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y) { return EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetViewedPlayerID(), false); } std::vector JSI_Simulation::PickPlayerEntitiesInRect(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x0, int y0, int x1, int y1, int player) { return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false); } std::vector JSI_Simulation::PickPlayerEntitiesOnScreen(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int player) { return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres, player, false); } std::vector JSI_Simulation::PickNonGaiaEntitiesOnScreen(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return EntitySelection::PickNonGaiaEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres, false); } std::vector JSI_Simulation::GetEntitiesWithStaticObstructionOnScreen(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { struct StaticObstructionFilter { bool operator()(IComponent* cmp) { ICmpObstruction* cmpObstruction = static_cast(cmp); return cmpObstruction->GetObstructionType() == ICmpObstruction::STATIC; } }; return EntitySelection::GetEntitiesWithComponentInRect(*g_Game->GetSimulation2(), IID_Obstruction, *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres); } +JS::Value JSI_Simulation::GetEdgesOfStaticObstructionsOnScreenNearTo(ScriptInterface::CxPrivate* pCxPrivate, entity_pos_t x, entity_pos_t z) +{ + if (!g_Game) + return JS::UndefinedValue(); + + CSimulation2* sim = g_Game->GetSimulation2(); + ENSURE(sim); + + JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); + JSAutoRequest rq(cx); + JS::RootedValue edgeList(cx); + ScriptInterface::CreateArray(cx, &edgeList); + int edgeListIndex = 0; + + float distanceThreshold = 10.0f; + CFG_GET_VAL("gui.session.snaptoedgesdistancethreshold", distanceThreshold); + CFixedVector2D entityPos(x, z); + + std::vector entities = GetEntitiesWithStaticObstructionOnScreen(pCxPrivate); + for (entity_id_t entity : entities) + { + CmpPtr cmpObstruction(sim->GetSimContext(), entity); + if (!cmpObstruction) + continue; + + CmpPtr cmpPosition(sim->GetSimContext(), entity); + if (!cmpPosition || !cmpPosition->IsInWorld()) + continue; + + CFixedVector2D halfSize = cmpObstruction->GetStaticSize() / 2; + if (halfSize.X.IsZero() || halfSize.Y.IsZero() || std::max(halfSize.X, halfSize.Y) <= fixed::FromInt(2)) + continue; + + std::array corners = { + CFixedVector2D(-halfSize.X, -halfSize.Y), + CFixedVector2D(-halfSize.X, halfSize.Y), + halfSize, + CFixedVector2D(halfSize.X, -halfSize.Y) + }; + fixed angle = cmpPosition->GetRotation().Y; + for (CFixedVector2D& corner : corners) + corner = corner.Rotate(angle) + cmpPosition->GetPosition2D(); + + for (size_t i = 0; i < corners.size(); ++i) + { + JS::RootedValue edge(cx); + const CFixedVector2D& corner = corners[i]; + const CFixedVector2D& nextCorner = corners[(i + 1) % corners.size()]; + + // TODO: calculate real distance; + fixed distanceToEdge = std::min( + (corner - entityPos).Length(), + (nextCorner - entityPos).Length()); + if (distanceToEdge.ToFloat() > distanceThreshold) + continue; + + CFixedVector2D normal = -(nextCorner - corner).Perpendicular(); + normal.Normalize(); + ScriptInterface::CreateObject( + cx, + &edge, + "begin", corner, + "end", nextCorner, + "angle", angle, + "normal", normal, + "order", "cw"); + + pCxPrivate->pScriptInterface->SetPropertyInt(edgeList, edgeListIndex++, edge); + } + } + return edgeList; +} + std::vector JSI_Simulation::PickSimilarPlayerEntities(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations) { return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetViewedPlayerID(), includeOffScreen, matchRank, false, allowFoundations); } JS::Value JSI_Simulation::GetAIs(ScriptInterface::CxPrivate* pCxPrivate) { return ICmpAIManager::GetAIs(*(pCxPrivate->pScriptInterface)); } void JSI_Simulation::SetBoundingBoxDebugOverlay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool enabled) { ICmpSelectable::ms_EnableDebugOverlays = enabled; } void JSI_Simulation::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GuiInterfaceCall"); scriptInterface.RegisterFunction("PostNetworkCommand"); scriptInterface.RegisterFunction("DumpSimState"); scriptInterface.RegisterFunction("GetAIs"); scriptInterface.RegisterFunction("PickEntityAtPoint"); scriptInterface.RegisterFunction, int, int, int, int, int, &PickPlayerEntitiesInRect>("PickPlayerEntitiesInRect"); scriptInterface.RegisterFunction, int, &PickPlayerEntitiesOnScreen>("PickPlayerEntitiesOnScreen"); scriptInterface.RegisterFunction, &PickNonGaiaEntitiesOnScreen>("PickNonGaiaEntitiesOnScreen"); scriptInterface.RegisterFunction, &GetEntitiesWithStaticObstructionOnScreen>("GetEntitiesWithStaticObstructionOnScreen"); + scriptInterface.RegisterFunction("GetEdgesOfStaticObstructionsOnScreenNearTo"); scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarPlayerEntities>("PickSimilarPlayerEntities"); scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay"); } Index: ps/trunk/source/simulation2/scripting/JSInterface_Simulation.h =================================================================== --- ps/trunk/source/simulation2/scripting/JSInterface_Simulation.h (revision 23329) +++ ps/trunk/source/simulation2/scripting/JSInterface_Simulation.h (revision 23330) @@ -1,41 +1,43 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_JSI_SIMULATION #define INCLUDED_JSI_SIMULATION #include "scriptinterface/ScriptInterface.h" +#include "simulation2/helpers/Position.h" #include "simulation2/system/Entity.h" namespace JSI_Simulation { JS::Value GuiInterfaceCall(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue data); void PostNetworkCommand(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue cmd); entity_id_t PickEntityAtPoint(ScriptInterface::CxPrivate* pCxPrivate, int x, int y); void DumpSimState(ScriptInterface::CxPrivate* pCxPrivate); std::vector PickPlayerEntitiesInRect(ScriptInterface::CxPrivate* pCxPrivate, int x0, int y0, int x1, int y1, int player); std::vector PickPlayerEntitiesOnScreen(ScriptInterface::CxPrivate* pCxPrivate, int player); std::vector PickNonGaiaEntitiesOnScreen(ScriptInterface::CxPrivate* pCxPrivate); std::vector GetEntitiesWithStaticObstructionOnScreen(ScriptInterface::CxPrivate* pCxPrivate); + JS::Value GetEdgesOfStaticObstructionsOnScreenNearTo(ScriptInterface::CxPrivate* pCxPrivate, entity_pos_t x, entity_pos_t z); std::vector PickSimilarPlayerEntities(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations); JS::Value GetAIs(ScriptInterface::CxPrivate* pCxPrivate); void SetBoundingBoxDebugOverlay(ScriptInterface::CxPrivate* pCxPrivate, bool enabled); void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); } #endif // INCLUDED_JSI_SIMULATION