Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 19271) +++ ps/trunk/binaries/data/config/default.cfg (revision 19272) @@ -1,433 +1,434 @@ ; 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 waterugly=false; 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 shadowpcf = true vsync = false particles = 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 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" [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 leave = Escape ; End current game or Exit 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+W" ; Toggle wireframe mode silhouettes = "Alt+S" ; Toggle unit silhouettes showsky = "Alt+Z" ; Toggle sky ; > 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 session.gui.toggle = "Alt+G" ; Toggle visibility of session GUI menu.toggle = "F10" ; Toggle in-game menu timeelapsedcounter.toggle = "F12" ; Toggle time elapsed 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 ; > 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 milonly = Alt ; Add only military units to selection idleonly = "I" ; Select only idle 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 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 attack = Ctrl ; Modifier to attack instead of another action (eg 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 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 [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.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) [gui.menu] limitfps = true ; Limit FPS in the menus and loading screen [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 batchtrainingsize = 5 ; Number of units to be trained per batch (when pressing the hotkey) [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 [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 [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 = "arena22" ; Default MUC room to join server = "lobby.wildfiregames.com" ; Address of lobby server xpartamupp = "wfgbot22" ; Name of the server-side xmpp client that manage games [mod] enabledmods = "mod public" [network] duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join. lateobserverjoins = false ; Allow observers to join the game after it started observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached [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 [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 = "http://feedback.wildfiregames.com/report/upload/v1/" [view] ; Camera control settings scroll.speed = 120.0 scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed rotate.x.speed = 1.2 rotate.x.min = 28.0 rotate.x.max = 60.0 rotate.x.default = 35.0 rotate.y.speed = 2.0 rotate.y.speed.wheel = 0.45 rotate.y.default = 0.0 rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed drag.speed = 0.5 zoom.speed = 256.0 zoom.speed.wheel = 32.0 zoom.min = 50.0 zoom.max = 200.0 zoom.default = 120.0 zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed pos.smoothness = 0.1 zoom.smoothness = 0.4 rotate.x.smoothness = 0.5 rotate.y.smoothness = 0.3 near = 2.0 ; Near plane distance far = 4096.0 ; Far plane distance fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide height.smoothness = 0.5 height.min = 16 Index: ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 19271) +++ ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 19272) @@ -1,223 +1,224 @@ { "Title": "Programming", "Content": [ { "Title": "Programming managers", "List": [ {"nick": "Acumen", "name": "Stuart Walpole"}, {"nick": "Dak Lozar", "name": "Dave Loeser"}, {"nick": "h20", "name": "Daniel Wilhelm"}, {"nick": "Janwas", "name": "Jan Wassenberg"}, {"nick": "Raj", "name": "Raj Sharma"} ] }, { "Subtitle": "Special thanks to", "List": [{"nick": "Ykkrosh", "name": "Philip Taylor"}] }, { "List": [ {"nick": "01d55"}, {"nick": "Acumen", "name": "Stuart Walpole"}, {"name": "Adrian Fatol"}, {"nick": "AI-Amsterdam"}, {"nick": "Alan", "name": "Alan Kemp"}, {"nick": "aBothe", "name": "Alexander Bothe"}, {"nick": "alpha123", "name": "Peter P. Cannici"}, {"nick": "andy5995", "name": "Andy Alt"}, {"nick": "ArnH", "name": "Arno Hemelhof"}, {"nick": "Aurium", "name": "Aurélio Heckert"}, {"nick": "badmadblacksad", "name": "Martin F"}, {"name": "Mikołaj \"Bajter\" Korcz"}, {"nick": "bb", "name": "Bouke Jansen"}, {"nick": "Ben", "name": "Ben Vinegar"}, {"nick": "Bird"}, {"nick": "Blue", "name": "Richard Welsh"}, {"nick": "bmwiedemann"}, {"nick": "boeseRaupe", "name": "Michael Kluge"}, {"nick": "bog_dan_ro", "name": "BogDan Vatra"}, {"nick": "Bonk", "name": "Christopher Ebbert"}, {"nick": "Caius", "name": "Lars Kemmann"}, {"nick": "Calefaction", "name": "Matt Holmes"}, {"nick": "Calvinh", "name": "Carl-Johan Höiby"}, {"nick": "causative", "name": "Bart Parkis"}, {"name": "Cédric Houbart"}, {"nick": "Chakakhan", "name": "Kenny Long"}, {"nick": "Clockwork-Muse", "name": "Stephen A. Imhoff"}, {"nick": "Cracker78", "name": "Chad Heim"}, {"nick": "Crynux", "name": "Stephen J. Fewer"}, {"nick": "cwprogger"}, {"nick": "Dak Lozar", "name": "Dave Loeser"}, {"nick": "dalerank", "name": "Sergey Kushnirenko"}, {"nick": "dan", "name": "Dan Strandberg"}, {"name": "Daniel Trevitz"}, {"nick": "DanCar", "name": "Daniel Cardenas"}, {"nick": "danger89", "name": "Melroy van den Berg"}, {"nick": "Dave", "name": "David Protasowski"}, {"nick": "dax", "name": "Dacian Fiordean"}, {"nick": "deebee", "name": "Deepak Anthony"}, {"nick": "Deiz"}, {"nick": "Dietger", "name": "Dietger van Antwerpen"}, {"nick": "dpiquet", "name": "Damien Piquet"}, {"nick": "dumbo"}, {"nick": "dvangennip", "name": "Doménique"}, {"nick": "Echelon9", "name": "Rhys Kidd"}, {"nick": "echotangoecho"}, {"nick": "eihrul", "name": "Lee Salzman"}, {"nick": "elexis", "name": "Alexander Heinsius"}, {"nick": "EmjeR", "name": "Matthijs de Rijk"}, {"nick": "EMontana"}, {"nick": "ericb"}, {"nick": "evanssthomas", "name": "Evans Thomas"}, {"nick": "Evulant", "name": "Alexander S."}, {"nick": "fabio", "name": "Fabio Pedretti"}, {"nick": "falsevision", "name": "Mahdi Khodadadifard"}, {"nick": "fatherbushido", "name": "Nicolas Tisserand"}, {"nick": "fcxSanya", "name": "Alexander Olkhovskiy"}, {"nick": "FeXoR", "name": "Florian Finke"}, {"nick": "Fire Giant", "name": "Malte Schwarzkopf"}, {"nick": "fpre", "name": "Frederick Stallmeyer"}, {"nick": "freenity", "name": "Anton Galitch"}, {"nick": "gbish (aka Iny)", "name": "Grant Bishop"}, {"nick": "Gee", "name": "Gustav Larsson"}, {"nick": "gerbilOFdoom"}, {"nick": "godlikeldh"}, {"nick": "greybeard", "name": "Joe Cocovich"}, {"nick": "grillaz"}, {"nick": "gudo"}, {"nick": "Guuts", "name": "Matthew Guttag"}, {"name": "Samuel Guarnieri"}, {"nick": "Hannibal_Baraq", "name": "Clive Juhász S."}, {"nick": "Haommin"}, {"nick": "h20", "name": "Daniel Wilhelm"}, {"nick": "historic_bruno", "name": "Ben Brian"}, {"nick": "idanwin"}, {"nick": "Imarok", "name": "J. S."}, {"nick": "infyquest", "name": "Vijay Kiran Kamuju"}, {"nick": "IronNerd", "name": "Matthew McMullan"}, {"nick": "Itms", "name": "Nicolas Auvray"}, {"nick": "Jaison", "name": "Marco tom Suden"}, {"nick": "jammus", "name": "James Scott"}, {"nick": "javiergodas", "name": "Javier Godas Vieitez"}, {"nick": "Jgwman"}, {"nick": "JonBaer", "name": "Jon Baer"}, {"nick": "Josh", "name": "Joshua J. Bakita"}, {"nick": "jP_wanN", "name": "Jonas Platte"}, {"nick": "Jubalbarca", "name": "James Baillie"}, {"nick": "JubJub", "name": "Sebastian Vetter"}, {"nick": "jurgemaister"}, {"nick": "kabzerek", "name": "Grzegorz Kabza"}, {"nick": "Kai", "name": "Kai Chen"}, {"name": "Kareem Ergawy"}, {"nick": "kevmo", "name": "Kevin Caffrey"}, {"nick": "kezz", "name": "Graeme Kerry"}, {"nick": "kingadami", "name": "Adam Winsor"}, {"nick": "kingbasil", "name": "Giannis Fafalios"}, {"nick": "lafferjm", "name": "Justin Lafferty"}, {"nick": "LeanderH", "name": "Leander Hemelhof"}, {"nick": "leper", "name": "Georg Kilzer"}, {"nick": "LittleDev"}, {"nick": "livingaftermidnight", "name": "Will Dull"}, {"nick": "Louhike"}, {"nick": "lsdh"}, {"nick": "madmax", "name": "Abhijit Nandy"}, {"nick": "m0l0t0ph", "name": "Christoph Gielisch"}, {"nick": "markcho"}, {"nick": "MarkT", "name": "Mark Thompson"}, {"nick": "Markus"}, {"nick": "Matei", "name": "Matei Zaharia"}, {"nick": "Mate-86", "name": "Mate Kovacs"}, {"nick": "MattDoerksen", "name": "Matt Doerksen"}, {"nick": "mattlott", "name": "Matt Lott"}, {"nick": "maveric", "name": "Anton Protko"}, {"nick": "Micnasty", "name": "Travis Gorkin"}, {"nick": "mimo"}, {"nick": "mk12", "name": "Mitchell Kember"}, {"nick": "Molotov", "name": "Dario Alvarez"}, {"nick": "mpmoreti", "name": "Marcos Paulo Moreti"}, {"nick": "mreiland", "name": "Michael Reiland"}, {"nick": "myconid"}, {"nick": "nd3c3nt", "name": "Gavin Fowler"}, {"nick": "niektb", "name": "Niek ten Brinke"}, {"nick": "njm"}, {"nick": "NoMonkey", "name": "John Mena"}, {"nick": "notpete", "name": "Rich Cross"}, {"nick": "Offensive ePeen", "name": "Jared Ryan Bills"}, {"nick": "Ols", "name": "Oliver Whiteman"}, {"nick": "olsner", "name": "Simon Brenner"}, + {"nick": "OptimusShepard", "name": "Pirmin Stanglmeier"}, {"nick": "otero"}, {"name": "Nick Owens"}, {"nick": "Palaxin", "name": "David A. Freitag"}, {"name": "Paul Withers"}, {"nick": "paulobezerr", "name": "Paulo George Gomes Bezerra"}, {"nick": "pcpa", "name": "Paulo Andrade"}, {"nick": "Pendingchaos"}, {"nick": "PeteVasi", "name": "Pete Vasiliauskas"}, {"nick": "pilino1234"}, {"nick": "Polakrity"}, {"nick": "Poya", "name": "Poya Manouchehri"}, {"name": "Quentin Pradet"}, {"nick": "prefect", "name": "Nicolai Hähnle"}, {"nick": "pstumpf", "name": "Pascal Stumpf"}, {"name": "André Puel"}, {"nick": "Prodigal Son"}, {"nick": "pyrolink", "name": "Andrew Decker"}, {"nick": "quantumstate", "name": "Jonathan Waller"}, {"nick": "QuickShot", "name": "Walter Krawec"}, {"nick": "quonter"}, {"nick": "qwertz"}, {"nick": "Radagast"}, {"nick": "Raj", "name": "Raj Sharma"}, {"nick": "RedFox", "name": "Jorma Rebane"}, {"nick": "RefinedCode"}, {"nick": "Riemer"}, {"name": "Rolf Sievers"}, {"nick": "s0600204", "name": "Matthew Norwood"}, {"nick": "SafaAlfulaij"}, {"nick": "Sandarac"}, {"nick": "sanderd17", "name": "Sander Deryckere"}, {"nick": "sathyam", "name": "Sathyam Vellal"}, {"nick": "sbirmi", "name": "Sharad Birmiwal"}, {"nick": "sbte", "name": "Sven Baars"}, {"nick": "scroogie", "name": "André Gemünd"}, {"nick": "scythetwirler", "name": "Casey X."}, {"nick": "serveurix"}, {"nick": "Shane", "name": "Shane Grant"}, {"nick": "Silk", "name": "Josh Godsiff"}, {"nick": "silure"}, {"nick": "Simikolon", "name": "Yannick & Simon"}, {"nick": "Spahbod", "name": "Omid Davoodi"}, {"nick": "stanislas69", "name": "Stanislas Dolcini"}, {"nick": "Stefan"}, {"nick": "stilz", "name": "Sławomir Zborowski"}, {"nick": "stwf", "name": "Steven Fuchs"}, {"nick": "svott", "name": "Sven Ott"}, {"nick": "t4nk004"}, {"nick": "tbm", "name": "Martin Michlmayr"}, {"nick": "tau"}, {"nick": "Teiresias"}, {"nick": "texane"}, {"nick": "thamlett", "name": "Timothy Hamlett"}, {"nick": "thedrunkyak", "name": "Dan Fuhr"}, {"nick": "TrinityDeath", "name": "Jethro Lu"}, {"nick": "triumvir", "name": "Corin Schedler"}, {"nick": "trompetin17", "name": "Juan Guillermo"}, {"nick": "usey11"}, {"nick": "vladislavbelov", "name": "Vladislav Belov"}, {"nick": "vts", "name": "Jeroen DR"}, {"nick": "WhiteTreePaladin", "name": "Brian Ashley"}, {"nick": "wraitii", "name": "Lancelot de Ferrière le Vayer"}, {"nick": "Xentelian", "name": "Mark Strawson"}, {"nick": "Xienen", "name": "Dayle Flowers"}, {"nick": "xtizer", "name": "Matt Green"}, {"nick": "yashi", "name": "Yasushi Shoji"}, {"nick": "Ykkrosh", "name": "Philip Taylor"}, {"nick": "Yves"}, {"nick": "Zeusthor", "name": "Jeffrey Tavares"}, {"nick": "zoot"}, {"nick": "zsol", "name": "Zsolt Dollenstein"}, {"nick": "Zyi", "name": "Charles De Meulenaer"} ] } ] } Index: ps/trunk/binaries/data/mods/public/gui/manual/intro.txt =================================================================== --- ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 19271) +++ ps/trunk/binaries/data/mods/public/gui/manual/intro.txt (revision 19272) @@ -1,136 +1,137 @@ [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 configuration file: look for binaries/data/config/default.cfg in the location where the game is installed, which gives instructions for editing, and try disabling the "fancywater" and "shadows" options. [font="sans-bold-16"]Playing the game [font="sans-14"]The controls and gameplay should be familiar to players of traditional 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 AI opponents. The AI/AIs are under development and may not always be up to date on the new features, but you can test playing the game against an actual opponent if you aren't able to play with a human. • [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 civilisations • [font="sans-bold-14"]Skirmish map[font="sans-14"] — A map created by map designers but where the civilisations 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-bold-14"]Global [font="sans-14"]Alt + F4: Close the game, without 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 (6400px*4800px, 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) [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 units/buildings Shift + Delete: Delete currently selected units/buildings without confirmation / (ForwardSlash): Select idle fighter Shift + /: add idle fighter to selection . (Period): Select idle worker (including citizen soldiers) Shift + .: add idle worker to selection (including citizen soldiers) 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 units/buildings 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) [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 Shift + Left click when training unit/s: Add units in batches of five 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 I + Left Drag over units on map: Only select idle units Ctrl + Left Click on unit/group icon with multiple units selected: Deselect Right Click with a building/buildings selected: sets a rally point for units created/ungarrisoned from that building. Ctrl + Right Click with units selected: - If the cursor is over an allied structure: Garrison - If the cursor is over a non-allied unit or building: Attack (instead of capture or gather) - Otherwise: Attack move (by default all enemy units and structures along the way are targeted, use Ctrl + Q + Right Click to target only units). [font="sans-bold-14"]Overlays [font="sans-14"]Alt + G: Hide/show the GUI Alt + D: Show/hide developer overlay (with developer options) Alt + 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 + S: Toggle unit silhouettes (might give a small performance boost) Alt + Z: Toggle sky [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 \[up]: Rotate camera to look upward Ctrl + S or \[down]: Rotate camera to look downward Ctrl + A or \[left]: Rotate camera clockwise around terrain Ctrl + D or \[right]: Rotate camera anticlockwise around terrain Q: Rotate camera clockwise around terrain E: Rotate camera anticlockwise around terrain Shift + Mouse Wheel Rotate Up: Rotate camera clockwise around terrain Shift + Mouse Wheel Rotate Down: Rotate camera anticlockwise around terrain F: Follow the selected unit (move the camera to stop the camera from following the unit/s) R: Reset camera zoom/orientation +: Zoom in (keep pressed for continuous zoom) -: Zoom out (keep pressed for continuous zoom) Alt + W: Toggle through wireframe modes Middle Mouse Button: Keep pressed and move the mouse to pan [font="sans-bold-14"]During Building Placement [font="sans-14"]\[: Rotate building 15 degrees counter-clockwise ]: 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"]Delete: delete the selected saved game asking confirmation Shift: do not ask confirmation when deleting a saved game Index: ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 19271) +++ ps/trunk/binaries/data/mods/public/gui/session/hotkeys/misc.xml (revision 19272) @@ -1,84 +1,88 @@ closeOpenDialogs(); - openChat(false); + openChat(); - openChat(true); + openChat(g_IsObserver ? "/observers" : "/allies"); + + + + openChat(g_LastChatAddressee); toggleMenu(); var newSetting = !Engine.Renderer_GetSilhouettesEnabled(); Engine.Renderer_SetSilhouettesEnabled(newSetting); var newSetting = !Engine.Renderer_GetShowSkyEnabled(); Engine.Renderer_SetShowSkyEnabled(newSetting); togglePause(); Engine.QuickSave(); Engine.QuickLoad(); let ent = g_Selection.getFirstSelected(); if (ent) performCommand(GetExtendedEntityState(ent), "delete"); unloadAll(); stopUnits(g_Selection.toList()); backToWork(); updateSelectionDetails(); updateSelectionDetails(); updateSelectionDetails(); updateSelectionDetails(); findIdleUnit(g_MilitaryTypes); clearSelection(); Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 19271) +++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 19272) @@ -1,1008 +1,1007 @@ // Menu / panel border size const MARGIN = 4; // Includes the main menu button const NUM_BUTTONS = 9; // Regular menu buttons const BUTTON_HEIGHT = 32; // The position where the bottom of the menu will end up (currently 228) const END_MENU_POSITION = (BUTTON_HEIGHT * NUM_BUTTONS) + MARGIN; // Menu starting position: bottom const MENU_BOTTOM = 0; // Menu starting position: top const MENU_TOP = MENU_BOTTOM - END_MENU_POSITION; // Menu starting position: overall const INITIAL_MENU_POSITION = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM; // Number of pixels per millisecond to move const MENU_SPEED = 1.2; // Trade menu: step for probability changes const STEP = 5; // Shown in the trade dialog. const g_IdleTraderTextColor = "orange"; var g_IsMenuOpen = false; var g_IsDiplomacyOpen = false; var g_IsTradeOpen = false; var g_IsObjectivesOpen = false; // Redefined every time someone makes a tribute (so we can save some data in a closure). Called in input.js handleInputBeforeGui. var g_FlushTributing = function() {}; // Ignore size defined in XML and set the actual menu size here function initMenuPosition() { Engine.GetGUIObjectByName("menu").size = INITIAL_MENU_POSITION; } function updateMenuPosition(dt) { let menu = Engine.GetGUIObjectByName("menu"); let maxOffset = g_IsMenuOpen ? END_MENU_POSITION - menu.size.bottom : menu.size.top - MENU_TOP; if (maxOffset <= 0) return; let offset = Math.min(MENU_SPEED * dt, maxOffset) * (g_IsMenuOpen ? +1 : -1); let size = menu.size; size.top += offset; size.bottom += offset; menu.size = size; } // Opens the menu by revealing the screen which contains the menu function openMenu() { g_IsMenuOpen = true; } // Closes the menu and resets position function closeMenu() { g_IsMenuOpen = false; } function toggleMenu() { g_IsMenuOpen = !g_IsMenuOpen; } function optionsMenuButton() { closeOpenDialogs(); openOptions(); } function chatMenuButton() { closeOpenDialogs(); openChat(); } function diplomacyMenuButton() { closeOpenDialogs(); openDiplomacy(); } function pauseMenuButton() { togglePause(); } function resignMenuButton() { closeOpenDialogs(); pauseGame(); messageBox( 400, 200, translate("Are you sure you want to resign?"), translate("Confirmation"), [translate("No"), translate("Yes")], [resumeGame, resignGame] ); } function exitMenuButton() { closeOpenDialogs(); pauseGame(); let messageTypes = { "host": { "caption": translate("Are you sure you want to quit? Leaving will disconnect all other players."), "buttons": [resumeGame, leaveGame] }, "client": { "caption": translate("Are you sure you want to quit?"), "buttons": [resumeGame, resignQuestion] }, "singleplayer": { "caption": translate("Are you sure you want to quit?"), "buttons": [resumeGame, leaveGame] } }; let messageType = g_IsNetworked && g_IsController ? "host" : (g_IsNetworked && !g_IsObserver ? "client" : "singleplayer"); messageBox( 400, 200, messageTypes[messageType].caption, translate("Confirmation"), [translate("No"), translate("Yes")], messageTypes[messageType].buttons ); } function resignQuestion() { messageBox( 400, 200, translate("Do you want to resign or will you return soon?"), translate("Confirmation"), [translate("I will return"), translate("I resign")], [leaveGame, resignGame], [true, false] ); } function openDeleteDialog(selection) { closeOpenDialogs(); let deleteSelectedEntities = function (selectionArg) { Engine.PostNetworkCommand({ "type": "delete-entities", "entities": selectionArg }); }; messageBox( 400, 200, translate("Destroy everything currently selected?"), translate("Delete"), [translate("No"), translate("Yes")], [resumeGame, deleteSelectedEntities], [null, selection] ); } function openSave() { closeOpenDialogs(); pauseGame(); Engine.PushGuiPage("page_savegame.xml", { "savedGameData": getSavedGameData(), "callback": "resumeGame" }); } function openOptions() { closeOpenDialogs(); pauseGame(); Engine.PushGuiPage("page_options.xml", { "callback": "resumeGame" }); } -function openChat(teamChat = false) +function openChat(command = "") { if (g_Disconnected) return; closeOpenDialogs(); let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); - let command = teamChat ? (g_IsObserver ? "/observers" : "/allies") : ""; chatAddressee.selected = chatAddressee.list_data.indexOf(command); Engine.GetGUIObjectByName("chatInput").focus(); Engine.GetGUIObjectByName("chatDialogPanel").hidden = false; updateChatHistory(); } function closeChat() { Engine.GetGUIObjectByName("chatInput").caption = ""; Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus Engine.GetGUIObjectByName("chatDialogPanel").hidden = true; } function resizeDiplomacyDialog() { let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel"); let size = dialog.size; let width = size.right - size.left; let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size; width += g_ResourceData.GetCodes().length * (tribSize.right - tribSize.left); size.left = -width / 2; size.right = width / 2; dialog.size = size; } function initChatWindow() { let filters = prepareForDropdown(g_ChatHistoryFilters); let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter"); chatHistoryFilter.list = filters.text; chatHistoryFilter.list_data = filters.key; chatHistoryFilter.selected = 0; Engine.GetGUIObjectByName("extendedChat").checked = Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true"; resizeChatWindow(); } function resizeChatWindow() { // Hide/show the panel let chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage"); let extended = Engine.GetGUIObjectByName("extendedChat").checked; chatHistoryPage.hidden = !extended; // Resize the window let chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel"); let chatPage = Engine.GetGUIObjectByName("chatPage"); let panelSize = chatDialogPanel.size; let topOffset = 80; let height = -chatPage.size.top + (extended ? chatHistoryPage.size.bottom : 0); panelSize.top = -height / 2 - topOffset; panelSize.bottom = height / 2 - topOffset; chatDialogPanel.size = panelSize; } function updateChatHistory() { if (Engine.GetGUIObjectByName("chatDialogPanel").hidden || !Engine.GetGUIObjectByName("extendedChat").checked) return; let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter"); let selected = chatHistoryFilter.list_data[chatHistoryFilter.selected]; Engine.GetGUIObjectByName("chatHistory").caption = g_ChatHistory.filter(msg => msg.filter[selected]).map(msg => Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ? sprintf(translate("%(time)s %(message)s"), { "time": msg.timePrefix, "message": msg.txt }) : msg.txt ).join("\n"); } function onToggleChatWindowExtended() { // Save user preference let extended = Engine.GetGUIObjectByName("extendedChat").checked.toString(); Engine.ConfigDB_CreateValue("user", "chat.session.extended", extended); Engine.ConfigDB_WriteValueToFile("user", "chat.session.extended", extended, "config/user.cfg"); resizeChatWindow(); Engine.GetGUIObjectByName("chatInput").focus(); } function openDiplomacy() { closeOpenDialogs(); if (g_ViewedPlayer < 1) return; g_IsDiplomacyOpen = true; updateDiplomacy(true); Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false; } function closeDiplomacy() { g_IsDiplomacyOpen = false; Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = true; } function toggleDiplomacy() { let open = g_IsDiplomacyOpen; closeOpenDialogs(); if (!open) openDiplomacy(); } function updateDiplomacy(opening = false) { if (g_ViewedPlayer < 1 || !g_IsDiplomacyOpen) return; let simState = GetSimState(); let isCeasefireActive = simState.ceasefireActive; let hasSharedLos = GetSimState().players[g_ViewedPlayer].hasSharedLos; // Get offset for one line let onesize = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size; let rowsize = onesize.bottom - onesize.top; // We don't include gaia for (let i = 1; i < g_Players.length; ++i) { let myself = i == g_ViewedPlayer; let playerInactive = isPlayerObserver(g_ViewedPlayer) || isPlayerObserver(i); let hasAllies = g_Players.filter(player => player.isMutualAlly[g_ViewedPlayer]).length > 1; diplomacySetupTexts(i, rowsize); diplomacyFormatStanceButtons(i, myself || playerInactive || isCeasefireActive || g_Players[g_ViewedPlayer].teamsLocked); // Tribute buttons do not need to be updated onTick, and should not because of massTributing if (opening) diplomacyFormatTributeButtons(i, myself || playerInactive); diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]); diplomacyFormatSpyRequestButton(i, myself || playerInactive || g_Players[i].isMutualAlly[g_ViewedPlayer] && hasSharedLos); } let diplomacyCeasefireCounter = Engine.GetGUIObjectByName("diplomacyCeasefireCounter"); diplomacyCeasefireCounter.caption = sprintf( translateWithContext("ceasefire", "Time remaining until ceasefire is over: %(time)s."), { "time": timeToString(simState.ceasefireTimeRemaining) } ); diplomacyCeasefireCounter.hidden = !isCeasefireActive; } function diplomacySetupTexts(i, rowsize) { // Apply offset let row = Engine.GetGUIObjectByName("diplomacyPlayer["+(i-1)+"]"); let size = row.size; size.top = rowsize * (i-1); size.bottom = rowsize * i; row.size = size; row.hidden = false; row.sprite = "color: " + rgbToGuiColor(g_Players[i].color) + " 32"; setOutcomeIcon(g_Players[i].state, "diplomacyPlayerOutcome["+(i-1)+"]"); Engine.GetGUIObjectByName("diplomacyPlayerName["+(i-1)+"]").caption = colorizePlayernameByID(i); Engine.GetGUIObjectByName("diplomacyPlayerCiv["+(i-1)+"]").caption = g_CivData[g_Players[i].civ].Name; Engine.GetGUIObjectByName("diplomacyPlayerTeam["+(i-1)+"]").caption = g_Players[i].team < 0 ? translateWithContext("team", "None") : g_Players[i].team+1; Engine.GetGUIObjectByName("diplomacyPlayerTheirs["+(i-1)+"]").caption = i == g_ViewedPlayer ? "" : g_Players[i].isAlly[g_ViewedPlayer] ? translate("Ally") : g_Players[i].isNeutral[g_ViewedPlayer] ? translate("Neutral") : translate("Enemy"); } function diplomacyFormatStanceButtons(i, hidden) { for (let stance of ["Ally", "Neutral", "Enemy"]) { let button = Engine.GetGUIObjectByName("diplomacyPlayer"+stance+"["+(i-1)+"]"); button.hidden = hidden; if (hidden) continue; button.caption = g_Players[g_ViewedPlayer]["is" + stance][i] ? translate("x") : ""; button.enabled = controlsPlayer(g_ViewedPlayer); button.onPress = (function(player, stance) { return function() { Engine.PostNetworkCommand({ "type": "diplomacy", "player": i, "to": stance.toLowerCase() }); }; })(i, stance); } } function diplomacyFormatTributeButtons(i, hidden) { let resNames = g_ResourceData.GetNames(); let resCodes = g_ResourceData.GetCodes(); let r = 0; for (let resCode of resCodes) { let button = Engine.GetGUIObjectByName("diplomacyPlayer["+(i-1)+"]_tribute["+r+"]"); if (!button) { warn("Current GUI limits prevent displaying more than " + r + " tribute buttons!"); break; } Engine.GetGUIObjectByName("diplomacyPlayer["+(i-1)+"]_tribute["+r+"]_image").sprite = "stretched:session/icons/resources/"+resCode+".png"; button.hidden = hidden; setPanelObjectPosition(button, r, r+1, 0); ++r; if (hidden) continue; button.enabled = controlsPlayer(g_ViewedPlayer); button.tooltip = formatTributeTooltip(i, resNames[resCode], 100); button.onPress = (function(i, resCode, button) { // Shift+click to send 500, shift+click+click to send 1000, etc. // See INPUT_MASSTRIBUTING in input.js let multiplier = 1; return function() { let isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute"); if (isBatchTrainPressed) { inputState = INPUT_MASSTRIBUTING; multiplier += multiplier == 1 ? 4 : 5; } let amounts = {}; for (let res of resCodes) amounts[res] = 0; amounts[resCode] = 100 * multiplier; button.tooltip = formatTributeTooltip(i, resNames[resCode], amounts[resCode]); // This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some // evil global variable hackery. g_FlushTributing = function() { Engine.PostNetworkCommand({ "type": "tribute", "player": i, "amounts": amounts }); multiplier = 1; button.tooltip = formatTributeTooltip(i, resNames[resCode], 100); }; if (!isBatchTrainPressed) g_FlushTributing(); }; })(i, resCode, button); } } function diplomacyFormatAttackRequestButton(i, hidden) { let button = Engine.GetGUIObjectByName("diplomacyAttackRequest["+(i-1)+"]"); button.hidden = hidden; if (hidden) return; button.enabled = controlsPlayer(g_ViewedPlayer); button.tooltip = translate("Request your allies to attack this enemy"); button.onPress = (function(i) { return function() { Engine.PostNetworkCommand({ "type": "attack-request", "source": g_ViewedPlayer, "player": i }); }; })(i); } function diplomacyFormatSpyRequestButton(i, hidden) { let button = Engine.GetGUIObjectByName("diplomacySpyRequest["+(i-1)+"]"); let template = GetTemplateData("special/spy"); button.hidden = hidden || !template || GetSimState().players[g_ViewedPlayer].disabledTemplates["special/spy"]; if (button.hidden) return; button.enabled = controlsPlayer(g_ViewedPlayer); let modifier = ""; let tooltips = [translate("Bribe a random unit from this player and share its vision during a limited period.")]; if (!button.enabled) modifier = "color:0 0 0 127:grayscale:"; else { if (template.requiredTechnology) { let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", { "tech": template.requiredTechnology, "player": g_ViewedPlayer }); if (!technologyEnabled) { modifier = "color:0 0 0 127:grayscale:"; button.enabled = false; tooltips.push(getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[g_ViewedPlayer].civ)); } } if (template.cost) { let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": template.cost, "player": g_ViewedPlayer }); if (neededResources) { if (button.enabled) modifier = resourcesToAlphaMask(neededResources) +":"; button.enabled = false; tooltips.push(getNeededResourcesTooltip(neededResources)); } } } let icon = Engine.GetGUIObjectByName("diplomacySpyRequestImage["+(i-1)+"]"); icon.sprite = modifier + "stretched:session/icons/economics.png"; button.tooltip = tooltips.filter(tip => tip).join("\n"); button.onPress = (function(i) { return function() { Engine.PostNetworkCommand({ "type": "spy-request", "source": g_ViewedPlayer, "player": i }); closeDiplomacy(); }; })(i); } function resizeTradeDialog() { let dialog = Engine.GetGUIObjectByName("tradeDialogPanel"); let size = dialog.size; let width = size.right - size.left; let tradeSize = Engine.GetGUIObjectByName("tradeResource[0]").size; width += g_ResourceData.GetCodes().length * (tradeSize.right - tradeSize.left); size.left = -width / 2; size.right = width / 2; dialog.size = size; } function openTrade() { closeOpenDialogs(); if (g_ViewedPlayer < 1) return; g_IsTradeOpen = true; var updateButtons = function() { for (let res in button) { button[res].label.caption = proba[res] + "%"; button[res].sel.hidden = !controlsPlayer(g_ViewedPlayer) || res != selec; button[res].up.hidden = !controlsPlayer(g_ViewedPlayer) || res == selec || proba[res] == 100 || proba[selec] == 0; button[res].dn.hidden = !controlsPlayer(g_ViewedPlayer) || res == selec || proba[res] == 0 || proba[selec] == 100; } }; let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer); let button = {}; let resCodes = g_ResourceData.GetCodes(); let selec = resCodes[0]; hideRemaining("tradeResources", resCodes.length); Engine.GetGUIObjectByName("tradeHelp").hidden = false; for (let i = 0; i < resCodes.length; ++i) { let tradeResource = Engine.GetGUIObjectByName("tradeResource["+i+"]"); if (!tradeResource) { warn("Current GUI limits prevent displaying more than " + r + " resources in the trading goods selection dialog!"); break; } setPanelObjectPosition(tradeResource, i, i+1); let resCode = resCodes[i]; proba[resCode] = proba[resCode] || 0; let icon = Engine.GetGUIObjectByName("tradeResourceIcon["+i+"]"); icon.sprite = "stretched:session/icons/resources/" + resCode + ".png"; let buttonUp = Engine.GetGUIObjectByName("tradeArrowUp["+i+"]"); let buttonDn = Engine.GetGUIObjectByName("tradeArrowDn["+i+"]"); button[resCode] = { "up": buttonUp, "dn": buttonDn, "label": Engine.GetGUIObjectByName("tradeResourceText["+i+"]"), "sel": Engine.GetGUIObjectByName("tradeResourceSelection["+i+"]") }; let buttonResource = Engine.GetGUIObjectByName("tradeResourceButton["+i+"]"); buttonResource.enabled = controlsPlayer(g_ViewedPlayer); buttonResource.onPress = (function(resource){ return function() { if (Engine.HotkeyIsPressed("session.fulltradeswap")) { for (let res of resCodes) proba[res] = 0; proba[resource] = 100; Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba}); } selec = resource; updateButtons(); }; })(resCode); buttonUp.enabled = controlsPlayer(g_ViewedPlayer); buttonUp.onPress = (function(resource){ return function() { proba[resource] += Math.min(STEP, proba[selec]); proba[selec] -= Math.min(STEP, proba[selec]); Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba}); updateButtons(); }; })(resCode); buttonDn.enabled = controlsPlayer(g_ViewedPlayer); buttonDn.onPress = (function(resource){ return function() { proba[selec] += Math.min(STEP, proba[resource]); proba[resource] -= Math.min(STEP, proba[resource]); Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba}); updateButtons(); }; })(resCode); } updateButtons(); let traderNumber = Engine.GuiInterfaceCall("GetTraderNumber", g_ViewedPlayer); Engine.GetGUIObjectByName("landTraders").caption = getIdleLandTradersText(traderNumber); Engine.GetGUIObjectByName("shipTraders").caption = getIdleShipTradersText(traderNumber); Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false; } function getIdleLandTradersText(traderNumber) { let active = traderNumber.landTrader.trading; let garrisoned = traderNumber.landTrader.garrisoned; let inactive = traderNumber.landTrader.total - active - garrisoned; let messageTypes = { "active": { "garrisoned": { "no-inactive": translate("%(openingTradingString)s, and %(garrisonedString)s."), "inactive": translate("%(openingTradingString)s, %(garrisonedString)s, and %(inactiveString)s.") }, "no-garrisoned": { "no-inactive": translate("%(openingTradingString)s."), "inactive": translate("%(openingTradingString)s, and %(inactiveString)s.") } }, "no-active": { "garrisoned": { "no-inactive": translate("%(openingGarrisonedString)s."), "inactive": translate("%(openingGarrisonedString)s, and %(inactiveString)s.") }, "no-garrisoned": { "inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive), "no-inactive": translate("There are no land traders.") } } }; let message = messageTypes[active ? "active" : "no-active"][garrisoned ? "garrisoned" : "no-garrisoned"][inactive ? "inactive" : "no-inactive"]; let activeString = sprintf( translatePlural( "There is %(numberTrading)s land trader trading", "There are %(numberTrading)s land traders trading", active ), { "numberTrading": active } ); let inactiveString = sprintf(active || garrisoned ? translatePlural( "%(numberOfLandTraders)s inactive", "%(numberOfLandTraders)s inactive", inactive ) : translatePlural( "%(numberOfLandTraders)s land trader inactive", "%(numberOfLandTraders)s land traders inactive", inactive ), { "numberOfLandTraders": inactive } ); let garrisonedString = sprintf(active || inactive ? translatePlural( "%(numberGarrisoned)s garrisoned on a trading merchant ship", "%(numberGarrisoned)s garrisoned on a trading merchant ship", garrisoned ) : translatePlural( "There is %(numberGarrisoned)s land trader garrisoned on a trading merchant ship", "There are %(numberGarrisoned)s land traders garrisoned on a trading merchant ship", garrisoned ), { "numberGarrisoned": garrisoned } ); return sprintf(message, { "openingTradingString": activeString, "openingGarrisonedString": garrisonedString, "garrisonedString": garrisonedString, "inactiveString": "[color=\"" + g_IdleTraderTextColor + "\"]" + inactiveString + "[/color]" }); } function getIdleShipTradersText(traderNumber) { let active = traderNumber.shipTrader.trading; let inactive = traderNumber.shipTrader.total - active; let messageTypes = { "active": { "inactive": translate("%(openingTradingString)s, and %(inactiveString)s."), "no-inactive": translate("%(openingTradingString)s.") }, "no-active": { "inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive), "no-inactive": translate("There are no merchant ships.") } }; let message = messageTypes[active ? "active" : "no-active"][inactive ? "inactive" : "no-inactive"]; let activeString = sprintf( translatePlural( "There is %(numberTrading)s merchant ship trading", "There are %(numberTrading)s merchant ships trading", active ), { "numberTrading": active } ); let inactiveString = sprintf(active ? translatePlural( "%(numberOfShipTraders)s inactive", "%(numberOfShipTraders)s inactive", inactive ) : translatePlural( "%(numberOfShipTraders)s merchant ship inactive", "%(numberOfShipTraders)s merchant ships inactive", inactive ), { "numberOfShipTraders": inactive } ); return sprintf(message, { "openingTradingString": activeString, "inactiveString": "[color=\"" + g_IdleTraderTextColor + "\"]" + inactiveString + "[/color]" }); } function closeTrade() { g_IsTradeOpen = false; Engine.GetGUIObjectByName("tradeDialogPanel").hidden = true; } function toggleTrade() { let open = g_IsTradeOpen; closeOpenDialogs(); if (!open) openTrade(); } function toggleGameSpeed() { let gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.hidden = !gameSpeed.hidden; } function toggleObjectives() { let open = g_IsObjectivesOpen; closeOpenDialogs(); if (!open) openObjectives(); } function openObjectives() { g_IsObjectivesOpen = true; let player = g_Players[Engine.GetPlayerID()]; let playerState = player && player.state; let isActive = !playerState || playerState == "active"; Engine.GetGUIObjectByName("gameDescriptionText").caption = getGameDescription(true); let objectivesPlayerstate = Engine.GetGUIObjectByName("objectivesPlayerstate"); objectivesPlayerstate.hidden = isActive; objectivesPlayerstate.caption = g_PlayerStateMessages[playerState] || ""; let gameDescription = Engine.GetGUIObjectByName("gameDescription"); let gameDescriptionSize = gameDescription.size; gameDescriptionSize.top = Engine.GetGUIObjectByName( isActive ? "objectivesTitle" : "objectivesPlayerstate").size.bottom; gameDescription.size = gameDescriptionSize; Engine.GetGUIObjectByName("objectivesPanel").hidden = false; } function closeObjectives() { g_IsObjectivesOpen = false; Engine.GetGUIObjectByName("objectivesPanel").hidden = true; } /** * Allows players to see their own summary. * If they have shared ally vision researched, they are able to see the summary of there allies too. */ function openGameSummary() { closeOpenDialogs(); pauseGame(); let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); Engine.PushGuiPage("page_summary.xml", { "sim": { "mapSettings": g_GameAttributes.settings, "playerStates":extendedSimState.players.filter((state,player) => g_IsObserver || player == 0 || player == g_ViewedPlayer || extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]), "timeElapsed" : extendedSimState.timeElapsed }, "gui": { "isInGame": true }, "callback": "resumeGame" }); } function openStrucTree() { closeOpenDialogs(); pauseGame(); // TODO add info about researched techs and unlocked entities Engine.PushGuiPage("page_structree.xml", { "civ" : g_Players[g_ViewedPlayer].civ, "callback": "resumeGame", }); } /** * Pause or resume the game. * * @param explicit - true if the player explicitly wants to pause or resume. * If this argument isn't set, a multiplayer game won't be paused and the pause overlay * won't be shown in single player. */ function pauseGame(pause = true, explicit = false) { if (g_IsNetworked && !explicit) return; if (explicit) g_Paused = pause; Engine.SetPaused(g_Paused || pause, !!explicit); if (g_IsNetworked) { setClientPauseState(Engine.GetPlayerGUID(), g_Paused); return; } updatePauseOverlay(); } function resumeGame(explicit = false) { pauseGame(false, explicit); } /** * Called when the current player toggles a pause button. */ function togglePause() { if (!Engine.GetGUIObjectByName("pauseButton").enabled) return; closeOpenDialogs(); pauseGame(!g_Paused, true); } /** * Called when a client pauses or resumes in a multiplayer game. */ function setClientPauseState(guid, paused) { // Update the list of pausing clients. let index = g_PausingClients.indexOf(guid); if (paused && index == -1) g_PausingClients.push(guid); else if (!paused && index != -1) g_PausingClients.splice(index, 1); updatePauseOverlay(); Engine.SetPaused(!!g_PausingClients.length, false); } /** * Update the pause overlay. */ function updatePauseOverlay() { Engine.GetGUIObjectByName("pauseButton").caption = g_Paused ? translate("Resume") : translate("Pause"); Engine.GetGUIObjectByName("resumeMessage").hidden = !g_Paused; Engine.GetGUIObjectByName("pausedByText").hidden = !g_IsNetworked; Engine.GetGUIObjectByName("pausedByText").caption = sprintf(translate("Paused by %(players)s"), { "players": g_PausingClients.map(guid => colorizePlayernameByGUID(guid)).join(translate(", ")) }); Engine.GetGUIObjectByName("pauseOverlay").hidden = !(g_Paused || g_PausingClients.length); Engine.GetGUIObjectByName("pauseOverlay").onPress = g_Paused ? togglePause : function() {}; } function openManual() { closeOpenDialogs(); pauseGame(); Engine.PushGuiPage("page_manual.xml", { "page": "manual/intro", "title": translate("Manual"), "url": "http://trac.wildfiregames.com/wiki/0adManual", "callback": "resumeGame" }); } function toggleDeveloperOverlay() { // The developer overlay is disabled in ranked games if (Engine.HasXmppClient() && Engine.IsRankedGame()) return; let devCommands = Engine.GetGUIObjectByName("devCommands"); devCommands.hidden = !devCommands.hidden; let message = devCommands.hidden ? markForTranslation("The Developer Overlay was closed.") : markForTranslation("The Developer Overlay was opened."); Engine.PostNetworkCommand({ "type": "aichat", "message": message, "translateMessage": true, "translateParameters": [], "parameters": {} }); } function closeOpenDialogs() { // TODO: also close message boxes closeMenu(); closeChat(); closeDiplomacy(); closeTrade(); closeObjectives(); } function formatTributeTooltip(playerID, resource, amount) { return sprintf(translate("Tribute %(resourceAmount)s %(resourceType)s to %(playerName)s. Shift-click to tribute %(greaterAmount)s."), { "resourceAmount": amount, "resourceType": getLocalizedResourceName(resource, "withinSentence"), "playerName": colorizePlayernameByID(playerID), "greaterAmount": amount < 500 ? 500 : amount + 500 }); } Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 19271) +++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 19272) @@ -1,1125 +1,1134 @@ /** * All known cheat commands. * @type {Object} */ const g_Cheats = getCheatsData(); /** * Number of seconds after which chatmessages will disappear. */ const g_ChatTimeout = 30; /** * Maximum number of lines to display simultaneously. */ const g_ChatLines = 20; /** * The currently displayed strings, limited by the given timeframe and limit above. */ var g_ChatMessages = []; /** * All unparsed chat messages received since connect, including timestamp. */ var g_ChatHistory = []; /** * Holds the timer-IDs used for hiding the chat after g_ChatTimeout seconds. */ var g_ChatTimers = []; /** + * Command to send to the previously selected private chat partner. + */ +var g_LastChatAddressee = ""; + +/** * Handle all netmessage types that can occur. */ var g_NetMessageTypes = { "netstatus": msg => { handleNetStatusMessage(msg); }, "netwarn": msg => { addNetworkWarning(msg); }, "players": msg => { handlePlayerAssignmentsMessage(msg); }, "paused": msg => { setClientPauseState(msg.guid, msg.pause); }, "rejoined": msg => { addChatMessage({ "type": "rejoined", "guid": msg.guid }); }, "kicked": msg => { addChatMessage({ "type": "kicked", "username": msg.username, "banned": msg.banned }); }, "chat": msg => { addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text }); }, "aichat": msg => { addChatMessage({ "type": "message", "guid": msg.guid, "text": msg.text, "translate": true }); }, "gamesetup": msg => {}, // Needed for autostart "start": msg => {} }; var g_FormatChatMessage = { "system": msg => msg.text, "connect": msg => sprintf( g_PlayerAssignments[msg.guid].player != -1 ? // Translation: A player that left the game joins again translate("%(player)s is starting to rejoin the game.") : // Translation: A player joins the game for the first time translate("%(player)s is starting to join the game."), { "player": colorizePlayernameByGUID(msg.guid) } ), "disconnect": msg => sprintf(translate("%(player)s has left the game."), { "player": colorizePlayernameByGUID(msg.guid) }), "rejoined": msg => sprintf( g_PlayerAssignments[msg.guid].player != -1 ? // Translation: A player that left the game joins again translate("%(player)s has rejoined the game.") : // Translation: A player joins the game for the first time translate("%(player)s has joined the game."), { "player": colorizePlayernameByGUID(msg.guid) } ), "kicked": msg => sprintf( msg.banned ? translate("%(username)s has been banned") : translate("%(username)s has been kicked"), { "username": colorizePlayernameHelper( msg.username, g_Players.findIndex(p => p.name == msg.username) ) } ), "clientlist": msg => getUsernameList(), "message": msg => formatChatCommand(msg), "defeat": msg => formatDefeatMessage(msg), "won": msg => formatWinMessage(msg), "diplomacy": msg => formatDiplomacyMessage(msg), "tribute": msg => formatTributeMessage(msg), "barter": msg => formatBarterMessage(msg), "attack": msg => formatAttackMessage(msg) }; /** * Show a label and grey overlay or hide both on connection change. */ var g_StatusMessageTypes = { "authenticated": msg => translate("Connection to the server has been authenticated."), "connected": msg => translate("Connected to the server."), "disconnected": msg => translate("Connection to the server has been lost.") + "\n" + // Translation: States the reason why the client disconnected from the server. sprintf(translate("Reason: %(reason)s."), { "reason": getDisconnectReason(msg.reason, true) }), "waiting_for_players": msg => translate("Waiting for other players to connect..."), "join_syncing": msg => translate("Synchronising gameplay with other players..."), "active": msg => "" }; /** * Chatmessage shown after commands like /me or /enemies. */ var g_ChatCommands = { "regular": { "context": translate("(%(context)s) %(userTag)s %(message)s"), "no-context": translate("%(userTag)s %(message)s") }, "me": { "context": translate("(%(context)s) * %(user)s %(message)s"), "no-context": translate("* %(user)s %(message)s") } }; var g_ChatAddresseeContext = { "/team": translate("Team"), "/allies": translate("Ally"), "/enemies": translate("Enemy"), "/observers": translate("Observer"), "/msg": translate("Private") }; /** * Returns true if the current player is an addressee, given the chat message type and sender. */ var g_IsChatAddressee = { "/team": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[Engine.GetPlayerID()].team != -1 && g_Players[Engine.GetPlayerID()].team == g_Players[senderID].team, "/allies": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[senderID].isMutualAlly[Engine.GetPlayerID()], "/enemies": senderID => g_Players[senderID] && g_Players[Engine.GetPlayerID()] && g_Players[senderID].isEnemy[Engine.GetPlayerID()], "/observers": senderID => g_IsObserver, "/msg": (senderID, addresseeGUID) => addresseeGUID == Engine.GetPlayerGUID() }; /** * Notice only messages will be filtered that are visible to the player in the first place. */ var g_ChatHistoryFilters = [ { "key": "all", "text": translateWithContext("chat history filter", "Chat and notifications"), "filter": (msg, senderID) => true }, { "key": "chat", "text": translateWithContext("chat history filter", "Chat messages"), "filter": (msg, senderID) => msg.type == "message" }, { "key": "player", "text": translateWithContext("chat history filter", "Players chat"), "filter": (msg, senderID) => msg.type == "message" && senderID > 0 && !isPlayerObserver(senderID) }, { "key": "ally", "text": translateWithContext("chat history filter", "Ally chat"), "filter": (msg, senderID) => msg.type == "message" && msg.cmd && msg.cmd == "/allies" }, { "key": "enemy", "text": translateWithContext("chat history filter", "Enemy chat"), "filter": (msg, senderID) => msg.type == "message" && msg.cmd && msg.cmd == "/enemies" }, { "key": "observer", "text": translateWithContext("chat history filter", "Observer chat"), "filter": (msg, senderID) => msg.type == "message" && msg.cmd && msg.cmd == "/observers" }, { "key": "private", "text": translateWithContext("chat history filter", "Private chat"), "filter": (msg, senderID) => !!msg.isVisiblePM } ]; var g_PlayerStateMessages = { "won": translate("You have won!"), "defeated": translate("You have been defeated!") }; /** * Chatmessage shown on diplomacy change. */ var g_DiplomacyMessages = { "active": { "ally": translate("You are now allied with %(player)s."), "enemy": translate("You are now at war with %(player)s."), "neutral": translate("You are now neutral with %(player)s.") }, "passive": { "ally": translate("%(player)s is now allied with you."), "enemy": translate("%(player)s is now at war with you."), "neutral": translate("%(player)s is now neutral with you.") }, "observer": { "ally": translate("%(player)s is now allied with %(player2)s."), "enemy": translate("%(player)s is now at war with %(player2)s."), "neutral": translate("%(player)s is now neutral with %(player2)s.") } }; /** * Defines how the GUI reacts to notifications that are sent by the simulation. * Don't open new pages (message boxes) here! Otherwise further notifications * handled in the same turn can't access the GUI objects anymore. */ var g_NotificationsTypes = { "chat": function(notification, player) { let message = { "type": "message", "guid": findGuidForPlayerID(player) || -1, "text": notification.message }; if (message.guid == -1) message.player = player; addChatMessage(message); }, "aichat": function(notification, player) { let message = { "type": "message", "text": notification.message, "guid": findGuidForPlayerID(player) || -1, "player": player, "translate": true }; if (notification.translateParameters) { message.translateParameters = notification.translateParameters; message.parameters = notification.parameters; colorizePlayernameParameters(notification.parameters); } addChatMessage(message); }, "defeat": function(notification, player) { addChatMessage({ "type": "defeat", "guid": findGuidForPlayerID(player), "player": player, "resign": !!notification.resign }); playerFinished(player, false); sendLobbyPlayerlistUpdate(); }, "won": function(notification, player) { addChatMessage({ "type": "won", "guid": findGuidForPlayerID(player), "player": player }); playerFinished(player, true); sendLobbyPlayerlistUpdate(); }, "diplomacy": function(notification, player) { addChatMessage({ "type": "diplomacy", "sourcePlayer": player, "targetPlayer": notification.targetPlayer, "status": notification.status }); updatePlayerData(); }, "ceasefire-ended": function(notification, player) { updatePlayerData(); }, "tribute": function(notification, player) { addChatMessage({ "type": "tribute", "sourcePlayer": notification.donator, "targetPlayer": player, "amounts": notification.amounts }); }, "barter": function(notification, player) { addChatMessage({ "type": "barter", "player": player, "amountsSold": notification.amountsSold, "amountsBought": notification.amountsBought, "resourceSold": notification.resourceSold, "resourceBought": notification.resourceBought }); }, "spy-response": function(notification, player) { if (g_ViewedPlayer == player) setCameraFollow(notification.entity); }, "attack": function(notification, player) { if (player != g_ViewedPlayer) return; // Focus camera on attacks if (g_FollowPlayer) { setCameraFollow(notification.target); g_Selection.reset(); if (notification.target) g_Selection.addList([notification.target]); } if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.attack") !== "true") return; addChatMessage({ "type": "attack", "player": player, "attacker": notification.attacker, "targetIsDomesticAnimal": notification.targetIsDomesticAnimal }); }, "dialog": function(notification, player) { if (player == Engine.GetPlayerID()) openDialog(notification.dialogName, notification.data, player); }, "resetselectionpannel": function(notification, player) { if (player != Engine.GetPlayerID()) return; g_Selection.rebuildSelection({}); }, "playercommand": function(notification, player) { // For observers, focus the camera on units commanded by the selected player if (!g_FollowPlayer || player != g_ViewedPlayer) return; let cmd = notification.cmd; // Ignore boring animals let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]); if (entState && entState.identity && entState.identity.classes && entState.identity.classes.indexOf("Animal") != -1) return; // Focus the building to construct if (cmd.type == "repair") { let targetState = GetEntityState(cmd.target); if (targetState) Engine.CameraMoveTo(targetState.position.x, targetState.position.z); } // Focus commanded entities, but don't lose previous focus when training units else if (cmd.type != "train" && cmd.type != "research" && entState) setCameraFollow(cmd.entities[0]); // Select units affected by that command let selection = []; if (cmd.entities) selection = cmd.entities; if (cmd.target) selection.push(cmd.target); // Allow gaia in selection when gathering g_Selection.reset(); g_Selection.addList(selection, false, cmd.type == "gather"); } }; /** * Loads all known cheat commands. * * @returns {Object} */ function getCheatsData() { let cheats = {}; for (let fileName of getJSONFileList("simulation/data/cheats/")) { let currentCheat = Engine.ReadJSONFile("simulation/data/cheats/"+fileName+".json"); if (!currentCheat) continue; if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1) warn("Cheat name '" + currentCheat.Name + "' is already present"); else cheats[currentCheat.Name] = currentCheat.Data; } return cheats; } /** * Reads userinput from the chat and sends a simulation command in case it is a known cheat. * * @returns {boolean} - True if a cheat was executed. */ function executeCheat(text) { if (!controlsPlayer(Engine.GetPlayerID()) || !g_Players[Engine.GetPlayerID()].cheatsEnabled) return false; // Find the cheat code that is a prefix of the user input let cheatCode = Object.keys(g_Cheats).find(cheatCode => text.indexOf(cheatCode) == 0); if (!cheatCode) return false; let cheat = g_Cheats[cheatCode]; let parameter = text.substr(cheatCode.length); if (cheat.isNumeric) parameter = +parameter; if (cheat.DefaultParameter && (isNaN(parameter) || parameter <= 0)) parameter = cheat.DefaultParameter; Engine.PostNetworkCommand({ "type": "cheat", "action": cheat.Action, "text": cheat.Type, "player": Engine.GetPlayerID(), "parameter": parameter, "templates": cheat.Templates, "selected": g_Selection.toList() }); return true; } function findGuidForPlayerID(playerID) { return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID); } /** * Processes all pending notifications sent from the GUIInterface simulation component. */ function handleNotifications() { for (let notification of Engine.GuiInterfaceCall("GetNotifications")) { if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type]) { error("Invalid GUI notification: " + uneval(notification)); continue; } for (let player of notification.players) g_NotificationsTypes[notification.type](notification, player); } } /** * Displays all active counters (messages showing the remaining time) for wonder-victory, ceasefire etc. */ function updateTimeNotifications() { let notifications = Engine.GuiInterfaceCall("GetTimeNotifications", g_ViewedPlayer); let notificationText = ""; for (let n of notifications) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); parameters.time = timeToString(n.endTime - g_SimState.timeElapsed); colorizePlayernameParameters(parameters); notificationText += sprintf(message, parameters) + "\n"; } Engine.GetGUIObjectByName("notificationText").caption = notificationText; } /** * Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. */ function handleNetMessages() { while (true) { let msg = Engine.PollNetworkClient(); if (!msg) return; log("Net message: " + uneval(msg)); if (g_NetMessageTypes[msg.type]) g_NetMessageTypes[msg.type](msg); else error("Unrecognised net message type '" + msg.type + "'"); } } /** * @param {Object} message */ function handleNetStatusMessage(message) { if (g_Disconnected) return; if (!g_StatusMessageTypes[message.status]) { error("Unrecognised netstatus type '" + message.status + "'"); return; } g_IsNetworkedActive = message.status == "active"; let label = Engine.GetGUIObjectByName("netStatus"); let statusMessage = g_StatusMessageTypes[message.status](message); label.caption = statusMessage; label.hidden = !statusMessage; if (message.status == "disconnected") { // Hide the pause overlay, and pause animations. Engine.GetGUIObjectByName("pauseOverlay").hidden = true; Engine.SetPaused(true, false); g_Disconnected = true; closeOpenDialogs(); } } function handlePlayerAssignmentsMessage(message) { for (let guid in g_PlayerAssignments) if (!message.newAssignments[guid]) onClientLeave(guid); let joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]); g_PlayerAssignments = message.newAssignments; joins.forEach(guid => { onClientJoin(guid); }); updateGUIObjects(); updateChatAddressees(); sendLobbyPlayerlistUpdate(); } function onClientJoin(guid) { let playerID = g_PlayerAssignments[guid].player; if (g_Players[playerID]) { g_Players[playerID].guid = guid; g_Players[playerID].name = g_PlayerAssignments[guid].name; g_Players[playerID].offline = false; } addChatMessage({ "type": "connect", "guid": guid }); } function onClientLeave(guid) { setClientPauseState(guid, false); for (let id in g_Players) if (g_Players[id].guid == guid) g_Players[id].offline = true; addChatMessage({ "type": "disconnect", "guid": guid }); } function updateChatAddressees() { // Remember previously selected item let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); let selectedName = chatAddressee.list_data[chatAddressee.selected] || ""; selectedName = selectedName.substr(0, 4) == "/msg" && selectedName.substr(5); let addressees = [ { "label": translateWithContext("chat addressee", "Everyone"), "cmd": "" } ]; if (!g_IsObserver) { addressees.push({ "label": translateWithContext("chat addressee", "Allies"), "cmd": "/allies" }); addressees.push({ "label": translateWithContext("chat addressee", "Enemies"), "cmd": "/enemies" }); } addressees.push({ "label": translateWithContext("chat addressee", "Observers"), "cmd": "/observers" }); // Add playernames for private messages let guids = sortGUIDsByPlayerID(); for (let guid of guids) { if (guid == Engine.GetPlayerGUID()) continue; let playerID = g_PlayerAssignments[guid].player; // Don't provide option for PM from observer to player if (g_IsObserver && !isPlayerObserver(playerID)) continue; let colorBox = isPlayerObserver(playerID) ? "" : colorizePlayernameHelper("■", playerID) + " "; addressees.push({ "cmd": "/msg " + g_PlayerAssignments[guid].name, "label": colorBox + g_PlayerAssignments[guid].name }); } // Select mock item if the selected addressee went offline if (selectedName && guids.every(guid => g_PlayerAssignments[guid].name != selectedName)) addressees.push({ "cmd": "/msg " + selectedName, "label": sprintf(translate("\\[OFFLINE] %(player)s"), { "player": selectedName }) }); let oldChatAddressee = chatAddressee.list_data[chatAddressee.selected]; chatAddressee.list = addressees.map(adressee => adressee.label); chatAddressee.list_data = addressees.map(adressee => adressee.cmd); chatAddressee.selected = Math.max(0, chatAddressee.list_data.indexOf(oldChatAddressee)); } /** * Send text as chat. Don't look for commands. * * @param {string} text */ function submitChatDirectly(text) { if (!text.length) return; if (g_IsNetworked) Engine.SendNetworkChat(text); else addChatMessage({ "type": "message", "guid": "local", "text": text }); } /** * Loads the text from the GUI window, checks if it is a local command * or cheat and executes it. Otherwise sends it as chat. */ function submitChatInput() { let text = Engine.GetGUIObjectByName("chatInput").caption; closeChat(); if (!text.length) return; if (executeNetworkCommand(text)) return; if (executeCheat(text)) return; let chatAddressee = Engine.GetGUIObjectByName("chatAddressee"); if (chatAddressee.selected > 0 && (text.indexOf("/") != 0 || text.indexOf("/me ") == 0)) text = chatAddressee.list_data[chatAddressee.selected] + " " + text; + let selectedChat = chatAddressee.list_data[chatAddressee.selected] + if (selectedChat.startsWith("/msg")) + g_LastChatAddressee = selectedChat; + submitChatDirectly(text); } /** * Displays the prepared chatmessage. * * @param msg {Object} */ function addChatMessage(msg) { if (!g_FormatChatMessage[msg.type]) return; let formatted = g_FormatChatMessage[msg.type](msg); if (!formatted) return; // Update chat overlay g_ChatMessages.push(formatted); g_ChatTimers.push(setTimeout(removeOldChatMessage, g_ChatTimeout * 1000)); if (g_ChatMessages.length > g_ChatLines) removeOldChatMessage(); else Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); // Save to chat history let historical = { "txt": formatted, "timePrefix": sprintf(translate("\\[%(time)s]"), { "time": Engine.FormatMillisecondsIntoDateStringLocal(new Date().getTime(), translate("HH:mm")) }), "filter": {} }; // Apply the filters now before diplomacies or playerstates change let senderID = msg.guid && g_PlayerAssignments[msg.guid] ? g_PlayerAssignments[msg.guid].player : 0; for (let filter of g_ChatHistoryFilters) historical.filter[filter.key] = filter.filter(msg, senderID); g_ChatHistory.push(historical); updateChatHistory(); } /** * Called when the timer has run out for the oldest chatmessage or when the message limit is reached. */ function removeOldChatMessage() { clearTimeout(g_ChatTimers[0]); g_ChatTimers.shift(); g_ChatMessages.shift(); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } /** * This function is used for AIs, whose names don't exist in g_PlayerAssignments. */ function colorizePlayernameByID(playerID) { let username = g_Players[playerID] && escapeText(g_Players[playerID].name); return colorizePlayernameHelper(username, playerID); } function colorizePlayernameByGUID(guid) { let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : ""; let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1; return colorizePlayernameHelper(username, playerID); } function colorizePlayernameHelper(username, playerID) { let playerColor = playerID > -1 ? rgbToGuiColor(g_Players[playerID].color) : "white"; return '[color="' + playerColor + '"]' + (username || translate("Unknown Player")) + "[/color]"; } /** * Insert the colorized playername to chat messages sent by the AI and time notifications. */ function colorizePlayernameParameters(parameters) { for (let param in parameters) if (param.startsWith("_player_")) parameters[param] = colorizePlayernameByID(parameters[param]); } function formatDefeatMessage(msg) { return sprintf( msg.resign ? translate("%(player)s has resigned.") : translate("%(player)s has been defeated."), { "player": colorizePlayernameByID(msg.player) } ); } function formatWinMessage(msg) { return sprintf(translate("%(player)s has won."), { "player": colorizePlayernameByID(msg.player) }); } function formatDiplomacyMessage(msg) { let messageType; if (g_IsObserver) messageType = "observer"; else if (Engine.GetPlayerID() == msg.sourcePlayer) messageType = "active"; else if (Engine.GetPlayerID() == msg.targetPlayer) messageType = "passive"; else return ""; return sprintf(g_DiplomacyMessages[messageType][msg.status], { "player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer), "player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer) }); } /** * Optionally show all tributes sent in observer mode and tributes sent between allied players. * Otherwise, only show tributes sent directly to us, and tributes that we send. */ function formatTributeMessage(msg) { let message = ""; if (msg.targetPlayer == Engine.GetPlayerID()) message = translate("%(player)s has sent you %(amounts)s."); else if (msg.sourcePlayer == Engine.GetPlayerID()) message = translate("You have sent %(player2)s %(amounts)s."); else if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.tribute") == "true" && (g_IsObserver || g_GameAttributes.settings.LockTeams && g_Players[msg.sourcePlayer].isMutualAlly[Engine.GetPlayerID()] && g_Players[msg.targetPlayer].isMutualAlly[Engine.GetPlayerID()])) message = translate("%(player)s has sent %(player2)s %(amounts)s."); return sprintf(message, { "player": colorizePlayernameByID(msg.sourcePlayer), "player2": colorizePlayernameByID(msg.targetPlayer), "amounts": getLocalizedResourceAmounts(msg.amounts) }); } function formatBarterMessage(msg) { if (!g_IsObserver || Engine.ConfigDB_GetValue("user", "gui.session.notifications.barter") != "true") return ""; let amountsSold = {}; amountsSold[msg.resourceSold] = msg.amountsSold; let amountsBought = {}; amountsBought[msg.resourceBought] = msg.amountsBought; return sprintf(translate("%(player)s bartered %(amountsBought)s for %(amountsSold)s."), { "player": colorizePlayernameByID(msg.player), "amountsBought": getLocalizedResourceAmounts(amountsBought), "amountsSold": getLocalizedResourceAmounts(amountsSold) }); } function formatAttackMessage(msg) { if (msg.player != g_ViewedPlayer) return ""; let message = msg.targetIsDomesticAnimal ? translate("Your livestock has been attacked by %(attacker)s!") : translate("You have been attacked by %(attacker)s!"); return sprintf(message, { "attacker": colorizePlayernameByID(msg.attacker) }); } function formatChatCommand(msg) { if (!msg.text) return ""; let isMe = msg.text.indexOf("/me ") == 0; if (!isMe && !parseChatAddressee(msg)) return ""; isMe = msg.text.indexOf("/me ") == 0; if (isMe) msg.text = msg.text.substr("/me ".length); // Translate or escape text if (!msg.text) return ""; if (msg.translate) { msg.text = translate(msg.text); if (msg.translateParameters) { let parameters = msg.parameters || {}; translateObjectKeys(parameters, msg.translateParameters); msg.text = sprintf(msg.text, parameters); } } else { msg.text = escapeText(msg.text); let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name; if (userName != g_PlayerAssignments[msg.guid].name) notifyUser(userName, msg.text); } // GUID for players, playerID for AIs let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player); return sprintf(g_ChatCommands[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"], { "message": msg.text, "context": msg.context || undefined, "user": coloredUsername, "userTag": sprintf(translate("<%(user)s>"), { "user": coloredUsername }) }); } /** * Checks if the current user is an addressee of the chatmessage sent by another player. * Sets the context and potentially addresseeGUID of that message. * Returns true if the message should be displayed. * * @param {Object} msg */ function parseChatAddressee(msg) { if (msg.text[0] != '/') return true; // Split addressee command and message-text msg.cmd = msg.text.split(/\s/)[0]; msg.text = msg.text.substr(msg.cmd.length + 1); // GUID is "local" in singleplayer, some string in multiplayer. // Chat messages sent by the simulation (AI) come with the playerID. let senderID = msg.player ? msg.player : (g_PlayerAssignments[msg.guid] || msg).player; let isSender = msg.guid ? msg.guid == Engine.GetPlayerGUID() : senderID == Engine.GetPlayerID(); // Parse private message let isPM = msg.cmd == "/msg"; let addresseeGUID; let addresseeIndex; if (isPM) { addresseeGUID = matchUsername(msg.text); let addressee = g_PlayerAssignments[addresseeGUID]; if (!addressee) { if (isSender) warn("Couldn't match username: " + msg.text); return false; } // Prohibit PM if addressee and sender are identical if (isSender && addresseeGUID == Engine.GetPlayerGUID()) return false; msg.text = msg.text.substr(addressee.name.length + 1); addresseeIndex = addressee.player; } // Set context string if (!g_ChatAddresseeContext[msg.cmd]) { if (isSender) warn("Unknown chat command: " + msg.cmd); return false; } msg.context = g_ChatAddresseeContext[msg.cmd]; // For observers only permit public- and observer-chat and PM to observers if (isPlayerObserver(senderID) && (isPM && !isPlayerObserver(addresseeIndex) || !isPM && msg.cmd != "/observers")) return false; let visible = isSender || g_IsChatAddressee[msg.cmd](senderID, addresseeGUID); msg.isVisiblePM = isPM && visible; return visible; } /** * Returns the guid of the user with the longest name that is a prefix of the given string. */ function matchUsername(text) { if (!text) return ""; let match = ""; let playerGUID = ""; for (let guid in g_PlayerAssignments) { let pName = g_PlayerAssignments[guid].name; if (text.indexOf(pName + " ") == 0 && pName.length > match.length) { match = pName; playerGUID = guid; } } return playerGUID; } /** * Custom dialog response handling, usable by trigger maps. */ function sendDialogAnswer(guiObject, dialogName) { Engine.GetGUIObjectByName(dialogName+"-dialog").hidden = true; Engine.PostNetworkCommand({ "type": "dialog-answer", "dialog": dialogName, "answer": guiObject.name.split("-").pop(), }); resumeGame(); } /** * Custom dialog opening, usable by trigger maps. */ function openDialog(dialogName, data, player) { let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog"); if (!dialog) { warn("messages.js: Unknow dialog with name " + dialogName); return; } dialog.hidden = false; for (let objName in data) { let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName); if (!obj) { warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog."); continue; } for (let key in data[objName]) { let n = data[objName][key]; if (typeof n == "object" && n.message) { let message = n.message; if (n.translateMessage) message = translate(message); let parameters = n.parameters || {}; if (n.translateParameters) translateObjectKeys(parameters, n.translateParameters); obj[key] = sprintf(message, parameters); } else obj[key] = n; } } pauseGame(); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 19271) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 19272) @@ -1,1563 +1,1564 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire); const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly || g_IsReplay)); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions); const g_WonderDurations = prepareForDropdown(g_Settings && g_Settings.WonderDurations); /** * Colors to flash when pop limit reached. */ const g_DefaultPopulationColor = "white"; const g_PopulationAlertColor = "orange"; /** * A random file will be played. TODO: more variety */ const g_Ambient = [ "audio/ambient/dayscape/day_temperate_gen_03.ogg" ]; /** * Map, player and match settings set in gamesetup. */ const g_GameAttributes = Object.freeze(Engine.GetInitAttributes()); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController; /** * True if this is a multiplayer game. */ var g_IsNetworked = false; /** * Whether we have finished the synchronization and * can start showing simulation related message boxes. */ var g_IsNetworkedActive = false; /** * True if the connection to the server has been lost. */ var g_Disconnected = false; /** * True if the current user has observer capabilities. */ var g_IsObserver = false; /** * True if the current user has rejoined (or joined the game after it started). */ var g_HasRejoined = false; /** * Shows a message box asking the user to leave if "won" or "defeated". */ var g_ConfirmExit = false; /** * True if the current player has paused the game explicitly. */ var g_Paused = false; /** * The list of GUIDs of players who have currently paused the game, if the game is networked. */ var g_PausingClients = []; /** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); /** * True if the camera should focus on attacks and player commands * and select the affected units. */ var g_FollowPlayer = false; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var lastTickTime = new Date(); /** * Not constant as we add "gaia". */ var g_CivData = {}; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; var g_PlayerAssignments = { "local": { "name": singleplayerName(), "player": 1 } }; /** * Cache dev-mode settings that are frequently or widely used. */ var g_DevSettings = { "changePerspective": false, "controlAll": false }; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Blink the population counter if the player can't train more units. */ var g_IsTrainingBlocked = false; /** * Cache simulation state (updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TemplateDataWithoutLocalization = {}; var g_TechnologyData = {}; var g_ResourceData = new Resources(); /** * Top coordinate of the research list. * Changes depending on the number of displayed counters. */ var g_ResearchListTop = 4; /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Display data of the current players heroes. */ var g_Heroes = []; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["Female", "Trader", "FishingBoat", "CitizenSoldier"]; /** * Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey. */ var g_MilitaryTypes = ["Melee", "Ranged"]; /** * Cache the idle worker status. */ var g_HasIdleWorker = false; function GetSimState() { if (!g_SimState) g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); return g_SimState; } function GetEntityState(entId) { if (!g_EntityStates[entId]) g_EntityStates[entId] = Engine.GuiInterfaceCall("GetEntityState", entId); return g_EntityStates[entId]; } function GetExtendedEntityState(entId) { let entState = GetEntityState(entId); if (!entState || entState.extended) return entState; let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId); for (let prop in extension) entState[prop] = extension[prop]; entState.extended = true; g_EntityStates[entId] = entState; return entState; } function GetTemplateData(templateName) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", templateName); translateObjectKeys(template, ["specific", "generic", "tooltip"]); g_TemplateData[templateName] = template; } return g_TemplateData[templateName]; } function GetTemplateDataWithoutLocalization(templateName) { if (!(templateName in g_TemplateDataWithoutLocalization)) { let template = Engine.GuiInterfaceCall("GetTemplateData", templateName); g_TemplateDataWithoutLocalization[templateName] = template; } return g_TemplateDataWithoutLocalization[templateName]; } function GetTechnologyData(technologyName, civ) { if (!g_TechnologyData[civ]) g_TechnologyData[civ] = {}; if (!(technologyName in g_TechnologyData[civ])) { let template = Engine.GuiInterfaceCall("GetTechnologyData", { "name": technologyName, "civ": civ }); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); g_TechnologyData[civ][technologyName] = template; } return g_TechnologyData[civ][technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } if (initData) { g_IsNetworked = initData.isNetworked; g_IsController = initData.isController; g_PlayerAssignments = initData.playerAssignments; g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked; } else // Needed for autostart loading option { if (g_IsReplay) g_PlayerAssignments.local.player = -1; } updatePlayerData(); g_CivData = loadCivData(); g_CivData.gaia = { "Code": "gaia", "Name": translate("Gaia") }; initializeMusic(); // before changing the perspective let gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.list = g_GameSpeeds.Title; gameSpeed.list_data = g_GameSpeeds.Speed; let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(Engine.GetSimRate()); gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default; gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); }; initMenuPosition(); resizeDiplomacyDialog(); resizeTradeDialog(); for (let slot in Engine.GetGUIObjectByName("unitHeroPanel").children) initGUIHeroes(slot); // Populate player selection dropdown let playerNames = [translate("Observer")]; let playerIDs = [-1]; for (let player in g_Players) { playerIDs.push(player); playerNames.push(colorizePlayernameHelper("■", player) + " " + g_Players[player].name); } // Select "observer" item when rejoining as a defeated player let viewedPlayer = g_Players[Engine.GetPlayerID()]; let viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer"); viewPlayerDropdown.list = playerNames; viewPlayerDropdown.list_data = playerIDs; viewPlayerDropdown.selected = viewedPlayer && viewedPlayer.state == "defeated" ? 0 : Engine.GetPlayerID() + 1; // If in Atlas editor, disable the exit button if (Engine.IsAtlasRunning()) Engine.GetGUIObjectByName("menuExitButton").enabled = false; if (hotloadData) g_Selection.selected = hotloadData.selection; initChatWindow(); sendLobbyPlayerlistUpdate(); onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); // Report the performance after 5 seconds (when we're still near // the initial camera view) and a minute (when the profiler will // have settled down if framerates as very low), to give some // extremely rough indications of performance // // DISABLED: this information isn't currently useful for anything much, // and it generates a massive amount of data to transmit and store //setTimeout(function() { reportPerformance(5); }, 5000); //setTimeout(function() { reportPerformance(60); }, 60000); } function updatePlayerData() { let simState = GetSimState(); if (!simState) return; let playerData = []; for (let i = 0; i < simState.players.length; ++i) { let playerState = simState.players[i]; playerData.push({ "name": playerState.name, "civ": playerState.civ, "color": { "r": playerState.color.r * 255, "g": playerState.color.g * 255, "b": playerState.color.b * 255, "a": playerState.color.a * 255 }, "team": playerState.team, "teamsLocked": playerState.teamsLocked, "cheatsEnabled": playerState.cheatsEnabled, "state": playerState.state, "isAlly": playerState.isAlly, "isMutualAlly": playerState.isMutualAlly, "isNeutral": playerState.isNeutral, "isEnemy": playerState.isEnemy, "guid": undefined, // network guid for players controlled by hosts "offline": g_Players[i] && !!g_Players[i].offline }); } for (let guid in g_PlayerAssignments) { let playerID = g_PlayerAssignments[guid].player; if (!playerData[playerID]) continue; playerData[playerID].guid = guid; playerData[playerID].name = g_PlayerAssignments[guid].name; } g_Players = playerData; } /** * Depends on the current player (g_IsObserver). */ function updateHotkeyTooltips() { Engine.GetGUIObjectByName("chatInput").tooltip = translateWithContext("chat input", "Type the message to send.") + "\n" + colorizeAutocompleteHotkey() + colorizeHotkey("\n" + translate("Press %(hotkey)s to open the public chat."), "chat") + colorizeHotkey( "\n" + (g_IsObserver ? translate("Press %(hotkey)s to open the observer chat.") : translate("Press %(hotkey)s to open the ally chat.")), - "teamchat"); + "teamchat") + + colorizeHotkey("\n" + translate("Press %(hotkey)s to open the previously selected private chat."), "privatechat"); Engine.GetGUIObjectByName("idleWorkerButton").tooltip = colorizeHotkey("%(hotkey)s" + " ", "selection.idleworker") + translate("Find idle worker"); Engine.GetGUIObjectByName("tradeHelp").tooltip = colorizeHotkey( translate("Select one type of goods you want to modify by clicking on it (Pressing %(hotkey)s while selecting will also bring its share to 100%%) and then use the arrows of the other types to modify their shares."), "session.fulltradeswap"); } function initGUIHeroes(slot) { let button = Engine.GetGUIObjectByName("unitHeroButton[" + slot + "]"); button.onPress = function() { let hero = g_Heroes.find(hero => hero.slot !== undefined && hero.slot == slot); if (!hero) return; if (!Engine.HotkeyIsPressed("selection.add")) g_Selection.reset(); g_Selection.addList([hero.ent]); }; button.onDoublePress = function() { let hero = g_Heroes.find(hero => hero.slot !== undefined && hero.slot == slot); if (hero) selectAndMoveTo(getEntityOrHolder(hero.ent)); }; } /** * Returns the entity itself except when garrisoned where it returns its garrisonHolder */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length && (entState.unitAI.orders[0].type == "Garrison" || entState.unitAI.orders[0].type == "Autogarrison")) return getEntityOrHolder(entState.unitAI.orders[0].data.target); return ent; } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); playAmbient(); } function toggleChangePerspective(enabled) { g_DevSettings.changePerspective = enabled; selectViewPlayer(g_ViewedPlayer); } /** * Change perspective tool. * Shown to observers or when enabling the developers option. */ function selectViewPlayer(playerID) { if (playerID < -1 || playerID > g_Players.length - 1) return; if (g_ShowAllStatusBars) recalculateStatusBarDisplay(true); g_IsObserver = isPlayerObserver(Engine.GetPlayerID()); if (g_IsObserver || g_DevSettings.changePerspective) { if (g_ViewedPlayer != playerID) clearSelection(); g_ViewedPlayer = playerID; } if (g_DevSettings.changePerspective) { Engine.SetPlayerID(g_ViewedPlayer); g_IsObserver = isPlayerObserver(g_ViewedPlayer); } Engine.SetViewedPlayer(g_ViewedPlayer); updateTopPanel(); updateChatAddressees(); updateHotkeyTooltips(); // Update GUI and clear player-dependent cache onSimulationUpdate(); if (g_IsDiplomacyOpen) openDiplomacy(); if (g_IsTradeOpen) openTrade(); } /** * Returns true if the player with that ID is in observermode. */ function isPlayerObserver(playerID) { let playerStates = GetSimState().players; return !playerStates[playerID] || playerStates[playerID].state != "active"; } /** * Returns true if the current user can issue commands for that player. */ function controlsPlayer(playerID) { let playerStates = GetSimState().players; return playerStates[Engine.GetPlayerID()] && playerStates[Engine.GetPlayerID()].controlsAll || Engine.GetPlayerID() == playerID && playerStates[playerID] && playerStates[playerID].state != "defeated"; } /** * Called when a player has won or was defeated. */ function playerFinished(player, won) { if (player == Engine.GetPlayerID()) reportGame(); updatePlayerData(); updateChatAddressees(); if (player != g_ViewedPlayer) return; // Select "observer" item on loss. On win enable observermode without changing perspective Engine.GetGUIObjectByName("viewPlayer").selected = won ? g_ViewedPlayer + 1 : 0; if (player != Engine.GetPlayerID() || Engine.IsAtlasRunning()) return; global.music.setState( won ? global.music.states.VICTORY : global.music.states.DEFEAT ); g_ConfirmExit = won ? "won" : "defeated"; } /** * Sets civ icon for the currently viewed player. * Hides most gui objects for observers. */ function updateTopPanel() { let isPlayer = g_ViewedPlayer > 0; let civIcon = Engine.GetGUIObjectByName("civIcon"); civIcon.hidden = !isPlayer; if (isPlayer) { civIcon.sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem; Engine.GetGUIObjectByName("civIconOverlay").tooltip = sprintf(translate("%(civ)s - Structure Tree"), { "civ": g_CivData[g_Players[g_ViewedPlayer].civ].Name }); } Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver || !isPlayer; let viewPlayer = Engine.GetGUIObjectByName("viewPlayer"); viewPlayer.hidden = !g_IsObserver && !g_DevSettings.changePerspective; let resCodes = g_ResourceData.GetCodes(); let r = 0; for (let res of resCodes) { if (!Engine.GetGUIObjectByName("resource["+r+"]")) { warn("Current GUI limits prevent displaying more than " + r + " resources in the top panel!"); break; } Engine.GetGUIObjectByName("resource["+r+"]_icon").sprite = "stretched:session/icons/resources/" + res + ".png"; Engine.GetGUIObjectByName("resource["+r+"]").hidden = !isPlayer; ++r; } horizontallySpaceObjects("resourceCounts", 5); hideRemaining("resourceCounts", r); let resPop = Engine.GetGUIObjectByName("population"); let resPopSize = resPop.size; resPopSize.left = Engine.GetGUIObjectByName("resource["+ (r-1) +"]").size.right; resPop.size = resPopSize; Engine.GetGUIObjectByName("population").hidden = !isPlayer; Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer; Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer; Engine.GetGUIObjectByName("observerText").hidden = isPlayer; let alphaLabel = Engine.GetGUIObjectByName("alphaLabel"); alphaLabel.hidden = isPlayer && !viewPlayer.hidden; alphaLabel.size = isPlayer ? "50%+20 0 100%-226 100%" : "200 0 100%-475 100%"; Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked; Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver; } function reportPerformance(time) { let settings = g_GameAttributes.settings; Engine.SubmitUserReport("profile", 3, JSON.stringify({ "time": time, "map": settings.Name, "seed": settings.Seed, // only defined for random maps "size": settings.Size, // only defined for random maps "profiler": Engine.GetProfilerState() })); } /** * Resign a player. * @param leaveGameAfterResign If player is quitting after resignation. */ function resignGame(leaveGameAfterResign) { if (g_IsObserver || g_Disconnected) return; Engine.PostNetworkCommand({ "type": "defeat-player", "playerId": Engine.GetPlayerID(), "resign": true }); if (!leaveGameAfterResign) resumeGame(true); } /** * Leave the game * @param willRejoin If player is going to be rejoining a networked game. */ function leaveGame(willRejoin) { if (!willRejoin && !g_IsObserver) resignGame(true); // Before ending the game let replayDirectory = Engine.GetCurrentReplayDirectory(); let simData = getReplayMetadata(); Engine.EndGame(); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_summary.xml", { "sim": simData, "gui": { "assignedPlayer": Engine.GetPlayerID(), "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected }; } function getSavedGameData() { return { "groups": g_Groups.groups }; } function restoreSavedGameData(data) { // Restore camera if any if (data.camera) Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ, data.camera.RotX, data.camera.RotY, data.camera.Zoom); // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (let groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } /** * Called every frame. */ function onTick() { if (!g_Settings) return; let now = new Date(); let tickLength = new Date() - lastTickTime; lastTickTime = now; handleNetMessages(); updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; updateGUIObjects(); // Display rally points for selected buildings if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } updateTimers(); updateMenuPosition(tickLength); // When training is blocked, flash population (alternates color every 500msec) Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && Date.now() % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor; Engine.GuiInterfaceCall("ClearRenamedEntities"); } function changeGameSpeed(speed) { if (!g_IsNetworked) Engine.SetSimRate(speed); } function hasIdleWorker() { return Engine.GuiInterfaceCall("HasIdleUnits", { "viewedPlayer": g_ViewedPlayer, "idleClasses": g_WorkerTypes, "excludeUnits": [] }); } function updateIdleWorkerButton() { g_HasIdleWorker = hasIdleWorker(); let idleWorkerButton = Engine.GetGUIObjectByName("idleOverlay"); let prefix = "stretched:session/"; if (!g_HasIdleWorker) idleWorkerButton.sprite = prefix + "minimap-idle-disabled.png"; else if (idleWorkerButton.sprite != prefix + "minimap-idle-highlight.png") idleWorkerButton.sprite = prefix + "minimap-idle.png"; } function onSimulationUpdate() { g_EntityStates = {}; g_TemplateData = {}; g_TechnologyData = {}; g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); if (!g_SimState) return; handleNotifications(); updateGUIObjects(); if (g_ConfirmExit) confirmExit(); } /** * Don't show the message box before all playerstate changes are processed. */ function confirmExit() { if (g_IsNetworked && !g_IsNetworkedActive) return; closeOpenDialogs(); // Don't ask for exit if other humans are still playing let isHost = g_IsController && g_IsNetworked; let askExit = !isHost || isHost && g_Players.every((player, i) => i == 0 || player.state != "active" || g_GameAttributes.settings.PlayerData[i].AI != ""); let subject = g_PlayerStateMessages[g_ConfirmExit]; if (askExit) subject += "\n" + translate("Do you want to quit?"); messageBox( 400, 200, subject, g_ConfirmExit == "won" ? translate("VICTORIOUS!") : translate("DEFEATED!"), askExit ? [translate("No"), translate("Yes")] : [translate("Ok")], askExit ? [resumeGame, leaveGame] : [resumeGame] ); g_ConfirmExit = false; } function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateHeroes(); displayHeroes(); updateGroups(); updateDebug(); updatePlayerDisplay(); updateResearchDisplay(); updateSelectionDetails(); updateBuildingPlacementPreview(); updateTimeNotifications(); updateIdleWorkerButton(); if (g_ViewedPlayer > 0) { let playerState = GetSimState().players[g_ViewedPlayer]; g_DevSettings.controlAll = playerState && playerState.controlsAll; Engine.GetGUIObjectByName("devControlAll").checked = g_DevSettings.controlAll; } if (!g_IsObserver) { // Update music state on basis of battle state. let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer); if (battleState) global.music.setState(global.music.states[battleState]); } updateDiplomacy(); } function onReplayFinished() { closeOpenDialogs(); pauseGame(); messageBox(400, 200, translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"), translateWithContext("replayFinished", "Confirmation"), [translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")], [resumeGame, leaveGame]); } /** * updates a status bar on the GUI * nameOfBar: name of the bar * points: points to show * maxPoints: max points * direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3; */ function updateGUIStatusBar(nameOfBar, points, maxPoints, direction) { // check, if optional direction parameter is valid. if (!direction || !(direction >= 0 && direction < 4)) direction = 0; // get the bar and update it let statusBar = Engine.GetGUIObjectByName(nameOfBar); if (!statusBar) return; let healthSize = statusBar.size; let value = 100*Math.max(0, Math.min(1, points / maxPoints)); // inverse bar if (direction == 2 || direction == 3) value = 100 - value; if (direction == 0) healthSize.rright = value; else if (direction == 1) healthSize.rbottom = value; else if (direction == 2) healthSize.rleft = value; else if (direction == 3) healthSize.rtop = value; statusBar.size = healthSize; } function updateHeroes() { let playerState = GetSimState().players[g_ViewedPlayer]; let heroes = playerState ? playerState.heroes : []; g_Heroes = g_Heroes.filter(hero => heroes.find(ent => ent == hero.ent)); for (let ent of heroes) { let heroState = GetExtendedEntityState(ent); let template = GetTemplateData(heroState.template); let hero = g_Heroes.find(hero => ent == hero.ent); if (!hero) { hero = { "ent": ent, "tooltip": undefined, "sprite": "stretched:session/portraits/" + template.icon, "maxHitpoints": undefined, "currentHitpoints": heroState.hitpoints, "previousHitpoints": undefined }; g_Heroes.push(hero); } hero.tooltip = createHeroTooltip(heroState, template); hero.previousHitpoints = hero.currentHitpoints; hero.currentHitpoints = heroState.hitpoints; hero.maxHitpoints = heroState.maxHitpoints; } } function createHeroTooltip(heroState, template) { return [ "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]" + "\n" + sprintf(translate("%(label)s %(current)s / %(max)s"), { "label": "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]", "current": Math.ceil(heroState.hitpoints), "max": Math.ceil(heroState.maxHitpoints) }), getAttackTooltip(heroState), getArmorTooltip(heroState), getEntityTooltip(heroState) ].filter(tip => tip).join("\n"); } function displayHeroes() { let buttons = Engine.GetGUIObjectByName("unitHeroPanel").children; buttons.forEach((button, slot) => { if (button.hidden || g_Heroes.some(hero => hero.slot !== undefined && hero.slot == slot)) return; button.hidden = true; stopColorFade("heroHitOverlay[" + slot + "]"); }); // The slot identifies the button, displayIndex determines its position. for (let displayIndex = 0; displayIndex < Math.min(g_Heroes.length, buttons.length); ++displayIndex) { let hero = g_Heroes[displayIndex]; // Find the first unused slot if new, otherwise reuse previous. let slot = hero.slot === undefined ? buttons.findIndex(button => button.hidden) : hero.slot; let heroButton = Engine.GetGUIObjectByName("unitHeroButton[" + slot + "]"); heroButton.tooltip = hero.tooltip; updateGUIStatusBar("heroHealthBar[" + slot + "]", hero.currentHitpoints, hero.maxHitpoints); if (hero.slot === undefined) { let heroImage = Engine.GetGUIObjectByName("unitHeroImage[" + slot + "]"); heroImage.sprite = hero.sprite; heroButton.hidden = false; hero.slot = slot; } // If the health of the hero changed since the last update, trigger the animation. if (hero.previousHitpoints > hero.currentHitpoints) startColorFade("heroHitOverlay[" + slot + "]", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit); // TODO: Instead of instant position changes, animate button movement. setPanelObjectPosition(heroButton, displayIndex, buttons.length); } } function updateGroups() { g_Groups.update(); // Determine the sum of the costs of a given template let getCostSum = (ent) => { let cost = GetTemplateData(GetEntityState(ent).template).cost; return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0; }; for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children) { Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i; let button = Engine.GetGUIObjectByName("unitGroupButton["+i+"]"); button.hidden = g_Groups.groups[i].getTotalCount() == 0; button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i); button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i); button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i); // Chose icon of the most common template (or the most costly if it's not unique) if (g_Groups.groups[i].getTotalCount() > 0) { let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => { if (pre.ents.length == cur.ents.length) return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur; return pre.ents.length > cur.ents.length ? pre : cur; }).ents[0]).template).icon; Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite = icon ? ("stretched:session/portraits/" + icon) : "groupsIcon"; } setPanelObjectPosition(button, i, 1); } } function updateDebug() { let debug = Engine.GetGUIObjectByName("debugEntityState"); if (!Engine.GetGUIObjectByName("devDisplayState").checked) { debug.hidden = true; return; } debug.hidden = false; let conciseSimState = deepcopy(GetSimState()); conciseSimState.players = "<<>>"; let text = "simulation: " + uneval(conciseSimState); let selection = g_Selection.toList(); if (selection.length) { let entState = GetExtendedEntityState(selection[0]); if (entState) { let template = GetTemplateData(entState.template); text += "\n\nentity: {\n"; for (let k in entState) text += " "+k+":"+uneval(entState[k])+"\n"; text += "}\n\ntemplate: " + uneval(template); } } debug.caption = text.replace(/\[/g, "\\["); } function getAllyStatTooltip(resource) { let playersState = GetSimState().players; let ret = ""; for (let player in playersState) { if (player != 0 && player != g_ViewedPlayer && g_Players[player].state != "defeated" && (g_IsObserver || playersState[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer])) { ret += "\n" + sprintf(translate("%(playername)s: %(statValue)s"),{ "playername": colorizePlayernameHelper("■", player) + " " + g_Players[player].name, "statValue": resource == "pop" ? sprintf(translate("%(popCount)s/%(popLimit)s/%(popMax)s"), playersState[player]) : Math.round(playersState[player].resourceCounts[resource]) }); } } return ret; } function updatePlayerDisplay() { let playerState = GetSimState().players[g_ViewedPlayer]; if (!playerState) return; let resCodes = g_ResourceData.GetCodes(); let resNames = g_ResourceData.GetNames(); for (let r = 0; r < resCodes.length; ++r) { if (!Engine.GetGUIObjectByName("resource["+r+"]")) break; let res = resCodes[r]; Engine.GetGUIObjectByName("resource["+r+"]").tooltip = getLocalizedResourceName(resNames[res], "firstWord") + getAllyStatTooltip(res); Engine.GetGUIObjectByName("resource["+r+"]_count").caption = Math.floor(playerState.resourceCounts[res]); } Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), playerState); Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" + sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax }) + getAllyStatTooltip("pop"); g_IsTrainingBlocked = playerState.trainingBlocked; } function selectAndMoveTo(ent) { let entState = GetEntityState(ent); if (!entState || !entState.position) return; g_Selection.reset(); g_Selection.addList([ent]); let position = entState.position; Engine.CameraMoveTo(position.x, position.z); } function updateResearchDisplay() { let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer); // Set up initial positioning. let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right; for (let i = 0; i < 10; ++i) { let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]"); let size = button.size; size.top = g_ResearchListTop + (4 + buttonSideLength) * i; size.bottom = size.top + buttonSideLength; button.size = size; } let numButtons = 0; for (let tech in researchStarted) { // Show at most 10 in-progress techs. if (numButtons >= 10) break; let template = GetTechnologyData(tech); let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]"); button.hidden = false; button.tooltip = getEntityNames(template); button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher); let icon = "stretched:session/portraits/" + template.icon; Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon; // Scale the progress indicator. let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left)); Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size; ++numButtons; } // Hide unused buttons. for (let i = numButtons; i < 10; ++i) Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true; } /** * Toggles the display of status bars for all of the player's entities. * * @param {Boolean} remove - Whether to hide all previously shown status bars. */ function recalculateStatusBarDisplay(remove = false) { let entities; if (g_ShowAllStatusBars && !remove) entities = g_ViewedPlayer == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer); else { let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); // Remove selected entities from the 'all entities' array, // to avoid disabling their status bars. entities = Engine.GuiInterfaceCall( g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", { "viewedPlayer": g_ViewedPlayer }).filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars && !remove }); } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { let entsAdd = []; // list of entities units to be highlighted let entsRemove = []; let highlighted = g_Selection.toList(); for (let ent in g_Selection.highlighted) highlighted.push(g_Selection.highlighted[ent]); if (g_ShowGuarding) { // flag the guarding entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.guard || !state.guard.entities.length) continue; for (let ent of state.guard.entities) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } } if (g_ShowGuarded) { // flag the guarded entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.unitAI || !state.unitAI.isGuarding) continue; let ent = state.unitAI.isGuarding; if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } } // flag the entities to remove (from the previously added) from this additional highlight for (let ent of g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd, g_HighlightedAlpha, true); _setHighlight(entsRemove, 0, false); g_AdditionalHighlight = entsAdd; } function playAmbient() { Engine.PlayAmbientSound(pickRandom(g_Ambient), true); } function getBuildString() { return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), { "buildDate": Engine.GetBuildTimestamp(0), "revision": Engine.GetBuildTimestamp(2) }); } function showTimeWarpMessageBox() { messageBox( 500, 250, translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."), translate("Time warp mode") ); } /** * Adds the ingame time and ceasefire counter to the global FPS and * realtime counters shown in the top right corner. */ function appendSessionCounters(counters) { let simState = GetSimState(); if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true") { let currentSpeed = Engine.GetSimRate(); if (currentSpeed != 1.0) // Translation: The "x" means "times", with the mathematical meaning of multiplication. counters.push(sprintf(translate("%(time)s (%(speed)sx)"), { "time": timeToString(simState.timeElapsed), "speed": Engine.FormatDecimalNumberIntoString(currentSpeed) })); else counters.push(timeToString(simState.timeElapsed)); } if (simState.ceasefireActive && Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true") counters.push(timeToString(simState.ceasefireTimeRemaining)); g_ResearchListTop = 4 + 14 * counters.length; } /** * Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby. * The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data. */ function sendLobbyPlayerlistUpdate() { if (!g_IsController || !Engine.HasXmppClient()) return; // Extract the relevant player data and minimize packet load let minPlayerData = []; for (let playerID in g_GameAttributes.settings.PlayerData) { if (+playerID == 0) continue; let pData = g_GameAttributes.settings.PlayerData[playerID]; let minPData = { "Name": pData.Name }; if (g_GameAttributes.settings.LockTeams) minPData.Team = pData.Team; if (pData.AI) { minPData.AI = pData.AI; minPData.AIDiff = pData.AIDiff; } if (g_Players[playerID].offline) minPData.Offline = true; // Whether the player has won or was defeated let state = g_Players[playerID].state; if (state != "active") minPData.State = state; minPlayerData.push(minPData); } // Add observers let connectedPlayers = 0; for (let guid in g_PlayerAssignments) { let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player]; if (pData) ++connectedPlayers; else minPlayerData.push({ "Name": g_PlayerAssignments[guid].name, "Team": "observer" }); } Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData)); } /** * Send a report on the gamestatus to the lobby. */ function reportGame() { // Only 1v1 games are rated (and Gaia is part of g_Players) if (!Engine.HasXmppClient() || !Engine.IsRankedGame() || g_Players.length != 3 || Engine.GetPlayerID() == -1) return; let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); let unitsClasses = [ "total", "Infantry", "Worker", "Female", "Cavalry", "Champion", "Hero", "Siege", "Ship", "Trader" ]; let unitsCountersTypes = [ "unitsTrained", "unitsLost", "enemyUnitsKilled" ]; let buildingsClasses = [ "total", "CivCentre", "House", "Economic", "Outpost", "Military", "Fortress", "Wonder" ]; let buildingsCountersTypes = [ "buildingsConstructed", "buildingsLost", "enemyBuildingsDestroyed" ]; let resourcesTypes = [ "wood", "food", "stone", "metal" ]; let resourcesCounterTypes = [ "resourcesGathered", "resourcesUsed", "resourcesSold", "resourcesBought" ]; let playerStatistics = {}; // Unit Stats for (let unitCounterType of unitsCountersTypes) { if (!playerStatistics[unitCounterType]) playerStatistics[unitCounterType] = { }; for (let unitsClass of unitsClasses) playerStatistics[unitCounterType][unitsClass] = ""; } playerStatistics.unitsLostValue = ""; playerStatistics.unitsKilledValue = ""; // Building stats for (let buildingCounterType of buildingsCountersTypes) { if (!playerStatistics[buildingCounterType]) playerStatistics[buildingCounterType] = { }; for (let buildingsClass of buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] = ""; } playerStatistics.buildingsLostValue = ""; playerStatistics.enemyBuildingsDestroyedValue = ""; // Resources for (let resourcesCounterType of resourcesCounterTypes) { if (!playerStatistics[resourcesCounterType]) playerStatistics[resourcesCounterType] = { }; for (let resourcesType of resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] = ""; } playerStatistics.resourcesGathered.vegetarianFood = ""; playerStatistics.tradeIncome = ""; // Tribute playerStatistics.tributesSent = ""; playerStatistics.tributesReceived = ""; // Total playerStatistics.economyScore = ""; playerStatistics.militaryScore = ""; playerStatistics.totalScore = ""; // Various playerStatistics.treasuresCollected = ""; playerStatistics.lootCollected = ""; playerStatistics.feminisation = ""; playerStatistics.percentMapExplored = ""; let mapName = g_GameAttributes.settings.Name; let playerStates = ""; let playerCivs = ""; let teams = ""; let teamsLocked = true; // Serialize the statistics for each player into a comma-separated list. // Ignore gaia for (let i = 1; i < extendedSimState.players.length; ++i) { let player = extendedSimState.players[i]; playerStates += player.state + ","; playerCivs += player.civ + ","; teams += player.team + ","; teamsLocked = teamsLocked && player.teamsLocked; for (let resourcesCounterType of resourcesCounterTypes) for (let resourcesType of resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] += player.statistics[resourcesCounterType][resourcesType] + ","; playerStatistics.resourcesGathered.vegetarianFood += player.statistics.resourcesGathered.vegetarianFood + ","; for (let unitCounterType of unitsCountersTypes) for (let unitsClass of unitsClasses) playerStatistics[unitCounterType][unitsClass] += player.statistics[unitCounterType][unitsClass] + ","; for (let buildingCounterType of buildingsCountersTypes) for (let buildingsClass of buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] += player.statistics[buildingCounterType][buildingsClass] + ","; let total = 0; for (let type in player.statistics.resourcesGathered) total += player.statistics.resourcesGathered[type]; playerStatistics.economyScore += total + ","; playerStatistics.militaryScore += Math.round((player.statistics.enemyUnitsKilledValue + player.statistics.enemyBuildingsDestroyedValue) / 10) + ","; playerStatistics.totalScore += (total + Math.round((player.statistics.enemyUnitsKilledValue + player.statistics.enemyBuildingsDestroyedValue) / 10)) + ","; playerStatistics.tradeIncome += player.statistics.tradeIncome + ","; playerStatistics.tributesSent += player.statistics.tributesSent + ","; playerStatistics.tributesReceived += player.statistics.tributesReceived + ","; playerStatistics.percentMapExplored += player.statistics.percentMapExplored + ","; playerStatistics.treasuresCollected += player.statistics.treasuresCollected + ","; playerStatistics.lootCollected += player.statistics.lootCollected + ","; } // Send the report with serialized data let reportObject = {}; reportObject.timeElapsed = extendedSimState.timeElapsed; reportObject.playerStates = playerStates; reportObject.playerID = Engine.GetPlayerID(); reportObject.matchID = g_GameAttributes.matchID; reportObject.civs = playerCivs; reportObject.teams = teams; reportObject.teamsLocked = String(teamsLocked); reportObject.ceasefireActive = String(extendedSimState.ceasefireActive); reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining); reportObject.mapName = mapName; reportObject.economyScore = playerStatistics.economyScore; reportObject.militaryScore = playerStatistics.militaryScore; reportObject.totalScore = playerStatistics.totalScore; for (let rct of resourcesCounterTypes) { for (let rt of resourcesTypes) reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt]; // eg. rt = food rct.substr = Gathered rct = resourcesGathered } reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood; for (let type of unitsClasses) { // eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsTrained"] = playerStatistics.unitsTrained[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsLost"] = playerStatistics.unitsLost[type]; reportObject["enemy"+type+"UnitsKilled"] = playerStatistics.enemyUnitsKilled[type]; } for (let type of buildingsClasses) { reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsConstructed"] = playerStatistics.buildingsConstructed[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsLost"] = playerStatistics.buildingsLost[type]; reportObject["enemy"+type+"BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type]; } reportObject.tributesSent = playerStatistics.tributesSent; reportObject.tributesReceived = playerStatistics.tributesReceived; reportObject.percentMapExplored = playerStatistics.percentMapExplored; reportObject.treasuresCollected = playerStatistics.treasuresCollected; reportObject.lootCollected = playerStatistics.lootCollected; reportObject.tradeIncome = playerStatistics.tradeIncome; Engine.SendGameReport(reportObject); }