Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 13200) +++ ps/trunk/binaries/data/config/default.cfg (revision 13201) @@ -1,312 +1,313 @@ ; 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. * ; * * ; * 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 ; Enable/disable the splashscreen splashscreenenable = true ; Pause the game on window focus loss (Only applicable to single player mode) pauseonfocusloss = 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 ; System settings: waternormals = true waterrealdepth = true waterfoam = false watercoastalwaves = false waterrefraction = true waterreflection = true watershadows = false shadows = true shadowpcf = true vsync = false particles = true silhouettes = true showsky = false 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 (not recommended). REQUIRES gentangents=true. preferglsl = false ; Generate tangents for normal and parallax mapping. REQUIRES preferglsl=true. gentangents = false ; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk gpuskinning = false ; Use smooth LOS interpolation; REQUIRES preferglsl=true. 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 = 0.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 ; Opt-in online user reporting system userreport.url = "http://feedback.wildfiregames.com/report/upload/v1/" ; Colour of the sky (in "r g b" format) skycolor = "0 0 0" ; GENERAL PREFERENCES: sound.mastergain = 0.9 sound.musicgain = 0.2 sound.ambientgain = 0.6 sound.actiongain = 0.7 sound.bufferCount = 50 sound.bufferSize = 98304 ; Camera control settings view.scroll.speed = 120.0 view.scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed view.rotate.x.speed = 1.2 view.rotate.x.min = 28.0 view.rotate.x.max = 60.0 view.rotate.x.default = 35.0 view.rotate.y.speed = 2.0 view.rotate.y.speed.wheel = 0.45 view.rotate.y.default = 0.0 view.rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed view.drag.speed = 0.5 view.zoom.speed = 256.0 view.zoom.speed.wheel = 32.0 view.zoom.min = 50.0 view.zoom.max = 200.0 view.zoom.default = 120.0 view.zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed view.pos.smoothness = 0.1 view.zoom.smoothness = 0.4 view.rotate.x.smoothness = 0.5 view.rotate.y.smoothness = 0.3 view.near = 2.0 ; Near plane distance view.far = 4096.0 ; Far plane distance view.fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide view.height.smoothness = 0.5 view.height.min = 16 ; HOTKEY MAPPINGS: ; Each one of the specified keys will trigger the action on the left ; for multiple-key combinations, separate keys with '+' and enclose the entire thing ; in doublequotes. ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS hotkey.exit = "Alt+F4", "Ctrl+Break", "Super+Q" ; Exit to desktop hotkey.leave = Escape ; End current game or Exit hotkey.pause = Pause ; Pause/unpause game hotkey.screenshot = F2 ; Take PNG screenshot hotkey.bigscreenshot = "Shift+F2" ; Take large BMP screenshot hotkey.togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode hotkey.screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots hotkey.wireframe = "Alt+W" ; Toggle wireframe mode hotkey.silhouettes = "Alt+S" ; Toggle unit silhouettes hotkey.showsky = "Alt+Z" ; Toggle sky ; > CAMERA SETTINGS hotkey.camera.reset = "H" ; Reset camera rotation to default. hotkey.camera.follow = "F" ; Follow the first unit in the selection hotkey.camera.zoom.in = Plus, Equals, NumPlus ; Zoom camera in (continuous control) hotkey.camera.zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) hotkey.camera.zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) hotkey.camera.zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) hotkey.camera.rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards hotkey.camera.rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards hotkey.camera.rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain hotkey.camera.rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain hotkey.camera.rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control) hotkey.camera.rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control) hotkey.camera.pan = MouseMiddle, ForwardSlash ; Enable scrolling by moving mouse hotkey.camera.left = A, LeftArrow ; Scroll or rotate left hotkey.camera.right = D, RightArrow ; Scroll or rotate right hotkey.camera.up = W, UpArrow ; Scroll or rotate up/forwards hotkey.camera.down = S, DownArrow ; Scroll or rotate down/backwards hotkey.camera.scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed hotkey.camera.scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed hotkey.camera.rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed hotkey.camera.rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed hotkey.camera.zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed hotkey.camera.zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed ; > CONSOLE SETTINGS hotkey.console.toggle = BackQuote, F9 ; Open/close console ; > CLIPBOARD CONTROLS hotkey.copy = "Ctrl+C" ; Copy to clipboard hotkey.paste = "Ctrl+V" ; Paste from clipboard hotkey.cut = "Ctrl+X" ; Cut selected text and copy to the clipboard ; > ENTITY SELECTION hotkey.selection.add = Shift ; Add units to selection hotkey.selection.milonly = Alt ; Add only military units to selection hotkey.selection.remove = Ctrl ; Remove units from selection hotkey.selection.idleworker = Period ; Select next idle worker hotkey.selection.idlewarrior = Comma ; Select next idle warrior hotkey.selection.offscreen = Alt ; Include offscreen units in selection hotkey.selection.group.select.0 = 0 hotkey.selection.group.save.0 = "Ctrl+0" hotkey.selection.group.add.0 = "Shift+0" hotkey.selection.group.select.1 = 1 hotkey.selection.group.save.1 = "Ctrl+1" hotkey.selection.group.add.1 = "Shift+1" hotkey.selection.group.select.2 = 2 hotkey.selection.group.save.2 = "Ctrl+2" hotkey.selection.group.add.2 = "Shift+2" hotkey.selection.group.select.3 = 3 hotkey.selection.group.save.3 = "Ctrl+3" hotkey.selection.group.add.3 = "Shift+3" hotkey.selection.group.select.4 = 4 hotkey.selection.group.save.4 = "Ctrl+4" hotkey.selection.group.add.4 = "Shift+4" hotkey.selection.group.select.5 = 5 hotkey.selection.group.save.5 = "Ctrl+5" hotkey.selection.group.add.5 = "Shift+5" hotkey.selection.group.select.6 = 6 hotkey.selection.group.save.6 = "Ctrl+6" hotkey.selection.group.add.6 = "Shift+6" hotkey.selection.group.select.7 = 7 hotkey.selection.group.save.7 = "Ctrl+7" hotkey.selection.group.add.7 = "Shift+7" hotkey.selection.group.select.8 = 8 hotkey.selection.group.save.8 = "Ctrl+8" hotkey.selection.group.add.8 = "Shift+8" hotkey.selection.group.select.9 = 9 hotkey.selection.group.save.9 = "Ctrl+9" hotkey.selection.group.add.9 = "Shift+9" ; > SESSION CONTROLS hotkey.session.kill = Delete ; Destroy selected units hotkey.session.attack = AltGr ; Modifier to force attack instead of another action hotkey.session.garrison = Ctrl ; Modifier to garrison when clicking on building hotkey.session.attackmove = Ctrl ; Modifier to attackmove when clicking on a point hotkey.session.queue = Shift ; Modifier to queue unit orders instead of replacing hotkey.session.batchtrain = Shift ; Modifier to train units in batches hotkey.session.massbarter = Shift ; Modifier to barter bunch of resources +hotkey.session.masstribute = Shift ; Modifier to tribute bunch of resources hotkey.session.unloadtype = Shift ; Modifier to unload all units of type hotkey.session.deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting hotkey.session.rotate.cw = RightBracket ; Rotate building placement preview clockwise hotkey.session.rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise hotkey.timewarp.fastforward = Space ; If timewarp mode enabled, speed up the game hotkey.timewarp.rewind = Backspace ; If timewarp mode enabled, go back to earlier point in the game ; > UNIT TRAINING hotkey.session.queueunit.1 = "Z" ; add first unit type to queue hotkey.session.queueunit.2 = "X" ; add second unit type to queue hotkey.session.queueunit.3 = "C" ; add third unit type to queue hotkey.session.queueunit.4 = "V" ; add fourth unit type to queue hotkey.session.queueunit.5 = "B" ; add fivth unit type to queue hotkey.session.queueunit.6 = "N" ; add sixth unit type to queue hotkey.session.queueunit.7 = "M" ; add seventh unit type to queue ; > OVERLAY KEYS hotkey.fps.toggle = "Alt+F" ; Toggle frame counter hotkey.session.devcommands.toggle = "Alt+D" ; Toggle developer commands panel hotkey.session.gui.toggle = "Alt+G" ; Toggle visibility of session GUI hotkey.menu.toggle = "F10" ; Toggle in-game menu hotkey.timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter hotkey.session.showstatusbars = Tab ; Toggle display of status bars ; > HOTKEYS ONLY hotkey.chat = Return ; Toggle chat window hotkey.teamchat = "T" ; Toggle chat window in team chat mode ; > GUI TEXTBOX HOTKEYS hotkey.text.delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor hotkey.text.delete.right = "Ctrl+Del" ; Delete word to the right of cursor hotkey.text.move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor hotkey.text.move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor ; > PROFILER hotkey.profile.toggle = "F11" ; Enable/disable real-time profiler hotkey.profile.save = "Shift+F11" ; Save current profiler data to logs/profile.txt hotkey.profile2.enable = "F11" ; Enable HTTP/GPU modes for new profiler profiler2.http.autoenable = false ; Enable HTTP server output at startup (default off for security/performance) profiler2.gpu.autoenable = false ; Enable GPU timing at startup (default off for performance/compatibility) profiler2.gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available profiler2.gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available profiler2.gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available ; > QUICKSAVE hotkey.quicksave = "Shift+F5" hotkey.quickload = "Shift+F8" ; EXPERIMENTAL: joystick/gamepad settings joystick.enable = false joystick.deadzone = 8192 joystick.camera.pan.x = 0 joystick.camera.pan.y = 1 joystick.camera.rotate.x = 3 joystick.camera.rotate.y = 2 joystick.camera.zoom.in = 5 joystick.camera.zoom.out = 4 ; GENERAL GUI SETTINGS gui.cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking) Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 13200) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 13201) @@ -1,1997 +1,2002 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; const INPUT_SELECTING = 1; const INPUT_BANDBOXING = 2; const INPUT_BUILDING_PLACEMENT = 3; const INPUT_BUILDING_CLICK = 4; const INPUT_BUILDING_DRAG = 5; const INPUT_BATCHTRAINING = 6; const INPUT_PRESELECTEDACTION = 7; const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; +const INPUT_MASSTRIBUTING = 10; var inputState = INPUT_NORMAL; var placementSupport = new PlacementSupport(); var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; // Number of pixels the mouse can move before the action is considered a drag var maxDragDelta = 4; // Time in milliseconds in which a double click is recognized const doubleClickTime = 500; var doubleClickTimer = 0; var doubleClicked = false; // Store the previously clicked entity - ensure a double/triple click happens on the same entity var prevClickedEntity = 0; // Same double-click behaviour for hotkey presses const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursorAndTooltip() { var cursorSet = false; var tooltipSet = false; var informationTooltip = getGUIObjectByName("informationTooltip"); if (!mouseIsOverObject) { var action = determineAction(mouseX, mouseY); if (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) { if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); cursorSet = true; } if (action.tooltip) { tooltipSet = true; informationTooltip.caption = action.tooltip; informationTooltip.hidden = false; } } } } if (!cursorSet) Engine.SetCursor("arrow-default"); if (!tooltipSet) informationTooltip.hidden = true; var placementTooltip = getGUIObjectByName("placementTooltip"); if (placementSupport.tooltipMessage) { if (placementSupport.tooltipError) placementTooltip.sprite = "BackgroundErrorTooltip"; else placementTooltip.sprite = "BackgroundInformationTooltip"; placementTooltip.caption = placementSupport.tooltipMessage; placementTooltip.hidden = false; } else { placementTooltip.caption = ""; placementTooltip.hidden = true; } } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or // in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to. // See onSimulationUpdate in session.js. if (placementSupport.mode === "building") { if (placementSupport.template && placementSupport.position) { var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed }); // Show placement info tooltip if invalid position placementSupport.tooltipError = !result.success; placementSupport.tooltipMessage = result.success ? "" : result.message; return result.success; } } else if (placementSupport.mode === "wall") { if (placementSupport.wallSet && placementSupport.position) { // Fetch an updated list of snapping candidate entities placementSupport.wallSnapEntities = Engine.PickSimilarFriendlyEntities( placementSupport.wallSet.templates.tower, placementSupport.wallSnapEntitiesIncludeOffscreen, true, // require exact template match true // include foundations ); return Engine.GuiInterfaceCall("SetWallPlacementPreview", { "wallSet": placementSupport.wallSet, "start": placementSupport.position, "end": placementSupport.wallEndPosition, "snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment }); } } return false; } function findGatherType(gatherer, supply) { if (!gatherer || !supply) return undefined; if (gatherer[supply.type.generic+"."+supply.type.specific]) return supply.type.specific; if (gatherer[supply.type.generic]) return supply.type.generic; return undefined; } function getActionInfo(action, target) { var selection = g_Selection.toList(); // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return {"possible": false}; // If the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return {"possible": false}; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); if (!target) { if (action == "set-rallypoint" && haveRallyPoints) return {"possible": true}; else if (action == "move" || action == "attack-move") return {"possible": true}; else return {"possible": false}; } if (haveRallyPoints && selection.indexOf(target) != -1 && action == "unset-rallypoint") return {"possible": true}; // Look at the first targeted entity // (TODO: maybe we eventually want to look at more, and be more context-sensitive? // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse) var targetState = GetEntityState(target); // Check if the target entity is a resource, dropsite, foundation, or enemy unit. // Check if any entities in the selection can gather the requested resource, // can return to the dropsite, can build the foundation, or can attack the enemy var simState = Engine.GuiInterfaceCall("GetSimulationState"); // Look to see what type of command units going to the rally point should use if (haveRallyPoints && action == "set-rallypoint") { // haveRallyPoints ensures all selected entities can have rally points. // We assume that all entities are owned by the same player. var entState = GetEntityState(selection[0]); var playerState = simState.players[entState.player]; var playerOwned = (targetState.player == entState.player); var allyOwned = playerState.isAlly[targetState.player]; var enemyOwned = playerState.isEnemy[targetState.player]; var gaiaOwned = (targetState.player == 0); var cursor = ""; var tooltip; // default to walking there var data = {command: "walk"}; if (targetState.garrisonHolder && playerOwned) { data.command = "garrison"; data.target = target; cursor = "action-garrison"; } else if (targetState.resourceSupply) { var resourceType = targetState.resourceSupply.type; if (resourceType.generic == "treasure") { cursor = "action-gather-" + resourceType.generic; } else { cursor = "action-gather-" + resourceType.specific; } data.command = "gather"; data.resourceType = resourceType; data.resourceTemplate = targetState.template; } else if (targetState.foundation && entState.buildEntities) { data.command = "build"; data.target = target; cursor = "action-build"; } else if (targetState.needsRepair && allyOwned) { data.command = "repair"; data.target = target; cursor = "action-repair"; } else if (hasClass(entState, "Market") && hasClass(targetState, "Market") && entState.id != targetState.id && (!hasClass(entState, "NavalMarket") || hasClass(targetState, "NavalMarket"))) { // Find a trader (if any) that this building can produce. var trader; if (entState.production && entState.production.entities.length) for (var i = 0; i < entState.production.entities.length; ++i) if ((trader = GetTemplateData(entState.production.entities[i]).trader)) break; var traderData = { "firstMarket": entState.id, "secondMarket": targetState.id, "template": trader }; var gain = Engine.GuiInterfaceCall("GetTradingRouteGain", traderData); if (gain !== null) { data.command = "trade"; data.target = traderData.secondMarket; data.source = traderData.firstMarket; cursor = "action-setup-trade-route"; tooltip = "Click to establish a default route for new traders. Gain: " + gain + " metal."; } } // Don't allow the rally point to be set on any of the currently selected entities for (var i = 0; i < selection.length; i++) if (target === selection[i]) return {"possible": false}; return {"possible": true, "data": data, "position": targetState.position, "cursor": cursor, "tooltip": tooltip}; } for each (var entityID in selection) { var entState = GetEntityState(entityID); if (!entState) continue; var playerState = simState.players[entState.player]; var playerOwned = (targetState.player == entState.player); var allyOwned = playerState.isAlly[targetState.player]; var neutralOwned = playerState.isNeutral[targetState.player]; var enemyOwned = playerState.isEnemy[targetState.player]; var gaiaOwned = (targetState.player == 0); // Find the resource type we're carrying, if any var carriedType = undefined; if (entState.resourceCarrying && entState.resourceCarrying.length) carriedType = entState.resourceCarrying[0].type; switch (action) { case "garrison": if (hasClass(entState, "Unit") && targetState.garrisonHolder && playerOwned) { var allowedClasses = targetState.garrisonHolder.allowedClasses; for each (var unitClass in entState.identity.classes) { if (allowedClasses.indexOf(unitClass) != -1) { return {"possible": true}; } } } break; case "setup-trade-route": // If ground or sea trade possible if (!targetState.foundation && ((entState.trader && hasClass(entState, "Organic") && (playerOwned || allyOwned) && hasClass(targetState, "Market")) || (entState.trader && hasClass(entState, "Ship") && (playerOwned || allyOwned) && hasClass(targetState, "NavalMarket")))) { var tradingData = {"trader": entState.id, "target": target}; var tradingDetails = Engine.GuiInterfaceCall("GetTradingDetails", tradingData); var tooltip; if (tradingDetails === null) return {"possible": false}; switch (tradingDetails.type) { case "is first": tooltip = "Origin trade market."; if (tradingDetails.hasBothMarkets) tooltip += " Gain: " + tradingDetails.gain + " " + tradingDetails.goods + ". Right-click to create a new trade route." else tooltip += " Right-click on another market to set it as a destination trade market." break; case "is second": tooltip = "Destination trade market. Gain: " + tradingDetails.gain + " " + tradingDetails.goods + "." + " Right-click to create a new trade route."; break; case "set first": tooltip = "Set as origin trade market"; break; case "set second": tooltip = "Set as destination trade market. Gain: " + tradingDetails.gain + " " + tradingDetails.goods + "."; break; } return {"possible": true, "tooltip": tooltip}; } break; case "heal": // The check if the target is unhealable is done by targetState.needsHeal if (entState.Healer && hasClass(targetState, "Unit") && targetState.needsHeal && (playerOwned || allyOwned)) { // Healers can't heal themselves. if (entState.id == targetState.id) return {"possible": false}; var unhealableClasses = entState.Healer.unhealableClasses; for each (var unitClass in targetState.identity.classes) { if (unhealableClasses.indexOf(unitClass) != -1) { return {"possible": false}; } } var healableClasses = entState.Healer.healableClasses; for each (var unitClass in targetState.identity.classes) { if (healableClasses.indexOf(unitClass) != -1) { return {"possible": true}; } } } break; case "gather": if (targetState.resourceSupply) { var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply); if (resource) return {"possible": true, "cursor": "action-gather-" + resource}; } break; case "returnresource": if (targetState.resourceDropsite && playerOwned && carriedType && targetState.resourceDropsite.types.indexOf(carriedType) != -1) return {"possible": true, "cursor": "action-return-" + carriedType}; break; case "build": if (targetState.foundation && entState.buildEntities && playerOwned) return {"possible": true}; break; case "repair": if (entState.buildEntities && targetState.needsRepair && allyOwned) return {"possible": true}; break; case "attack": if (entState.attack && targetState.hitpoints && (enemyOwned || neutralOwned)) return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": target})}; break; } } if (action == "move" || action == "attack-move") return {"possible": true}; else return {"possible": false}; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMinimap) { var selection = g_Selection.toList(); // No action if there's no selection if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return undefined; // If the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return undefined; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); var targets = []; var target = undefined; var type = "none"; var cursor = ""; var targetState = undefined; if (!fromMinimap) targets = Engine.PickEntitiesAtPoint(x, y); if (targets.length) { target = targets[0]; } if (preSelectedAction != ACTION_NONE) { switch (preSelectedAction) { case ACTION_GARRISON: if (getActionInfo("garrison", target).possible) return {"type": "garrison", "cursor": "action-garrison", "target": target}; else return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined}; break; case ACTION_REPAIR: if (getActionInfo("repair", target).possible) return {"type": "repair", "cursor": "action-repair", "target": target}; else return {"type": "none", "cursor": "action-repair-disabled", "target": undefined}; break; } } else if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible) { return {"type": "attack", "cursor": "action-attack", "target": target}; } else if (Engine.HotkeyIsPressed("session.garrison") && getActionInfo("garrison", target).possible) { return {"type": "garrison", "cursor": "action-garrison", "target": target}; } else if (Engine.HotkeyIsPressed("session.attackmove") && getActionInfo("attack-move", target).possible) { return {"type": "attack-move", "cursor": "action-attack"}; } else { var actionInfo = undefined; if ((actionInfo = getActionInfo("setup-trade-route", target)).possible) return {"type": "setup-trade-route", "cursor": "action-setup-trade-route", "tooltip": actionInfo.tooltip, "target": target}; else if ((actionInfo = getActionInfo("gather", target)).possible) return {"type": "gather", "cursor": actionInfo.cursor, "target": target}; else if ((actionInfo = getActionInfo("returnresource", target)).possible) return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target}; else if (getActionInfo("build", target).possible) return {"type": "build", "cursor": "action-build", "target": target}; else if (getActionInfo("repair", target).possible) return {"type": "build", "cursor": "action-repair", "target": target}; else if ((actionInfo = getActionInfo("set-rallypoint", target)).possible) return {"type": "set-rallypoint", "cursor": actionInfo.cursor, "data": actionInfo.data, "tooltip": actionInfo.tooltip, "position": actionInfo.position}; else if (getActionInfo("heal", target).possible) return {"type": "heal", "cursor": "action-heal", "target": target}; else if (getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; else if (getActionInfo("unset-rallypoint", target).possible) return {"type": "unset-rallypoint"}; else if (getActionInfo("move", target).possible) return {"type": "move"}; } return {"type": type, "cursor": cursor, "target": target}; } var dragStart; // used for remembering mouse coordinates at start of drag operations function tryPlaceBuilding(queued) { if (placementSupport.mode !== "building") { error("[tryPlaceBuilding] Called while in '"+placementSupport.mode+"' placement mode instead of 'building'"); return false; } var selection = g_Selection.toList(); // Use the preview to check it's a valid build location if (!updateBuildingPlacementPreview()) { // invalid location - don't build it // TODO: play a sound? return false; } // Start the construction Engine.PostNetworkCommand({ "type": "construct", "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, "angle": placementSupport.angle, "actorSeed": placementSupport.actorSeed, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); if (!queued) placementSupport.Reset(); else placementSupport.RandomizeActorSeed(); return true; } function tryPlaceWall(queued) { if (placementSupport.mode !== "wall") { error("[tryPlaceWall] Called while in '" + placementSupport.mode + "' placement mode; expected 'wall' mode"); return false; } var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) { error("[tryPlaceWall] Unexpected return value from updateBuildingPlacementPreview: '" + uneval(placementInfo) + "'; expected either 'false' or 'object'"); return false; } if (!wallPlacementInfo) return false; var selection = g_Selection.toList(); var cmd = { "type": "construct-wall", "autorepair": true, "autocontinue": true, "queued": queued, "entities": selection, "wallSet": placementSupport.wallSet, "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, }; // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end // point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed // (this is somewhat non-ideal and hardcode-ish) var hasWallSegment = false; for (var k in cmd.pieces) { if (cmd.pieces[k].template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( { hasWallSegment = true; break; } } if (hasWallSegment) { Engine.PostNetworkCommand(cmd); Engine.GuiInterfaceCall("PlaySound", {"name": "order_repair", "entity": selection[0] }); } return true; } // Limits bandboxed selections to certain types of entities based on priority function getPreferredEntities(ents) { var entStateList = []; var preferredEnts = []; // Check if there are units in the selection and get a list of entity states for each (var ent in ents) { var entState = GetEntityState(ent); if (!entState) continue; if (hasClass(entState, "Unit")) preferredEnts.push(ent); entStateList.push(entState); } // If there are no units, check if there are defensive entities in the selection if (!preferredEnts.length) for (var i = 0; i < ents.length; i++) if (hasClass(entStateList[i], "Defensive")) preferredEnts.push(ents[i]); return preferredEnts; } // Removes any support units from the passed list of entities function getMilitaryEntities(ents) { var militaryEnts = []; for each (var ent in ents) { var entState = GetEntityState(ent); if (!hasClass(entState, "Support")) militaryEnts.push(ent); } return militaryEnts; } function handleInputBeforeGui(ev, hoveredObject) { // Capture mouse position so we can use it for displaying cursors, // and key states switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } // Remember whether the mouse is over a GUI object or not mouseIsOverObject = (hoveredObject != null); // Close the menu when interacting with the game world if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) closeMenu(); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: switch (ev.type) { case "mousemotion": var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.size = [x0, y0, x1, y1].join(" "); bandbox.hidden = false; // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; // Get list of entities limited to preferred entities // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); var preferredEntities = getPreferredEntities(ents) if (preferredEntities.length) { ents = preferredEntities; if (Engine.HotkeyIsPressed("selection.milonly")) { var militaryEntities = getMilitaryEntities(ents); if (militaryEntities.length) ents = militaryEntities; } } // Remove the bandbox hover highlighting g_Selection.setHighlightList([]); // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; var maxDragDelta = 16; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_CLICK: // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. switch (ev.type) { case "mousebuttonup": if (ev.button === SDL_BUTTON_LEFT) { inputState = INPUT_BUILDING_WALL_PATHING; return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_WALL_PATHING: // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the // user to continue building walls. switch (ev.type) { case "mousemotion": placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!) // the ending point and the starting point to snap to. // // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers // in them. Might be useful to query only for entities within a certain range around the starting point and ending // points. placementSupport.wallSnapEntitiesIncludeOffscreen = true; var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates if (result && result.cost) { placementSupport.tooltipMessage = getEntityCostTooltip(result); var neededResources = Engine.GuiInterfaceCall("GetNeededResources", result.cost); if (neededResources) placementSupport.tooltipMessage += getNeededResourcesTooltip(neededResources); } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceWall(queued)) { if (queued) { // continue building, just set a new starting position where we left off placementSupport.position = placementSupport.wallEndPosition; placementSupport.wallEndPosition = undefined; inputState = INPUT_BUILDING_WALL_CLICK; } else { placementSupport.Reset(); inputState = INPUT_NORMAL; } } else { placementSupport.tooltipMessage = "Cannot build wall here!"; } updateBuildingPlacementPreview(); return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // reset to normal input mode placementSupport.Reset(); updateBuildingPlacementPreview(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_DRAG: switch (ev.type) { case "mousemotion": var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; var maxDragDelta = 16; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { // Rotate in the direction of the mouse var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); placementSupport.angle = Math.atan2(target.x - placementSupport.position.x, target.z - placementSupport.position.z); } else { // If the mouse is near the center, snap back to the default orientation placementSupport.SetDefaultAngle(); } var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } updateBuildingPlacementPreview(); break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; } break; + case INPUT_MASSTRIBUTING: + if (ev.type == "hotkeyup" && ev.hotkey == "session.masstribute") + { + flushTributing(); + inputState = INPUT_NORMAL; + } + break; + case INPUT_BATCHTRAINING: - switch (ev.type) + if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") { - case "hotkeyup": - if (ev.hotkey == "session.batchtrain") - { - flushTrainingBatch(); - inputState = INPUT_NORMAL; - } - break; + flushTrainingBatch(); + inputState = INPUT_NORMAL; } + break; } return false; } function handleInputAfterGui(ev) { // Handle the time-warp testing features, restricted to single-player if (!g_IsNetworked && getGUIObjectByName("devTimeWarp").checked) { if (ev.type == "hotkeydown" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(20.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(1.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind") Engine.RewindTimeWarp(); } if (ev.hotkey == "session.showstatusbars") { g_ShowAllStatusBars = (ev.type == "hotkeydown"); recalculateStatusBarDisplay(); } // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (ents.length) g_Selection.setHighlightList([ents[0]]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { dragStart = [ ev.x, ev.y ]; inputState = INPUT_SELECTING; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { var action = determineAction(ev.x, ev.y); if (!action) break; return doAction(action, ev); } break; case "hotkeydown": if (ev.hotkey.indexOf("selection.group.") == 0) { var now = new Date(); if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey)) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { var sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { var sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now.getTime(); prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { var action = determineAction(ev.x, ev.y); if (!action) break; preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; return doAction(action, ev); } else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } // else default: // Slight hack: If selection is empty, reset the input state if (g_Selection.toList().length == 0) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to bandbox mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BANDBOXING; return false; } var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (!ents.length) { if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) { g_Selection.reset(); resetIdleUnit(); } inputState = INPUT_NORMAL; return true; } var selectedEntity = ents[0]; var now = new Date(); // If camera following and we select different unit, stop if (Engine.GetFollowedEntity() != selectedEntity) { Engine.CameraFollow(0); } if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity)) { // Double click or triple click has occurred var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); var matchRank = true; var templateToMatch; // Check for double click or triple click if (!doubleClicked) { // If double click hasn't already occurred, this is a double click. // Select similar units regardless of rank templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).identity.selectionGroupName; if (templateToMatch) { matchRank = false; } else { // No selection group name defined, so fall back to exact match templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template; } doubleClicked = true; // Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click doubleClickTimer = now.getTime(); } else { // Double click has already occurred, so this is a triple click. // Select units matching exact template name (same rank) templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template; } // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank, false); } else { // It's single click right now but it may become double or triple click doubleClicked = false; doubleClickTimer = now.getTime(); prevClickedEntity = selectedEntity; // We only want to include the first picked unit in the selection ents = [ents[0]]; } // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (placementSupport.mode === "wall") { // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities // itself happens in the call to updateBuildingPlacementPreview below). placementSupport.wallSnapEntitiesIncludeOffscreen = false; } else { var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, }); if (snapData) { placementSupport.angle = snapData.angle; placementSupport.position.x = snapData.x; placementSupport.position.z = snapData.z; } } updateBuildingPlacementPreview(); // includes an update of the snap entity candidates return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { if (placementSupport.mode === "wall") { var validPlacement = updateBuildingPlacementPreview(); if (validPlacement !== false) { inputState = INPUT_BUILDING_WALL_CLICK; } } else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); dragStart = [ ev.x, ev.y ]; inputState = INPUT_BUILDING_CLICK; } return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building placementSupport.Reset(); inputState = INPUT_NORMAL; return true; } break; case "hotkeydown": var rotation_step = Math.PI / 12; // 24 clicks make a full rotation switch (ev.hotkey) { case "session.rotate.cw": placementSupport.angle += rotation_step; updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementSupport.angle -= rotation_step; updateBuildingPlacementPreview(); break; } break; } break; } return false; } function doAction(action, ev) { var selection = g_Selection.toList(); // If shift is down, add the order to the unit's order queue instead // of running it immediately var queued = Engine.HotkeyIsPressed("session.queue"); switch (action.type) { case "move": var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "attack-move": var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "attack": Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); return true; case "heal": Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] }); return true; case "build": // (same command as repair) case "repair": Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; case "gather": Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "returnresource": Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "setup-trade-route": Engine.PostNetworkCommand({"type": "setup-trade-route", "entities": selection, "target": action.target}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_trade", "entity": selection[0] }); return true; case "garrison": Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] }); return true; case "set-rallypoint": var pos = undefined; // if there is a position set in the action then use this so that when setting a // rally point on an entity it is centered on that entity if (action.position) { pos = action.position; } else { pos = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); } Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": pos.x, "z": pos.z, "data": action.data, "queued": queued}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": pos.x, "z": pos.z, "queued": queued }); return true; case "unset-rallypoint": var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection}); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; case "none": return true; default: error("Invalid action.type "+action.type); return false; } } function handleMinimapEvent(target) { // Partly duplicated from handleInputAfterGui(), but with the input being // world coordinates instead of screen coordinates. if (inputState == INPUT_NORMAL) { var fromMinimap = true; var action = determineAction(undefined, undefined, fromMinimap); if (!action) return false; var selection = g_Selection.toList(); var queued = Engine.HotkeyIsPressed("session.queue"); switch (action.type) { case "move": Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "attack-move": Engine.PostNetworkCommand({"type": "attack-walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "set-rallypoint": Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z }); return true; default: error("Invalid action.type "+action.type); } } return false; } // Called by GUI when user clicks construction button // @param buildTemplate Template name of the entity the user wants to build function startBuildingPlacement(buildTemplate) { // TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI // to start building a structure, then the highlight selection rings are kept during the construction of the building. // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing. placementSupport.Reset(); // find out if we're building a wall, and change the entity appropriately if so var templateData = GetTemplateData(buildTemplate); if (templateData.wallSet) { placementSupport.mode = "wall"; placementSupport.wallSet = templateData.wallSet; inputState = INPUT_BUILDING_PLACEMENT; } else { placementSupport.mode = "building"; placementSupport.template = buildTemplate; inputState = INPUT_BUILDING_PLACEMENT; } } // Called by GUI when user changes preferred trading goods function selectTradingPreferredGoods(data) { Engine.PostNetworkCommand({"type": "select-trading-goods", "entities": data.entities, "preferredGoods": data.preferredGoods}); } // Called by GUI when user clicks exchange resources button function exchangeResources(command) { Engine.PostNetworkCommand({"type": "barter", "sell": command.sell, "buy": command.buy, "amount": command.amount}); } // Batch training: // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING // When the user releases shift, or clicks on a different training button, we create the batched units var batchTrainingEntities; var batchTrainingType; var batchTrainingCount; var batchTrainingEntityAllowedCount; const batchIncrementSize = 5; function flushTrainingBatch() { var appropriateBuildings = getBuildingsWhichCanTrainEntity(batchTrainingEntities, batchTrainingType); // If training limits don't allow us to train batchTrainingCount in each appropriate building if (batchTrainingEntityAllowedCount !== undefined && batchTrainingEntityAllowedCount < batchTrainingCount * appropriateBuildings.length) { // Train as many full batches as we can var buildingsCountToTrainFullBatch = Math.floor(batchTrainingEntityAllowedCount / batchTrainingCount); var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch); Engine.PostNetworkCommand({"type": "train", "entities": buildingsToTrainFullBatch, "template": batchTrainingType, "count": batchTrainingCount}); // Train remainer in one more building var remainderToTrain = batchTrainingEntityAllowedCount % batchTrainingCount; Engine.PostNetworkCommand({"type": "train", "entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ], "template": batchTrainingType, "count": remainderToTrain}); } else { Engine.PostNetworkCommand({"type": "train", "entities": appropriateBuildings, "template": batchTrainingType, "count": batchTrainingCount}); } } function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) { return entitiesToCheck.filter(function(entity) { var state = GetEntityState(entity); var canTrain = state && state.production && state.production.entities.length && state.production.entities.indexOf(trainEntType) != -1; return canTrain; }); } function getEntityLimitAndCount(playerState, entType) { var template = GetTemplateData(entType); var trainingCategory = null; if (template.trainingRestrictions) trainingCategory = template.trainingRestrictions.category; var trainEntLimit = undefined; var trainEntCount = undefined; var canBeTrainedCount = undefined; if (trainingCategory && playerState.entityLimits[trainingCategory]) { trainEntLimit = playerState.entityLimits[trainingCategory]; trainEntCount = playerState.entityCounts[trainingCategory]; canBeTrainedCount = Math.max(trainEntLimit - trainEntCount, 0); } return [trainEntLimit, trainEntCount, canBeTrainedCount]; } // Add the unit shown at position to the training queue for all entities in the selection function addTrainingByPosition(position) { var simState = Engine.GuiInterfaceCall("GetSimulationState"); var playerState = simState.players[Engine.GetPlayerID()]; var selection = g_Selection.toList(); if (!selection.length) return; var trainableEnts = getAllTrainableEntities(selection); // Check if the position is valid if (!trainableEnts.length || trainableEnts.length <= position) return; var entToTrain = trainableEnts[position]; addTrainingToQueue(selection, entToTrain, playerState); return; } // Called by GUI when user clicks training button function addTrainingToQueue(selection, trainEntType, playerState) { // Create list of buildings which can train trainEntType var appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); // Check trainEntType entity limit and count var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType) // Batch training possible if we can train at least 2 units var batchTrainingPossible = canBeTrainedCount == undefined || canBeTrainedCount > 1; if (Engine.HotkeyIsPressed("session.batchtrain") && batchTrainingPossible) { if (inputState == INPUT_BATCHTRAINING) { // Check if we are training in the same building(s) as the last batch var sameEnts = false; if (batchTrainingEntities.length == selection.length) { // NOTE: We just check if the arrays are the same and if the order is the same // If the order changed, we have a new selection and we should create a new batch. for (var i = 0; i < batchTrainingEntities.length; ++i) { if (!(sameEnts = batchTrainingEntities[i] == selection[i])) break; } } // If we're already creating a batch of this unit (in the same building(s)), then just extend it // (if training limits allow) if (sameEnts && batchTrainingType == trainEntType) { if (canBeTrainedCount == undefined || canBeTrainedCount > batchTrainingCount * appropriateBuildings.length) batchTrainingCount += batchIncrementSize; batchTrainingEntityAllowedCount = canBeTrainedCount; return; } // Otherwise start a new one else { flushTrainingBatch(); // fall through to create the new batch } } inputState = INPUT_BATCHTRAINING; batchTrainingEntities = selection; batchTrainingType = trainEntType; batchTrainingEntityAllowedCount = canBeTrainedCount; batchTrainingCount = batchIncrementSize; } else { // Non-batched - just create a single entity in each building // (but no more than entity limit allows) var buildingsForTraining = appropriateBuildings; if (trainEntLimit) buildingsForTraining = buildingsForTraining.slice(0, canBeTrainedCount); Engine.PostNetworkCommand({"type": "train", "template": trainEntType, "count": 1, "entities": buildingsForTraining}); } } // Called by GUI when user clicks research button function addResearchToQueue(entity, researchType) { Engine.PostNetworkCommand({"type": "research", "entity": entity, "template": researchType}); } // Returns the number of units that will be present in a batch if the user clicks // the training button with shift down function getTrainingBatchStatus(playerState, entity, trainEntType, selection) { var appropriateBuildings = [entity]; if (selection && selection.indexOf(entity) != -1) appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); var nextBatchTrainingCount = 0; if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 && batchTrainingType == trainEntType) { nextBatchTrainingCount = batchTrainingCount; var canBeTrainedCount = batchTrainingEntityAllowedCount; } else { var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType); var batchSize = Math.min(canBeTrainedCount, batchIncrementSize); } // We need to calculate count after the next increment if it's possible if (canBeTrainedCount == undefined || canBeTrainedCount > nextBatchTrainingCount * appropriateBuildings.length) nextBatchTrainingCount += batchIncrementSize; // If training limits don't allow us to train batchTrainingCount in each appropriate building // train as many full batches as we can and remainer in one more building. var buildingsCountToTrainFullBatch = appropriateBuildings.length; var remainderToTrain = 0; if (canBeTrainedCount !== undefined && canBeTrainedCount < nextBatchTrainingCount * appropriateBuildings.length) { buildingsCountToTrainFullBatch = Math.floor(canBeTrainedCount / nextBatchTrainingCount); remainderToTrain = canBeTrainedCount % nextBatchTrainingCount; } return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; } // Called by GUI when user clicks production queue item function removeFromProductionQueue(entity, id) { Engine.PostNetworkCommand({"type": "stop-production", "entity": entity, "id": id}); } // Called by unit selection buttons function changePrimarySelectionGroup(templateName, deselectGroup) { if (Engine.HotkeyIsPressed("session.deselectgroup") || deselectGroup) g_Selection.makePrimarySelection(templateName, true); else g_Selection.makePrimarySelection(templateName, false); } // Performs the specified command (delete, town bell, repair, etc.) function performCommand(entity, commandName) { if (entity) { var entState = GetEntityState(entity); var template = GetTemplateData(entState.template); var unitName = getEntityName(template); var playerID = Engine.GetPlayerID(); if (entState.player == playerID || g_DevSettings.controlAll) { switch (commandName) { case "delete": var selection = g_Selection.toList(); if (selection.length > 0) if (!entState.resourceSupply || !entState.resourceSupply.killBeforeGather) openDeleteDialog(selection); break; case "stop": var selection = g_Selection.toList(); if (selection.length > 0) stopUnits(selection); break; case "garrison": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; break; case "repair": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; break; case "unload-all": unloadAll(); break; case "focus-rally": // if the selected building has a rally point set, move the camera to it; otherwise, move to the building itself // (since that's where units will spawn without a rally point) var focusTarget = null; if (entState.rallyPoint && entState.rallyPoint.position) { focusTarget = entState.rallyPoint.position; } else { if (entState.position) focusTarget = entState.position; } if (focusTarget !== null) Engine.CameraMoveTo(focusTarget.x, focusTarget.z); break; default: break; } } } } // Performs the specified formation function performFormation(entity, formationName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "formation", "entities": selection, "name": formationName }); } } // Performs the specified group function performGroup(action, groupId) { switch (action) { case "snap": case "select": case "add": var toSelect = []; g_Groups.update(); for (var ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) Engine.CameraFollow(toSelect[0]); break; case "save": var selection = g_Selection.toList(); g_Groups.groups[groupId].reset(); g_Groups.addEntities(groupId, selection); updateGroups(); break; } } // Performs the specified stance function performStance(entity, stanceName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "stance", "entities": selection, "name": stanceName }); } } // Lock / Unlock the gate function lockGate(lock) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "lock-gate", "entities": selection, "lock": lock, }); } // Pack / unpack unit(s) function packUnit(pack) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "pack", "entities": selection, "pack": pack, "queued": false }); } // Cancel un/packing unit(s) function cancelPackUnit(pack) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "cancel-pack", "entities": selection, "pack": pack, "queued": false }); } // Transform a wall to a gate function transformWallToGate(template) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "wall-to-gate", "entities": selection.filter( function(e) { return getWallGateTemplate(e) == template } ), "template": template, }); } // Gets the gate form (if any) of a given long wall piece function getWallGateTemplate(entity) { // TODO: find the gate template name in a better way var entState = GetEntityState(entity); var index; if (entState && !entState.foundation && hasClass(entState, "LongWall") && (index = entState.template.indexOf("long")) >= 0) return entState.template.substr(0, index) + "gate"; return undefined; } // Set the camera to follow the given unit function setCameraFollow(entity) { // Follow the given entity if it's a unit if (entity) { var entState = GetEntityState(entity); if (entState && hasClass(entState, "Unit")) { Engine.CameraFollow(entity); return; } } // Otherwise stop following Engine.CameraFollow(0); } var lastIdleUnit = 0; var currIdleClass = 0; var lastIdleType = undefined; function resetIdleUnit() { lastIdleUnit = 0; currIdleClass = 0; lastIdleType = undefined; } function findIdleUnit(classes) { var append = Engine.HotkeyIsPressed("selection.add"); var selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. var type = classes.join(); if (selectall || type != lastIdleType) resetIdleUnit(); lastIdleType = type; // If selectall is true, there is no limit and it's necessary to iterate // over all of the classes, resetting only when the first match is found. var matched = false; for (var i = 0; i < classes.length; ++i) { var data = { idleClass: classes[currIdleClass], prevUnit: lastIdleUnit, limit: 1 }; if (append) data.excludeUnits = g_Selection.toList(); if (selectall) data = { idleClass: classes[currIdleClass] }; // Check if we have new valid entity var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); if (idleUnits.length && idleUnits[0] != lastIdleUnit) { lastIdleUnit = idleUnits[0]; if (!append && (!selectall || selectall && !matched)) g_Selection.reset() if (selectall) g_Selection.addList(idleUnits); else { g_Selection.addList([lastIdleUnit]); var position = GetEntityState(lastIdleUnit).position; Engine.CameraMoveTo(position.x, position.z); return; } matched = true; } lastIdleUnit = 0; currIdleClass = (currIdleClass + 1) % classes.length; } // TODO: display a message or play a sound to indicate no more idle units, or something // Reset for next cycle resetIdleUnit(); } function stopUnits(entities) { Engine.PostNetworkCommand({ "type": "stop", "entities": entities, "queued": false }); } function unload(garrisonHolder, entities) { if (Engine.HotkeyIsPressed("session.unloadtype")) Engine.PostNetworkCommand({"type": "unload", "entities": entities, "garrisonHolder": garrisonHolder}); else Engine.PostNetworkCommand({"type": "unload", "entities": [entities[0]], "garrisonHolder": garrisonHolder}); } function unloadTemplate(template) { // Filter out all entities that aren't garrisonable. var garrisonHolders = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); if (state && state.garrisonHolder) return true; return false; }); Engine.PostNetworkCommand({ "type": "unload-template", "all": Engine.HotkeyIsPressed("session.unloadtype"), "template": template, "garrisonHolders": garrisonHolders }); } function unloadAll() { // Filter out all entities that aren't garrisonable. var garrisonHolders = g_Selection.toList().filter(function(e) { var state = GetEntityState(e); if (state && state.garrisonHolder) return true; return false; }); Engine.PostNetworkCommand({"type": "unload-all", "garrisonHolders": garrisonHolders}); } Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 13200) +++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 13201) @@ -1,398 +1,430 @@ const PAUSE = "Pause"; const RESUME = "Resume"; /* * MENU POSITION CONSTANTS */ // Menu / panel border size const MARGIN = 4; // Includes the main menu button const NUM_BUTTONS = 8; // 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; var isMenuOpen = false; var menu; var isDiplomacyOpen = false; +// Redefined every time someone makes a tribute (so we can save some data in a closure). Called in input.js handleInputBeforeGui. +var flushTributing = function() {}; + // Ignore size defined in XML and set the actual menu size here function initMenuPosition() { menu = getGUIObjectByName("menu"); menu.size = INITIAL_MENU_POSITION; } // ============================================================================= // Overall Menu // ============================================================================= // // Slide menu function updateMenuPosition(dt) { if (isMenuOpen) { var maxOffset = END_MENU_POSITION - menu.size.bottom; if (maxOffset > 0) { var offset = Math.min(MENU_SPEED * dt, maxOffset); var size = menu.size; size.top += offset; size.bottom += offset; menu.size = size; } } else { var maxOffset = menu.size.top - MENU_TOP; if (maxOffset > 0) { var offset = Math.min(MENU_SPEED * dt, maxOffset); var 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() { // playButtonSound(); isMenuOpen = true; } // Closes the menu and resets position function closeMenu() { // playButtonSound(); isMenuOpen = false; } function toggleMenu() { if (isMenuOpen == true) closeMenu(); else openMenu(); } // Menu buttons // ============================================================================= function settingsMenuButton() { closeMenu(); closeOpenDialogs(); openSettings(true); } function chatMenuButton() { closeMenu(); closeOpenDialogs(); openChat(); } function diplomacyMenuButton() { closeMenu(); closeOpenDialogs(); openDiplomacy(); } function pauseMenuButton() { togglePause(); } function resignMenuButton() { closeMenu(); closeOpenDialogs(); pauseGame(); var btCaptions = ["Yes", "No"]; var btCode = [resignGame, resumeGame]; messageBox(400, 200, "Are you sure you want to resign?", "Confirmation", 0, btCaptions, btCode); } function exitMenuButton() { closeMenu(); closeOpenDialogs(); pauseGame(); var btCaptions = ["Yes", "No"]; var btCode = [leaveGame, resumeGame]; messageBox(400, 200, "Are you sure you want to quit?", "Confirmation", 0, btCaptions, btCode); } function openDeleteDialog(selection) { closeMenu(); closeOpenDialogs(); var deleteSelectedEntities = function () { Engine.PostNetworkCommand({"type": "delete-entities", "entities": selection}); }; var btCaptions = ["Yes", "No"]; var btCode = [deleteSelectedEntities, resumeGame]; messageBox(400, 200, "Destroy everything currently selected?", "Delete", 0, btCaptions, btCode); } // Menu functions // ============================================================================= function openSettings(pause) { getGUIObjectByName("settingsDialogPanel").hidden = false; if (pause) pauseGame(); } function closeSettings(resume) { getGUIObjectByName("settingsDialogPanel").hidden = true; if (resume) resumeGame(); } function openChat() { getGUIObjectByName("chatInput").focus(); // Grant focus to the input area getGUIObjectByName("chatDialogPanel").hidden = false; } function closeChat() { getGUIObjectByName("chatInput").caption = ""; // Clear chat input getGUIObjectByName("chatInput").blur(); // Remove focus getGUIObjectByName("chatDialogPanel").hidden = true; } function toggleChatWindow(teamChat) { closeSettings(); var chatWindow = getGUIObjectByName("chatDialogPanel"); var chatInput = getGUIObjectByName("chatInput"); if (chatWindow.hidden) chatInput.focus(); // Grant focus to the input area else { if (chatInput.caption.length) { submitChatInput(); return; } chatInput.caption = ""; // Clear chat input } getGUIObjectByName("toggleTeamChat").checked = teamChat; chatWindow.hidden = !chatWindow.hidden; } function setDiplomacy(data) { Engine.PostNetworkCommand({"type": "diplomacy", "to": data.to, "player": data.player}); } function tributeResource(data) { Engine.PostNetworkCommand({"type": "tribute", "player": data.player, "amounts": data.amounts}); } function openDiplomacy() { isDiplomacyOpen = true; var we = Engine.GetPlayerID(); var players = getPlayerData(g_PlayerAssignments); // Get offset for one line var onesize = getGUIObjectByName("diplomacyPlayer[0]").size; var rowsize = onesize.bottom - onesize.top; // We don't include gaia for (var i = 1; i < players.length; i++) { // Apply offset var row = getGUIObjectByName("diplomacyPlayer["+(i-1)+"]"); var size = row.size; size.top = rowsize*(i-1); size.bottom = rowsize*i; row.size = size; // Set background colour var playerColor = players[i].color.r+" "+players[i].color.g+" "+players[i].color.b; row.sprite = "colour: "+playerColor + " 32"; getGUIObjectByName("diplomacyPlayerName["+(i-1)+"]").caption = "[color=\"" + playerColor + "\"]" + players[i].name + "[/color]"; getGUIObjectByName("diplomacyPlayerCiv["+(i-1)+"]").caption = g_CivData[players[i].civ].Name; getGUIObjectByName("diplomacyPlayerTeam["+(i-1)+"]").caption = (players[i].team < 0) ? "None" : players[i].team+1; if (i != we) getGUIObjectByName("diplomacyPlayerTheirs["+(i-1)+"]").caption = (players[i].isAlly[we] ? "Ally" : (players[i].isNeutral[we] ? "Neutral" : "Enemy")); // Don't display the options for ourself, or if we or the other player aren't active anymore if (i == we || players[we].state != "active" || players[i].state != "active") { // Hide the unused/unselectable options for each (var a in ["TributeFood", "TributeWood", "TributeStone", "TributeMetal", "Ally", "Neutral", "Enemy"]) getGUIObjectByName("diplomacyPlayer"+a+"["+(i-1)+"]").hidden = true; continue; } // Tribute for each (var resource in ["food", "wood", "stone", "metal"]) { var button = getGUIObjectByName("diplomacyPlayerTribute"+toTitleCase(resource)+"["+(i-1)+"]"); - // TODO: Make amounts changeable or change to 500 if shift is pressed - var amounts = { - "food": (resource=="food")?100:0, - "wood": (resource=="wood")?100:0, - "stone": (resource=="stone")?100:0, - "metal": (resource=="metal")?100:0, - }; - button.onpress = (function(e){ return function() { tributeResource(e) } })({"player": i, "amounts": amounts}); + button.onpress = (function(player, resource, button){ + // Implement something like how unit batch training works. Shift+click to send 500, shift+click+click to send 1000, etc. + // Also see input.js (searching for "INPUT_MASSTRIBUTING" should get all the relevant parts). + var multiplier = 1; + return function() { + var isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute"); + if (isBatchTrainPressed) + { + inputState = INPUT_MASSTRIBUTING; + multiplier += multiplier == 1 ? 4 : 5; + } + var amounts = { + "food": (resource == "food" ? 100 : 0) * multiplier, + "wood": (resource == "wood" ? 100 : 0) * multiplier, + "stone": (resource == "stone" ? 100 : 0) * multiplier, + "metal": (resource == "metal" ? 100 : 0) * multiplier, + }; + button.tooltip = formatTributeTooltip(players[player], resource, amounts[resource]); + // This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some + // evil global variable hackery. + flushTributing = function() { + tributeResource({"player": player, "amounts": amounts}); + multiplier = 1; + button.tooltip = formatTributeTooltip(players[player], resource, 100); + }; + if (!isBatchTrainPressed) + flushTributing(); + }; + })(i, resource, button); button.hidden = false; + button.tooltip = formatTributeTooltip(players[i], resource, 100); } // Skip our own teams on teams locked if (players[we].teamsLocked && players[we].team != -1 && players[we].team == players[i].team) continue; // Diplomacy settings // Set up the buttons for each (var setting in ["ally", "neutral", "enemy"]) { var button = getGUIObjectByName("diplomacyPlayer"+toTitleCase(setting)+"["+(i-1)+"]"); if (setting == "ally") { if (players[we].isAlly[i]) button.caption = "x"; else button.caption = ""; } else if (setting == "neutral") { if (players[we].isNeutral[i]) button.caption = "x"; else button.caption = ""; } else // "enemy" { if (players[we].isEnemy[i]) button.caption = "x"; else button.caption = ""; } button.onpress = (function(e){ return function() { setDiplomacy(e) } })({"player": i, "to": setting}); button.hidden = false; } } getGUIObjectByName("diplomacyDialogPanel").hidden = false; } function closeDiplomacy() { isDiplomacyOpen = false; getGUIObjectByName("diplomacyDialogPanel").hidden = true; } function toggleDiplomacy() { if (isDiplomacyOpen) closeDiplomacy(); else openDiplomacy(); }; function pauseGame() { getGUIObjectByName("pauseButtonText").caption = RESUME; getGUIObjectByName("pauseOverlay").hidden = false; setPaused(true); } function resumeGame() { getGUIObjectByName("pauseButtonText").caption = PAUSE; getGUIObjectByName("pauseOverlay").hidden = true; setPaused(false); } function togglePause() { closeMenu(); closeOpenDialogs(); var pauseOverlay = getGUIObjectByName("pauseOverlay"); if (pauseOverlay.hidden) { getGUIObjectByName("pauseButtonText").caption = RESUME; setPaused(true); } else { setPaused(false); getGUIObjectByName("pauseButtonText").caption = PAUSE; } pauseOverlay.hidden = !pauseOverlay.hidden; } function openManual() { closeMenu(); closeOpenDialogs(); pauseGame(); Engine.PushGuiPage("page_manual.xml", {"page": "intro", "closeCallback": resumeGame}); } function toggleDeveloperOverlay() { var devCommands = getGUIObjectByName("devCommands"); var text = devCommands.hidden ? "opened." : "closed."; submitChatDirectly("The Developer Overlay was " + text); // Update the options dialog getGUIObjectByName("developerOverlayCheckbox").checked = devCommands.hidden; devCommands.hidden = !devCommands.hidden; } function closeOpenDialogs() { closeMenu(); closeChat(); closeDiplomacy(); closeSettings(false); } + +function formatTributeTooltip(player, resource, amount) +{ + var playerColor = player.color.r + " " + player.color.g + " " + player.color.b; + return "Tribute " + amount + " " + resource + " to [color=\"" + playerColor + "\"]" + player.name + + "[/color]. Shift-click to tribute " + (amount < 500 ? 500 : amount + 500) + "."; +} Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 13200) +++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 13201) @@ -1,1123 +1,1123 @@