Index: binaries/data/mods/mod/gui/common/SDL_events_codes.js =================================================================== --- binaries/data/mods/mod/gui/common/SDL_events_codes.js +++ binaries/data/mods/mod/gui/common/SDL_events_codes.js @@ -0,0 +1,11 @@ +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; \ No newline at end of file Index: binaries/data/mods/public/gui/session/hotkeys/misc.xml =================================================================== --- binaries/data/mods/public/gui/session/hotkeys/misc.xml +++ binaries/data/mods/public/gui/session/hotkeys/misc.xml @@ -106,11 +106,11 @@ - findIdleUnit(g_MilitaryTypes); + g_IdleUnits.findUnit(g_MilitaryTypes); - findIdleUnit(["!Domestic"]); + g_IdleUnits.findUnit(["!Domestic"]); Index: binaries/data/mods/public/gui/session/hotkeys/training.xml =================================================================== --- binaries/data/mods/public/gui/session/hotkeys/training.xml +++ binaries/data/mods/public/gui/session/hotkeys/training.xml @@ -2,36 +2,36 @@ - addTrainingByPosition(0); + g_BatchTraining.addTrainingByPosition(0); - addTrainingByPosition(1); + g_BatchTraining.addTrainingByPosition(1); - addTrainingByPosition(2); + g_BatchTraining.addTrainingByPosition(2); - addTrainingByPosition(3); + g_BatchTraining.addTrainingByPosition(3); - addTrainingByPosition(4); + g_BatchTraining.addTrainingByPosition(4); - addTrainingByPosition(5); + g_BatchTraining.addTrainingByPosition(5); - addTrainingByPosition(6); + g_BatchTraining.addTrainingByPosition(6); Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -1,97 +1,58 @@ -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 +var g_MouseIsOverObject = false; +var g_Mouse = new Vector2D(); -const ACTION_NONE = 0; -const ACTION_GARRISON = 1; -const ACTION_REPAIR = 2; -const ACTION_GUARD = 3; -const ACTION_PATROL = 4; -var preSelectedAction = ACTION_NONE; +var g_FreehandSelection = { + /** + * Containing the ingame position which span the line. + */ + "inputLine": [], -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; -const INPUT_UNIT_POSITION_START = 11; -const INPUT_UNIT_POSITION = 12; + /** + * Minimum squared distance when a mouse move is called a drag. + */ + "resolutionInputLineSquared": 1, -var inputState = INPUT_NORMAL; + /** + * To start the freehandSelection function you need a minimum number of units. + * Minimum must be 2, for better performance you could set it higher. + */ + "minLengthOfLine": 8, -const INVALID_ENTITY = 0; + /** + * Number of pixels the mouse can move before the action is considered a drag + */ + "minNumberOfUnits": 2 +}; -var mouseX = 0; -var mouseY = 0; -var mouseIsOverObject = false; +var g_Drag = { + /** + * Number of pixels the mouse can move before the action is considered a drag. + */ + "maxDelta": 4, -/** - * Containing the ingame position which span the line. - */ -var g_FreehandSelection_InputLine = []; + /** + * Used for remembering mouse coordinates at start of drag operations. + */ + "start": undefined +}; /** - * Minimum squared distance when a mouse move is called a drag. + * Same double-click behaviour for hotkey presses */ -const g_FreehandSelection_ResolutionInputLineSquared = 1; +var g_DoublePress = { + "time" : 500, + "timer": 0 +}; +var g_PrevHotkey = 0; -/** - * Minimum length a dragged line should have to use the freehand selection. - */ -const g_FreehandSelection_MinLengthOfLine = 8; - -/** - * To start the freehandSelection function you need a minimum number of units. - * Minimum must be 2, for better performance you could set it higher. - */ -const g_FreehandSelection_MinNumberOfUnits = 2; - -/** - * Number of pixels the mouse can move before the action is considered a drag. - */ -const g_MaxDragDelta = 4; - -/** - * Used for remembering mouse coordinates at start of drag operations. - */ -var g_DragStart; - -/** - * Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. - * If any mousedown or mouseup of a sequence of clicks lands on a unit, - * that unit will be selected, which makes it easier to click on moving units. - */ -var clickedEntity = INVALID_ENTITY; - -// Same double-click behaviour for hotkey presses -const doublePressTime = 500; -var doublePressTimer = 0; -var prevHotkey = 0; - function updateCursorAndTooltip() { - var cursorSet = false; - var tooltipSet = false; - var informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); - if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION)) + let cursorSet = false; + let tooltipSet = false; + let informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); + if (!g_MouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION)) { - let action = determineAction(mouseX, mouseY); + let action = determineAction(g_Mouse.x, g_Mouse.y); if (action) { if (action.cursor) @@ -114,7 +75,7 @@ if (!tooltipSet) informationTooltip.hidden = true; - var placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); + let placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); if (placementSupport.tooltipMessage) placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip"; @@ -132,7 +93,7 @@ { if (placementSupport.template && placementSupport.position) { - var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { + let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, @@ -148,13 +109,13 @@ { if (result.message && result.parameters) { - var message = result.message; + let message = result.message; if (result.translateMessage) if (result.pluralMessage) message = translatePlural(result.message, result.pluralMessage, result.pluralCount); else message = translate(message); - var parameters = result.parameters; + let parameters = result.parameters; if (result.translateParameters) translateObjectKeys(parameters, result.translateParameters); placementSupport.tooltipMessage = sprintf(message, parameters); @@ -166,14 +127,14 @@ { // building can be placed here, and has an attack // show the range advantage in the tooltip - var cmd = { + let cmd = { "x": placementSupport.position.x, "z": placementSupport.position.z, "range": placementSupport.attack.Ranged.maxRange, "elevationBonus": placementSupport.attack.Ranged.elevationBonus, }; - var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); - var range = Math.round(cmd.range); + let averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); + let range = Math.round(cmd.range); placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); } @@ -209,7 +170,7 @@ */ function determineAction(x, y, fromMinimap) { - var selection = g_Selection.toList(); + let selection = g_Selection.toList(); // No action if there's no selection if (!selection.length) @@ -219,13 +180,13 @@ } // If the selection doesn't exist, no action - var entState = GetEntityState(selection[0]); + let entState = GetEntityState(selection[0]); if (!entState) return undefined; // If the selection isn't friendly units, no action - var allOwnedByPlayer = selection.every(ent => { - var entState = GetEntityState(ent); + let allOwnedByPlayer = selection.every(ent => { + let entState = GetEntityState(ent); return entState && entState.player == g_ViewedPlayer; }); @@ -232,10 +193,10 @@ if (!g_DevSettings.controlAll && !allOwnedByPlayer) return undefined; - var target = undefined; + let target = undefined; if (!fromMinimap) { - var ent = Engine.PickEntityAtPoint(x, y); + let ent = Engine.PickEntityAtPoint(x, y); if (ent != INVALID_ENTITY) target = ent; } @@ -243,16 +204,16 @@ // decide between the following ordered actions // if two actions are possible, the first one is taken // so the most specific should appear first - var actions = Object.keys(g_UnitActions).slice(); + let actions = Object.keys(g_UnitActions).slice(); actions.sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness); - var actionInfo = undefined; + let actionInfo = undefined; if (preSelectedAction != ACTION_NONE) { - for (var action of actions) + for (let action of actions) if (g_UnitActions[action].preSelectedActionCheck) { - var r = g_UnitActions[action].preSelectedActionCheck(target, selection); + let r = g_UnitActions[action].preSelectedActionCheck(target, selection); if (r) return r; } @@ -260,18 +221,18 @@ return { "type": "none", "cursor": "", "target": target }; } - for (var action of actions) + for (let action of actions) if (g_UnitActions[action].hotkeyActionCheck) { - var r = g_UnitActions[action].hotkeyActionCheck(target, selection); + let r = g_UnitActions[action].hotkeyActionCheck(target, selection); if (r) return r; } - for (var action of actions) + for (let action of actions) if (g_UnitActions[action].actionCheck) { - var r = g_UnitActions[action].actionCheck(target, selection); + let r = g_UnitActions[action].actionCheck(target, selection); if (r) return r; } @@ -294,7 +255,7 @@ return false; } - var selection = g_Selection.toList(); + let selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "construct", @@ -326,7 +287,7 @@ return false; } - var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) + let wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) { error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo)); @@ -336,23 +297,23 @@ 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, + let selection = g_Selection.toList(); + let cmd = { + "type" : "construct-wall", + "autorepair" : true, + "autocontinue" : true, + "queued" : queued, + "entities" : selection, + "wallSet" : placementSupport.wallSet, + "pieces" : wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, - "endSnappedEntity": wallPlacementInfo.endSnappedEnt, + "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; + let hasWallSegment = false; for (let piece of cmd.pieces) { if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( @@ -378,8 +339,8 @@ function updateBandbox(bandbox, ev, hidden) { let scale = +Engine.ConfigDB_GetValue("user", "gui.scale"); - let vMin = Vector2D.min(g_DragStart, ev); - let vMax = Vector2D.max(g_DragStart, ev); + let vMin = Vector2D.min(g_Drag.start, ev); + let vMax = Vector2D.max(g_Drag.start, ev); bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale); bandbox.hidden = hidden; @@ -387,29 +348,31 @@ return [vMin.x, vMin.y, vMax.x, vMax.y]; } -// Define some useful unit filters for getPreferredEntities +/** + * Define some useful unit filters for getPreferredEntities + */ var unitFilters = { "isUnit": entity => { - var entState = GetEntityState(entity); + let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit"); }, "isDefensive": entity => { - var entState = GetEntityState(entity); + let entState = GetEntityState(entity); return entState && hasClass(entState, "Defensive"); }, "isMilitary": entity => { - var entState = GetEntityState(entity); + let entState = GetEntityState(entity); return entState && g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isNonMilitary": entity => { - var entState = GetEntityState(entity); + let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && !g_MilitaryTypes.some(c => hasClass(entState, c)); }, "isIdle": entity => { - var entState = GetEntityState(entity); + let entState = GetEntityState(entity); return entState && hasClass(entState, "Unit") && @@ -429,12 +392,14 @@ } }; -// Choose, inside a list of entities, which ones will be selected. -// We may use several entity filters, until one returns at least one element. +/** + * Choose, inside a list of entities, which ones will be selected. + * We may use several entity filters, until one returns at least one element. + */ function getPreferredEntities(ents) { // Default filters - var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; + let filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; // Handle hotkeys if (Engine.HotkeyIsPressed("selection.militaryonly")) @@ -446,8 +411,8 @@ if (Engine.HotkeyIsPressed("selection.woundedonly")) filters = [unitFilters.isWounded]; - var preferredEnts = []; - for (var i = 0; i < filters.length; ++i) + let preferredEnts = []; + for (let i = 0; i < filters.length; ++i) { preferredEnts = ents.filter(filters[i]); if (preferredEnts.length) @@ -460,669 +425,27 @@ { if (GetSimState().cinemaPlaying) return false; - - // Capture mouse position so we can use it for displaying cursors, - // and key states - switch (ev.type) - { - case "mousebuttonup": - case "mousebuttondown": - case "mousemotion": - mouseX = ev.x; - mouseY = ev.y; - break; - } - + // Remember whether the mouse is over a GUI object or not - mouseIsOverObject = (hoveredObject != null); + g_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(); + g_InputBreforeGui.onInput[ev.type](ev); - // State-machine processing: - // - // (This is for states which should override the normal GUI processing - events will - // be processed here before being passed on, and propagation will stop if this function - // returns true) - // - // TODO: it'd probably be nice to have a better state-machine system, with guaranteed - // entry/exit functions, since this is a bit broken now - - switch (inputState) - { - case INPUT_BANDBOXING: - var bandbox = Engine.GetGUIObjectByName("bandbox"); - switch (ev.type) - { - case "mousemotion": - var rect = updateBandbox(bandbox, ev, false); - - var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); - var preferredEntities = getPreferredEntities(ents); - g_Selection.setHighlightList(preferredEntities); - - return false; - - case "mousebuttonup": - if (ev.button == SDL_BUTTON_LEFT) - { - var rect = updateBandbox(bandbox, ev, true); - - // Get list of entities limited to preferred entities - var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); - - // Remove the bandbox hover highlighting - g_Selection.setHighlightList([]); - - // Update the list of selected units - if (Engine.HotkeyIsPressed("selection.add")) - { - g_Selection.addList(ents); - } - else if (Engine.HotkeyIsPressed("selection.remove")) - { - g_Selection.removeList(ents); - } - else - { - g_Selection.reset(); - g_Selection.addList(ents); - } - - inputState = INPUT_NORMAL; - return true; - } - else if (ev.button == SDL_BUTTON_RIGHT) - { - // Cancel selection - bandbox.hidden = true; - - g_Selection.setHighlightList([]); - - inputState = INPUT_NORMAL; - return true; - } - break; - } - break; - - case INPUT_UNIT_POSITION: - switch (ev.type) - { - case "mousemotion": - return positionUnitsFreehandSelectionMouseMove(ev); - case "mousebuttonup": - return positionUnitsFreehandSelectionMouseUp(ev); - } - break; - - case INPUT_BUILDING_CLICK: - switch (ev.type) - { - case "mousemotion": - // If the mouse moved far enough from the original click location, - // then switch to drag-orientation mode - let maxDragDelta = 16; - if (g_DragStart.distanceTo(ev) >= maxDragDelta) - { - inputState = INPUT_BUILDING_DRAG; - return false; - } - break; - - case "mousebuttonup": - if (ev.button == SDL_BUTTON_LEFT) - { - // If shift is down, let the player continue placing another of the same building - var queued = Engine.HotkeyIsPressed("session.queue"); - if (tryPlaceBuilding(queued)) - { - if (queued) - inputState = INPUT_BUILDING_PLACEMENT; - else - inputState = INPUT_NORMAL; - } - else - { - inputState = INPUT_BUILDING_PLACEMENT; - } - return true; - } - break; - - case "mousebuttondown": - if (ev.button == SDL_BUTTON_RIGHT) - { - // Cancel building - placementSupport.Reset(); - inputState = INPUT_NORMAL; - return true; - } - break; - } - break; - - case INPUT_BUILDING_WALL_CLICK: - // User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point - // by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. - switch (ev.type) - { - case "mousebuttonup": - if (ev.button === SDL_BUTTON_LEFT) - { - inputState = INPUT_BUILDING_WALL_PATHING; - return true; - } - break; - - case "mousebuttondown": - if (ev.button == SDL_BUTTON_RIGHT) - { - // Cancel building - placementSupport.Reset(); - updateBuildingPlacementPreview(); - - inputState = INPUT_NORMAL; - return true; - } - break; - } - break; - - case INPUT_BUILDING_WALL_PATHING: - // User has chosen a starting point for constructing the wall, and is now looking to set the endpoint. - // Right-clicking cancels wall building mode, left-clicking sets the endpoint and builds the wall and returns to - // normal input mode. Optionally, shift + left-clicking does not return to normal input, and instead allows the - // user to continue building walls. - switch (ev.type) - { - case "mousemotion": - placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); - - // Update the building placement preview, and by extension, the list of snapping candidate entities for both (!) - // the ending point and the starting point to snap to. - // - // TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case - // where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a - // foundation and has been replaced with a completed entity since the user first chose it. Fetching all towers on - // the entire map instead of only the current screen might get expensive fast since walls all have a ton of towers - // in them. Might be useful to query only for entities within a certain range around the starting point and ending - // points. - - placementSupport.wallSnapEntitiesIncludeOffscreen = true; - var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates - - if (result && result.cost) - { - var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost }); - placementSupport.tooltipMessage = [ - getEntityCostTooltip(result), - getNeededResourcesTooltip(neededResources) - ].filter(tip => tip).join("\n"); - } - - break; - - case "mousebuttondown": - if (ev.button == SDL_BUTTON_LEFT) - { - var queued = Engine.HotkeyIsPressed("session.queue"); - if (tryPlaceWall(queued)) - { - if (queued) - { - // continue building, just set a new starting position where we left off - placementSupport.position = placementSupport.wallEndPosition; - placementSupport.wallEndPosition = undefined; - - inputState = INPUT_BUILDING_WALL_CLICK; - } - else - { - placementSupport.Reset(); - inputState = INPUT_NORMAL; - } - } - else - placementSupport.tooltipMessage = translate("Cannot build wall here!"); - - updateBuildingPlacementPreview(); - return true; - } - else if (ev.button == SDL_BUTTON_RIGHT) - { - // reset to normal input mode - placementSupport.Reset(); - updateBuildingPlacementPreview(); - - inputState = INPUT_NORMAL; - return true; - } - break; - } - break; - - case INPUT_BUILDING_DRAG: - switch (ev.type) - { - case "mousemotion": - let maxDragDelta = 16; - if (g_DragStart.distanceTo(ev) >= maxDragDelta) - { - // Rotate in the direction of the mouse - placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); - } - else - { - // If the mouse is near the center, snap back to the default orientation - placementSupport.SetDefaultAngle(); - } - - var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { - "template": placementSupport.template, - "x": placementSupport.position.x, - "z": placementSupport.position.z - }); - 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") - { - g_FlushTributing(); - inputState = INPUT_NORMAL; - } - break; - - case INPUT_BATCHTRAINING: - if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") - { - flushTrainingBatch(); - inputState = INPUT_NORMAL; - } - break; - } - - return false; + return !!g_InputBreforeGui[inputState] && + !!g_InputBreforeGui[inputState][ev.type] && + !!g_InputBreforeGui[inputState][ev.type](ev); } - + function handleInputAfterGui(ev) { if (GetSimState().cinemaPlaying) return false; - if (ev.hotkey === undefined) - ev.hotkey = null; + g_InputAfterGui.onInput[ev.type](ev); - // Handle the time-warp testing features, restricted to single-player - if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked) - { - if (ev.type == "hotkeydown" && ev.hotkey == "session.timewarp.fastforward") - Engine.SetSimRate(20.0); - else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward") - Engine.SetSimRate(1.0); - else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind") - Engine.RewindTimeWarp(); - } - - if (ev.hotkey == "session.highlightguarding") - { - g_ShowGuarding = (ev.type == "hotkeydown"); - updateAdditionalHighlight(); - } - else if (ev.hotkey == "session.highlightguarded") - { - g_ShowGuarded = (ev.type == "hotkeydown"); - updateAdditionalHighlight(); - } - - if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) - clickedEntity = INVALID_ENTITY; - - // State-machine processing: - - switch (inputState) - { - case INPUT_NORMAL: - switch (ev.type) - { - case "mousemotion": - // Highlight the first hovered entity (if any) - var ent = Engine.PickEntityAtPoint(ev.x, ev.y); - if (ent != INVALID_ENTITY) - g_Selection.setHighlightList([ent]); - else - g_Selection.setHighlightList([]); - - return false; - - case "mousebuttondown": - if (ev.button == SDL_BUTTON_LEFT) - { - g_DragStart = new Vector2D(ev.x, ev.y); - inputState = INPUT_SELECTING; - // If a single click occured, reset the clickedEntity. - // Also set it if we're double/triple clicking and missed the unit earlier. - if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY) - clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); - return true; - } - else if (ev.button == SDL_BUTTON_RIGHT) - { - g_DragStart = new Vector2D(ev.x, ev.y); - inputState = INPUT_UNIT_POSITION_START; - } - break; - - case "hotkeydown": - if (ev.hotkey.indexOf("selection.group.") == 0) - { - let now = Date.now(); - if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey) - { - if (ev.hotkey.indexOf("selection.group.select.") == 0) - { - var sptr = ev.hotkey.split("."); - performGroup("snap", sptr[3]); - } - } - else - { - var sptr = ev.hotkey.split("."); - performGroup(sptr[2], sptr[3]); - - doublePressTimer = now; - prevHotkey = ev.hotkey; - } - } - break; - } - break; - - case INPUT_PRESELECTEDACTION: - switch (ev.type) - { - case "mousemotion": - // Highlight the first hovered entity (if any) - var ent = Engine.PickEntityAtPoint(ev.x, ev.y); - if (ent != INVALID_ENTITY) - g_Selection.setHighlightList([ent]); - else - g_Selection.setHighlightList([]); - - return false; - - case "mousebuttondown": - if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) - { - var action = determineAction(ev.x, ev.y); - if (!action) - break; - if (!Engine.HotkeyIsPressed("session.queue")) - { - preSelectedAction = ACTION_NONE; - inputState = INPUT_NORMAL; - } - return doAction(action, ev); - } - else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) - { - preSelectedAction = ACTION_NONE; - inputState = INPUT_NORMAL; - break; - } - // else - default: - // Slight hack: If selection is empty, reset the input state - if (g_Selection.toList().length == 0) - { - preSelectedAction = ACTION_NONE; - inputState = INPUT_NORMAL; - break; - } - } - break; - - case INPUT_SELECTING: - switch (ev.type) - { - case "mousemotion": - // If the mouse moved further than a limit, switch to bandbox mode - if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta) - { - inputState = INPUT_BANDBOXING; - return false; - } - - var ent = Engine.PickEntityAtPoint(ev.x, ev.y); - if (ent != INVALID_ENTITY) - g_Selection.setHighlightList([ent]); - else - g_Selection.setHighlightList([]); - return false; - - case "mousebuttonup": - if (ev.button == SDL_BUTTON_LEFT) - { - if (clickedEntity == INVALID_ENTITY) - clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); - // Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event. - if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity)) - { - clickedEntity = INVALID_ENTITY; - if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) - { - g_Selection.reset(); - resetIdleUnit(); - } - inputState = INPUT_NORMAL; - return true; - } - - // If camera following and we select different unit, stop - if (Engine.GetFollowedEntity() != clickedEntity) - Engine.CameraFollow(0); - - var ents = []; - if (ev.clicks == 1) - ents = [clickedEntity]; - else - { - // Double click or triple click has occurred - var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); - var matchRank = true; - var templateToMatch; - - // Check for double click or triple click - if (ev.clicks == 2) - { - // Select similar units regardless of rank - templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName; - if (templateToMatch) - matchRank = false; - else - // No selection group name defined, so fall back to exact match - templateToMatch = GetEntityState(clickedEntity).template; - - } - else - // Triple click - // Select units matching exact template name (same rank) - templateToMatch = GetEntityState(clickedEntity).template; - - // TODO: Should we handle "control all units" here as well? - ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); - } - - // Update the list of selected units - if (Engine.HotkeyIsPressed("selection.add")) - g_Selection.addList(ents); - else if (Engine.HotkeyIsPressed("selection.remove")) - g_Selection.removeList(ents); - else - { - g_Selection.reset(); - g_Selection.addList(ents); - } - - inputState = INPUT_NORMAL; - return true; - } - break; - } - break; - - case INPUT_UNIT_POSITION_START: - switch (ev.type) - { - case "mousemotion": - // If the mouse moved further than a limit, switch to unit position mode - if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta)) - { - inputState = INPUT_UNIT_POSITION; - return false; - } - break; - case "mousebuttonup": - inputState = INPUT_NORMAL; - if (ev.button == SDL_BUTTON_RIGHT) - { - let action = determineAction(ev.x, ev.y); - if (action) - return doAction(action, ev); - } - break; - } - break; - - case INPUT_BUILDING_PLACEMENT: - switch (ev.type) - { - case "mousemotion": - - placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); - - if (placementSupport.mode === "wall") - { - // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is - // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities - // itself happens in the call to updateBuildingPlacementPreview below). - placementSupport.wallSnapEntitiesIncludeOffscreen = false; - } - else - { - // cancel if not enough resources - if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost })) - { - placementSupport.Reset(); - inputState = INPUT_NORMAL; - return true; - } - - var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { - "template": placementSupport.template, - "x": placementSupport.position.x, - "z": placementSupport.position.z, - }); - 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); - g_DragStart = new Vector2D(ev.x, ev.y); - inputState = INPUT_BUILDING_CLICK; - } - return true; - } - else if (ev.button == SDL_BUTTON_RIGHT) - { - // Cancel building - placementSupport.Reset(); - inputState = INPUT_NORMAL; - return true; - } - break; - - case "hotkeydown": - - var rotation_step = Math.PI / 12; // 24 clicks make a full rotation - - switch (ev.hotkey) - { - case "session.rotate.cw": - placementSupport.angle += rotation_step; - updateBuildingPlacementPreview(); - break; - case "session.rotate.ccw": - placementSupport.angle -= rotation_step; - updateBuildingPlacementPreview(); - break; - } - break; - - } - break; - } - return false; + return !!g_InputAfterGui[inputState] && + !!g_InputAfterGui[inputState][ev.type] && + !!g_InputAfterGui[inputState][ev.type](ev); } function doAction(action, ev) @@ -1132,9 +455,9 @@ // If shift is down, add the order to the unit's order queue instead // of running it immediately - var orderone = Engine.HotkeyIsPressed("session.orderone"); - var queued = Engine.HotkeyIsPressed("session.queue"); - var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); + let orderone = Engine.HotkeyIsPressed("session.orderone"); + let queued = Engine.HotkeyIsPressed("session.queue"); + let target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); if (g_UnitActions[action.type] && g_UnitActions[action.type].execute) { @@ -1165,10 +488,10 @@ // Converting the input line into a List of points. // For better performance the points must have a minimum distance to each other. let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); - if (!g_FreehandSelection_InputLine.length || - target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= - g_FreehandSelection_ResolutionInputLineSquared) - g_FreehandSelection_InputLine.push(target); + if (!g_FreehandSelection.inputLine.length || + target.distanceToSquared(g_FreehandSelection.inputLine[g_FreehandSelection.inputLine.length - 1]) >= + g_FreehandSelection.resolutionInputLineSquared) + g_FreehandSelection.inputLine.push(target); return false; } @@ -1175,8 +498,8 @@ function positionUnitsFreehandSelectionMouseUp(ev) { inputState = INPUT_NORMAL; - let inputLine = g_FreehandSelection_InputLine; - g_FreehandSelection_InputLine = []; + let inputLine = g_FreehandSelection.inputLine; + g_FreehandSelection.inputLine = []; if (ev.button != SDL_BUTTON_RIGHT) return true; @@ -1187,7 +510,7 @@ let selection = g_Selection.toList().filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); // Checking the line for a minimum length to save performance. - if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) + if (lengthOfLine < g_FreehandSelection.minLengthOfLine || selection.length < g_FreehandSelection.minNumberOfUnits) { let action = determineAction(ev.x, ev.y); return !!action && doAction(action, ev); @@ -1251,14 +574,14 @@ if (inputState != INPUT_NORMAL) return false; - var fromMinimap = true; - var action = determineAction(undefined, undefined, fromMinimap); + let fromMinimap = true; + let action = determineAction(undefined, undefined, fromMinimap); if (!action) return false; - var selection = g_Selection.toList(); + let selection = g_Selection.toList(); - var queued = Engine.HotkeyIsPressed("session.queue"); + let queued = Engine.HotkeyIsPressed("session.queue"); if (g_UnitActions[action.type] && g_UnitActions[action.type].execute) return g_UnitActions[action.type].execute(target, action, selection, queued); error("Invalid action.type " + action.type); @@ -1289,9 +612,11 @@ return ret; } -// Called by GUI when user clicks construction button -// @param buildTemplate Template name of the entity the user wants to build -function startBuildingPlacement(buildTemplate, playerState) +/** + * Called by GUI when user clicks construction button + * @param buildTemplate Template name of the entity the user wants to build + */ + function startBuildingPlacement(buildTemplate, playerState) { if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) return; @@ -1303,7 +628,7 @@ placementSupport.Reset(); // find out if we're building a wall, and change the entity appropriately if so - var templateData = GetTemplateData(buildTemplate); + let templateData = GetTemplateData(buildTemplate); if (templateData.wallSet) { placementSupport.mode = "wall"; @@ -1326,32 +651,6 @@ } } -// Batch training: -// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING -// When the user releases shift, or clicks on a different training button, we create the batched units -var g_BatchTrainingEntities; -var g_BatchTrainingType; -var g_NumberOfBatches; -var g_BatchTrainingEntityAllowedCount; -var g_BatchSize = getDefaultBatchTrainingSize(); - -function OnTrainMouseWheel(dir) -{ - if (Engine.HotkeyIsPressed("session.batchtrain")) - g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio"); - if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize)) - g_BatchSize = 1; -} - -function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) -{ - return entitiesToCheck.filter(entity => { - let state = GetEntityState(entity); - return state && state.production && state.production.entities.length && - state.production.entities.indexOf(trainEntType) != -1; - }); -} - function getDefaultBatchTrainingSize() { let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize"); @@ -1358,187 +657,216 @@ return Number.isInteger(num) && num > 0 ? num : 5; } -function getBatchTrainingSize() -{ - return Math.max(Math.round(g_BatchSize), 1); -} +/** + * Batch training: + * When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING + * When the user releases shift, or clicks on a different training button, we create the batched units + */ +var g_BatchTraining = { + "entities" : undefined, + "type" : undefined, + "numberOfBatches" : undefined, + "entityAllowedCount": undefined, + "batchSize" : getDefaultBatchTrainingSize(), + "OnTrainMouseWheel" : function (dir) + { + if (Engine.HotkeyIsPressed("session.batchtrain")) + this.batchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio"); + if (this.batchSize < 1 || !Number.isFinite(this.batchSize)) + this.batchSize = 1; + }, + "getBuildingsWhichCanTrainEntity": function (entitiesToCheck, trainEntType) + { + return entitiesToCheck.filter(entity => { + let state = GetEntityState(entity); + return state && state.production && state.production.entities.length && + state.production.entities.indexOf(trainEntType) != -1; + }); + }, + "getBatchTrainingSize": function () + { + return Math.max(Math.round(this.batchSize), 1); + }, + "updateDefaultBatchSize": function () + { + this.batchSize = getDefaultBatchTrainingSize(); + }, -function updateDefaultBatchSize() -{ - g_BatchSize = getDefaultBatchTrainingSize(); -} - -// Add the unit shown at position to the training queue for all entities in the selection -function addTrainingByPosition(position) -{ - let playerState = GetSimState().players[Engine.GetPlayerID()]; - let selection = g_Selection.toList(); - - if (!playerState || !selection.length) + /** + * Add the unit shown at position to the training queue for all entities in the selection + */ + "addTrainingByPosition": function (position) + { + let playerState = GetSimState().players[Engine.GetPlayerID()]; + let selection = g_Selection.toList(); + + if (!playerState || !selection.length) + return; + + let trainableEnts = getAllTrainableEntitiesFromSelection(); + + let entToTrain = trainableEnts[position]; + // When we have no building to train or the position is invalid + if (!entToTrain) + return; + + this.addTrainingToQueue(selection, entToTrain, playerState); return; + }, - let trainableEnts = getAllTrainableEntitiesFromSelection(); - - let entToTrain = trainableEnts[position]; - // When we have no building to train or the position is invalid - if (!entToTrain) - return; - - addTrainingToQueue(selection, entToTrain, playerState); - return; -} - -// Called by GUI when user clicks training button -function addTrainingToQueue(selection, trainEntType, playerState) -{ - let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); - - let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; - - let decrement = Engine.HotkeyIsPressed("selection.remove"); - let template; - if (!decrement) - template = GetTemplateData(trainEntType); - - // Batch training only possible if we can train at least 2 units - if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) + /** + * Called by GUI when user clicks training button + */ + "addTrainingToQueue": function (selection, trainEntType, playerState) { - if (inputState == INPUT_BATCHTRAINING) + let appropriateBuildings = this.getBuildingsWhichCanTrainEntity(selection, trainEntType); + + let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; + + let decrement = Engine.HotkeyIsPressed("selection.remove"); + let template; + if (!decrement) + template = GetTemplateData(trainEntType); + + // Batch training only possible if we can train at least 2 units + if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) { - // Check if we are training in the same building(s) as the last batch - // NOTE: We just check if the arrays are the same and if the order is the same - // If the order changed, we have a new selection and we should create a new batch. - // If we're already creating a batch of this unit (in the same building(s)), then just extend it - // (if training limits allow) - if (g_BatchTrainingEntities.length == selection.length && - g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && - g_BatchTrainingType == trainEntType) + if (inputState == INPUT_BATCHTRAINING) { - if (decrement) + // Check if we are training in the same building(s) as the last batch + // NOTE: We just check if the arrays are the same and if the order is the same + // If the order changed, we have a new selection and we should create a new batch. + // If we're already creating a batch of this unit (in the same building(s)), then just extend it + // (if training limits allow) + if (this.entities.length == selection.length && + this.entities.every((ent, i) => ent == selection[i]) && + this.type == trainEntType) { - --g_NumberOfBatches; - if (g_NumberOfBatches <= 0) - inputState = INPUT_NORMAL; + if (decrement) + { + --this.numberOfBatches; + if (this.numberOfBatches <= 0) + inputState = INPUT_NORMAL; + } + else if (canBeAddedCount == undefined || + canBeAddedCount > this.numberOfBatches * this.getBatchTrainingSize() * appropriateBuildings.length) + { + if (Engine.GuiInterfaceCall("GetNeededResources", { + "cost": multiplyEntityCosts(template, (this.numberOfBatches + 1) * this.getBatchTrainingSize()) + })) + return; + + ++this.numberOfBatches; + } + this.entityAllowedCount = canBeAddedCount; + return; } - else if (canBeAddedCount == undefined || - canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) - { - if (Engine.GuiInterfaceCall("GetNeededResources", { - "cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) - })) - return; - - ++g_NumberOfBatches; - } - g_BatchTrainingEntityAllowedCount = canBeAddedCount; + // Otherwise start a new one + else if (!decrement) + g_BatchTraining.flushTrainingBatch(); + // fall through to create the new batch + } + + // Don't start a new batch if decrementing or unable to afford it. + if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost": + multiplyEntityCosts(template, this.getBatchTrainingSize()) })) return; - } - // Otherwise start a new one - else if (!decrement) - flushTrainingBatch(); - // fall through to create the new batch + + inputState = INPUT_BATCHTRAINING; + this.entities = selection; + this.type = trainEntType; + this.entityAllowedCount = canBeAddedCount; + this.numberOfBatches = 1; } + else + { + // Non-batched - just create a single entity in each building + // (but no more than entity limit allows) + let buildingsForTraining = appropriateBuildings; + if (canBeAddedCount !== undefined) + buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); + Engine.PostNetworkCommand({ + "type": "train", + "template": trainEntType, + "count": 1, + "entities": buildingsForTraining + }); + } + }, - // Don't start a new batch if decrementing or unable to afford it. - if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost": - multiplyEntityCosts(template, getBatchTrainingSize()) })) - return; - - inputState = INPUT_BATCHTRAINING; - g_BatchTrainingEntities = selection; - g_BatchTrainingType = trainEntType; - g_BatchTrainingEntityAllowedCount = canBeAddedCount; - g_NumberOfBatches = 1; - } - else + /** + * Returns the number of units that will be present in a batch if the user clicks + * the training button depending on the batch training modifier hotkey + */ + "getTrainingStatus": function (selection, trainEntType, playerState) { - // Non-batched - just create a single entity in each building - // (but no more than entity limit allows) - let buildingsForTraining = appropriateBuildings; - if (canBeAddedCount !== undefined) - buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); - Engine.PostNetworkCommand({ - "type": "train", - "template": trainEntType, - "count": 1, - "entities": buildingsForTraining - }); - } -} - -/** - * Returns the number of units that will be present in a batch if the user clicks - * the training button depending on the batch training modifier hotkey - */ -function getTrainingStatus(selection, trainEntType, playerState) -{ - let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); - let nextBatchTrainingCount = 0; - - let canBeAddedCount; - if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType) + let appropriateBuildings = this.getBuildingsWhichCanTrainEntity(selection, trainEntType); + let nextBatchTrainingCount = 0; + + let canBeAddedCount; + if (inputState == INPUT_BATCHTRAINING && this.type == trainEntType) + { + nextBatchTrainingCount = this.numberOfBatches * this.getBatchTrainingSize(); + canBeAddedCount = this.entityAllowedCount; + } + else + canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; + + // We need to calculate count after the next increment if it's possible + if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && + Engine.HotkeyIsPressed("session.batchtrain")) + nextBatchTrainingCount += this.getBatchTrainingSize(); + + nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); + + // If training limits don't allow us to train batchTrainingCount in each appropriate building + // train as many full batches as we can and remainer in one more building. + let buildingsCountToTrainFullBatch = appropriateBuildings.length; + let remainderToTrain = 0; + if (canBeAddedCount !== undefined && + canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) + { + buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); + remainderToTrain = canBeAddedCount % nextBatchTrainingCount; + } + + return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; + }, + "flushTrainingBatch": function () { - nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize(); - canBeAddedCount = g_BatchTrainingEntityAllowedCount; + let batchedSize = this.numberOfBatches * this.getBatchTrainingSize(); + let appropriateBuildings = this.getBuildingsWhichCanTrainEntity(this.entities, this.type); + // If training limits don't allow us to train batchedSize in each appropriate building + if (this.entityAllowedCount !== undefined && + this.entityAllowedCount < batchedSize * appropriateBuildings.length) + { + // Train as many full batches as we can + let buildingsCountToTrainFullBatch = Math.floor( this.entityAllowedCount / batchedSize); + Engine.PostNetworkCommand({ + "type": "train", + "entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), + "template": this.type, + "count": batchedSize + }); + + // Train remainer in one more building + Engine.PostNetworkCommand({ + "type": "train", + "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], + "template": this.type, + "count": this.entityAllowedCount % batchedSize + }); + } + else + Engine.PostNetworkCommand({ + "type": "train", + "entities": appropriateBuildings, + "template": this.type, + "count": batchedSize + }); } - else - canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; +}; - // We need to calculate count after the next increment if it's possible - if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && - Engine.HotkeyIsPressed("session.batchtrain")) - nextBatchTrainingCount += getBatchTrainingSize(); - - nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); - - // If training limits don't allow us to train batchTrainingCount in each appropriate building - // train as many full batches as we can and remainer in one more building. - let buildingsCountToTrainFullBatch = appropriateBuildings.length; - let remainderToTrain = 0; - if (canBeAddedCount !== undefined && - canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) - { - buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); - remainderToTrain = canBeAddedCount % nextBatchTrainingCount; - } - - return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; -} - -function flushTrainingBatch() -{ - let batchedSize = g_NumberOfBatches * getBatchTrainingSize(); - let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); - // If training limits don't allow us to train batchedSize in each appropriate building - if (g_BatchTrainingEntityAllowedCount !== undefined && - g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) - { - // Train as many full batches as we can - let buildingsCountToTrainFullBatch = Math.floor( g_BatchTrainingEntityAllowedCount / batchedSize); - Engine.PostNetworkCommand({ - "type": "train", - "entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), - "template": g_BatchTrainingType, - "count": batchedSize - }); - - // Train remainer in one more building - Engine.PostNetworkCommand({ - "type": "train", - "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], - "template": g_BatchTrainingType, - "count": g_BatchTrainingEntityAllowedCount % batchedSize - }); - } - else - Engine.PostNetworkCommand({ - "type": "train", - "entities": appropriateBuildings, - "template": g_BatchTrainingType, - "count": batchedSize - }); -} - function performGroup(action, groupId) { switch (action) @@ -1546,9 +874,9 @@ case "snap": case "select": case "add": - var toSelect = []; + let toSelect = []; g_Groups.update(); - for (var ent in g_Groups.groups[groupId].ents) + for (let ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); if (action != "add") @@ -1576,63 +904,63 @@ } } -var lastIdleUnit = 0; -var currIdleClassIndex = 0; -var lastIdleClasses = []; +var g_IdleUnits = { + "last" : 0, + "currClassIndex" : 0, + "lastClasses" : [], + "resetIdleUnit": function() + { + this.last = 0; + this.currClassIndex = 0; + this.lastClasses = []; + }, + "findUnit": function(classes) + { + let append = Engine.HotkeyIsPressed("selection.add"); + let selectall = Engine.HotkeyIsPressed("selection.offscreen"); -function resetIdleUnit() -{ - lastIdleUnit = 0; - currIdleClassIndex = 0; - lastIdleClasses = []; -} + // Reset the last idle unit, etc., if the selection type has changed. + if (selectall || classes.length != this.lastClasses.length || !classes.every((v,i) => v === this.lastClasses[i])) + this.resetIdleUnit(); + this.lastClasses = classes; -function findIdleUnit(classes) -{ - var append = Engine.HotkeyIsPressed("selection.add"); - var selectall = Engine.HotkeyIsPressed("selection.offscreen"); + let data = { + "viewedPlayer": g_ViewedPlayer, + "excludeUnits": append ? g_Selection.toList() : [], + // If the current idle class index is not 0, put the class at that index first. + "idleClasses": classes.slice(this.currClassIndex, classes.length).concat(classes.slice(0, this.currClassIndex)) + }; + if (!selectall) + { + data.limit = 1; + data.prevUnit = this.lastIdleUnit; + } - // Reset the last idle unit, etc., if the selection type has changed. - if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i])) - resetIdleUnit(); - lastIdleClasses = classes; + let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); + if (!idleUnits.length) + { + // TODO: display a message or play a sound to indicate no more idle units, or something + // Reset for next cycle + this.resetIdleUnit(); + return; + } - var data = { - "viewedPlayer": g_ViewedPlayer, - "excludeUnits": append ? g_Selection.toList() : [], - // If the current idle class index is not 0, put the class at that index first. - "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) - }; - if (!selectall) - { - data.limit = 1; - data.prevUnit = lastIdleUnit; - } + if (!append) + g_Selection.reset(); + g_Selection.addList(idleUnits); - var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); - if (!idleUnits.length) - { - // TODO: display a message or play a sound to indicate no more idle units, or something - // Reset for next cycle - resetIdleUnit(); - return; + if (selectall) + return; + + this.lastIdleUnit = idleUnits[0]; + let entityState = GetEntityState(this.lastIdleUnit); + let position = entityState.position; + if (position) + Engine.CameraMoveTo(position.x, position.z); + // Move the idle class index to the first class an idle unit was found for. + let indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); + this.currClassIndex = (this.currClassIndex + indexChange) % classes.length; } - - if (!append) - g_Selection.reset(); - g_Selection.addList(idleUnits); - - if (selectall) - return; - - lastIdleUnit = idleUnits[0]; - var entityState = GetEntityState(lastIdleUnit); - var position = entityState.position; - if (position) - Engine.CameraMoveTo(position.x, position.z); - // Move the idle class index to the first class an idle unit was found for. - var indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); - currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; } function clearSelection() @@ -1646,4 +974,3 @@ g_Selection.reset(); preSelectedAction = ACTION_NONE; } - Index: binaries/data/mods/public/gui/session/input_events.js =================================================================== --- binaries/data/mods/public/gui/session/input_events.js +++ binaries/data/mods/public/gui/session/input_events.js @@ -0,0 +1,690 @@ +const ACTION_NONE = 0; +const ACTION_GARRISON = 1; +const ACTION_REPAIR = 2; +const ACTION_GUARD = 3; +const ACTION_PATROL = 4; +var preSelectedAction = ACTION_NONE; + +const INPUT_NORMAL = 0; +const INPUT_SELECTING = 1; +const INPUT_BANDBOXING = 2; +const INPUT_BUILDING_PLACEMENT = 3; +const INPUT_BUILDING_CLICK = 4; +const INPUT_BUILDING_DRAG = 5; +const INPUT_BATCHTRAINING = 6; +const INPUT_PRESELECTEDACTION = 7; +const INPUT_BUILDING_WALL_CLICK = 8; +const INPUT_BUILDING_WALL_PATHING = 9; +const INPUT_MASSTRIBUTING = 10; +const INPUT_UNIT_POSITION_START = 11; +const INPUT_UNIT_POSITION = 12; + +const INVALID_ENTITY = 0; + +var inputState = INPUT_NORMAL; + +/** + * Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. + * If any mousedown or mouseup of a sequence of clicks lands on a unit, + * that unit will be selected, which makes it easier to click on moving units. + */ +var clickedEntity = INVALID_ENTITY; + +var g_InputBreforeGui = { + [INPUT_BANDBOXING]: + { + "mousemotion": function (ev) + { + let bandbox = Engine.GetGUIObjectByName("bandbox"); + let rect = updateBandbox(bandbox, ev, false); + + let ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); + let preferredEntities = getPreferredEntities(ents); + g_Selection.setHighlightList(preferredEntities); + + return false; + }, + "mousebuttonup": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT) + { + let bandbox = Engine.GetGUIObjectByName("bandbox"); + let rect = updateBandbox(bandbox, ev, true); + + // Get list of entities limited to preferred entities + let ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); + + // Remove the bandbox hover highlighting + g_Selection.setHighlightList([]); + + // Update the list of selected units + if (Engine.HotkeyIsPressed("selection.add")) + { + g_Selection.addList(ents); + } + else if (Engine.HotkeyIsPressed("selection.remove")) + { + g_Selection.removeList(ents); + } + else + { + g_Selection.reset(); + g_Selection.addList(ents); + } + + inputState = INPUT_NORMAL; + return true; + } + else if (ev.button == SDL_BUTTON_RIGHT) + { + // Cancel selection + bandbox.hidden = true; + + g_Selection.setHighlightList([]); + + inputState = INPUT_NORMAL; + return true; + } + } + }, + [INPUT_UNIT_POSITION]: + { + "mousemotion": function (ev) + { + return positionUnitsFreehandSelectionMouseMove(ev); + }, + "mousebuttonup": function (ev) + { + return positionUnitsFreehandSelectionMouseUp(ev); + } + }, + [INPUT_BUILDING_CLICK]: + { + "mousemotion": function (ev) + { // If the mouse moved far enough from the original click location, + // then switch to drag-orientation mode + let maxDragDelta = 16; + if (g_Drag.start.distanceTo(ev) >= maxDragDelta) + { + inputState = INPUT_BUILDING_DRAG; + return false; + } + }, + "mousebuttonup": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT) + { + // If shift is down, let the player continue placing another of the same building + let 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; + } + }, + "mousebuttondown": function (ev) + { + if (ev.button == SDL_BUTTON_RIGHT) + { + // Cancel building + placementSupport.Reset(); + inputState = INPUT_NORMAL; + return true; + } + } + }, + [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. + "mousebuttonup": function (ev) + { + if (ev.button === SDL_BUTTON_LEFT) + { + inputState = INPUT_BUILDING_WALL_PATHING; + return true; + } + }, + "mousebuttondown": function (ev) + { + if (ev.button == SDL_BUTTON_RIGHT) + { + // Cancel building + placementSupport.Reset(); + updateBuildingPlacementPreview(); + + inputState = INPUT_NORMAL; + return true; + } + } + }, + [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. + "mousemotion": function (ev) + { + 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; + let result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates + + if (result && result.cost) + { + let neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost }); + placementSupport.tooltipMessage = [ + getEntityCostTooltip(result), + getNeededResourcesTooltip(neededResources) + ].filter(tip => tip).join("\n"); + } + + }, + "mousebuttondown": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT) + { + let queued = Engine.HotkeyIsPressed("session.queue"); + if (tryPlaceWall(queued)) + { + if (queued) + { + // continue building, just set a new starting position where we left off + placementSupport.position = placementSupport.wallEndPosition; + placementSupport.wallEndPosition = undefined; + + inputState = INPUT_BUILDING_WALL_CLICK; + } + else + { + placementSupport.Reset(); + inputState = INPUT_NORMAL; + } + } + else + placementSupport.tooltipMessage = translate("Cannot build wall here!"); + + updateBuildingPlacementPreview(); + return true; + } + else if (ev.button == SDL_BUTTON_RIGHT) + { + // reset to normal input mode + placementSupport.Reset(); + updateBuildingPlacementPreview(); + + inputState = INPUT_NORMAL; + return true; + } + } + }, + [INPUT_BUILDING_DRAG]: + { + "mousemotion": function (ev) + { + let maxDragDelta = 16; + if (g_Drag.start.distanceTo(ev) >= maxDragDelta) + { + // Rotate in the direction of the mouse + placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); + } + else + { + // If the mouse is near the center, snap back to the default orientation + placementSupport.SetDefaultAngle(); + } + + let 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(); + }, + "mousebuttonup": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT) + { + // If shift is down, let the player continue placing another of the same building + let 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; + } + }, + "mousebuttondown": function (ev) + { + if (ev.button == SDL_BUTTON_RIGHT) + { + // Cancel building + placementSupport.Reset(); + inputState = INPUT_NORMAL; + return true; + } + } + }, + [INPUT_MASSTRIBUTING]: + { + "hotkeyup": function (ev) + { + if (ev.hotkey == "session.masstribute") + { + g_FlushTributing(); + inputState = INPUT_NORMAL; + } + }, + }, + [INPUT_BATCHTRAINING]: + { + "hotkeyup": function (ev) + { + if (ev.hotkey == "session.batchtrain") + { + g_BatchTraining.flushTrainingBatch(); + inputState = INPUT_NORMAL; + } + } + }, + "onInput": new Proxy( + { + // Capture mouse position so we can use it for displaying cursors, + // and key states + "mousemotion": function (ev) + { + g_Mouse.set(ev.x,ev.y); + }, + "mousebuttonup": function (ev) + { + g_Mouse.set(ev.x,ev.y); + // Close the menu when interacting with the game world + if (!g_MouseIsOverObject && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) + closeMenu(); + }, + "mousebuttondown": function (ev) + { + g_Mouse.set(ev.x,ev.y); + // Close the menu when interacting with the game world + if (!g_MouseIsOverObject && (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) + closeMenu(); + }, + "default": function (ev) { } + }, + { + get: (object, property) => !!object[property] ? object[property] : object.default + }) +}; + +var g_InputAfterGui = { + [INPUT_NORMAL]: + { + "mousemotion": function (ev) + { + // Highlight the first hovered entity (if any) + let ent = Engine.PickEntityAtPoint(ev.x, ev.y); + if (ent != INVALID_ENTITY) + g_Selection.setHighlightList([ent]); + else + g_Selection.setHighlightList([]); + + return false; + }, + "mousebuttondown": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT) + { + g_Drag.start = new Vector2D(ev.x, ev.y); + inputState = INPUT_SELECTING; + // If a single click occured, reset the clickedEntity. + // Also set it if we're double/triple clicking and missed the unit earlier. + if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY) + clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); + return true; + } + else if (ev.button == SDL_BUTTON_RIGHT) + { + g_Drag.start = new Vector2D(ev.x, ev.y); + inputState = INPUT_UNIT_POSITION_START; + } + }, + "hotkeydown": function (ev) + { + if (ev.hotkey.indexOf("selection.group.") == 0) + { + let now = Date.now(); + if (now - g_DoublePress.timer < g_DoublePress.time && ev.hotkey == g_PrevHotkey) + { + if (ev.hotkey.indexOf("selection.group.select.") == 0) + { + let sptr = ev.hotkey.split("."); + performGroup("snap", sptr[3]); + } + } + else + { + let sptr = ev.hotkey.split("."); + performGroup(sptr[2], sptr[3]); + + g_DoublePress.timer = now; + g_PrevHotkey = ev.hotkey; + } + } + } + }, + [INPUT_PRESELECTEDACTION]: new Proxy( + { + "mousemotion": function (ev) + { + // Highlight the first hovered entity (if any) + let ent = Engine.PickEntityAtPoint(ev.x, ev.y); + if (ent != INVALID_ENTITY) + g_Selection.setHighlightList([ent]); + else + g_Selection.setHighlightList([]); + + return false; + }, + "mousebuttondown": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) + { + let action = determineAction(ev.x, ev.y); + if (!action) + return; + if (!Engine.HotkeyIsPressed("session.queue")) + { + preSelectedAction = ACTION_NONE; + inputState = INPUT_NORMAL; + } + return doAction(action, ev); + } + else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) + { + preSelectedAction = ACTION_NONE; + inputState = INPUT_NORMAL; + return; + } + }, + "default": function (ev) + { + // Slight hack: If selection is empty, reset the input state + if (g_Selection.toList().length == 0) + { + preSelectedAction = ACTION_NONE; + inputState = INPUT_NORMAL; + } + } + }, + { + get: function(object, property) + { + if (!!object[property]) + return object[property]; + + return object.default; + } + }), + [INPUT_SELECTING]: + { + "mousemotion": function (ev) + { + // If the mouse moved further than a limit, switch to bandbox mode + if (g_Drag.start.distanceTo(ev) >= g_Drag.maxDelta) + { + inputState = INPUT_BANDBOXING; + return false; + } + + let ent = Engine.PickEntityAtPoint(ev.x, ev.y); + if (ent != INVALID_ENTITY) + g_Selection.setHighlightList([ent]); + else + g_Selection.setHighlightList([]); + return false; + }, + "mousebuttonup": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT) + { + if (clickedEntity == INVALID_ENTITY) + clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); + // Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event. + if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity)) + { + clickedEntity = INVALID_ENTITY; + if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) + { + g_Selection.reset(); + g_IdleUnits.resetIdleUnit(); + } + inputState = INPUT_NORMAL; + return true; + } + + // If camera following and we select different unit, stop + if (Engine.GetFollowedEntity() != clickedEntity) + Engine.CameraFollow(0); + + let ents = []; + if (ev.clicks == 1) + ents = [clickedEntity]; + else + { + // Double click or triple click has occurred + let showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); + let matchRank = true; + let templateToMatch; + + // Check for double click or triple click + if (ev.clicks == 2) + { + // Select similar units regardless of rank + templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName; + if (templateToMatch) + matchRank = false; + else + // No selection group name defined, so fall back to exact match + templateToMatch = GetEntityState(clickedEntity).template; + + } + else + // Triple click + // Select units matching exact template name (same rank) + templateToMatch = GetEntityState(clickedEntity).template; + + // TODO: Should we handle "control all units" here as well? + ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); + } + + // Update the list of selected units + if (Engine.HotkeyIsPressed("selection.add")) + g_Selection.addList(ents); + else if (Engine.HotkeyIsPressed("selection.remove")) + g_Selection.removeList(ents); + else + { + g_Selection.reset(); + g_Selection.addList(ents); + } + + inputState = INPUT_NORMAL; + return true; + } + } + }, + [INPUT_UNIT_POSITION_START]: + { + "mousemotion": function (ev) + { + // If the mouse moved further than a limit, switch to unit position mode + if (g_Drag.start.distanceToSquared(ev) >= Math.square(g_Drag.maxDelta)) + { + inputState = INPUT_UNIT_POSITION; + return false; + } + }, + "mousebuttonup": function (ev) + { + inputState = INPUT_NORMAL; + if (ev.button == SDL_BUTTON_RIGHT) + { + let action = determineAction(ev.x, ev.y); + if (action) + return doAction(action, ev); + } + } + }, + [INPUT_BUILDING_PLACEMENT]: + { + "mousemotion": function (ev) + { + placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); + + if (placementSupport.mode === "wall") + { + // Including only the on-screen towers in the next snap candidate list is sufficient here, since the user is + // still selecting a starting point (which must necessarily be on-screen). (The update of the snap entities + // itself happens in the call to updateBuildingPlacementPreview below). + placementSupport.wallSnapEntitiesIncludeOffscreen = false; + } + else + { + // cancel if not enough resources + if (placementSupport.template && Engine.GuiInterfaceCall("GetNeededResources", { "cost": GetTemplateData(placementSupport.template).cost })) + { + placementSupport.Reset(); + inputState = INPUT_NORMAL; + return true; + } + + let 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 + }, + "mousebuttondown": function (ev) + { + if (ev.button == SDL_BUTTON_LEFT) + { + if (placementSupport.mode === "wall") + { + let validPlacement = updateBuildingPlacementPreview(); + if (validPlacement !== false) + inputState = INPUT_BUILDING_WALL_CLICK; + } + else + { + placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); + g_Drag.start = new Vector2D(ev.x, ev.y); + inputState = INPUT_BUILDING_CLICK; + } + return true; + } + + else if (ev.button == SDL_BUTTON_RIGHT) + { + // Cancel building + placementSupport.Reset(); + inputState = INPUT_NORMAL; + return true; + } + }, + "hotkeydown": function (ev) + { + let rotation_step = Math.PI / 12; // 24 clicks make a full rotation + + switch (ev.hotkey) + { + case "session.rotate.cw": + placementSupport.angle += rotation_step; + updateBuildingPlacementPreview(); + break; + case "session.rotate.ccw": + placementSupport.angle -= rotation_step; + updateBuildingPlacementPreview(); + break; + } + } + }, + "onInput" : new Proxy( + { + "default": function(ev) + { + if (ev.hotkey === undefined) + ev.hotkey = null; + + // Handle the time-warp testing features, restricted to single-player + if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked) + { + if (ev.type == "hotkeydown" && ev.hotkey == "session.timewarp.fastforward") + Engine.SetSimRate(20.0); + else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward") + Engine.SetSimRate(1.0); + else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind") + Engine.RewindTimeWarp(); + } + + if (ev.hotkey == "session.highlightguarding") + { + g_ShowGuarding = (ev.type == "hotkeydown"); + updateAdditionalHighlight(); + } + else if (ev.hotkey == "session.highlightguarded") + { + g_ShowGuarded = (ev.type == "hotkeydown"); + updateAdditionalHighlight(); + } + + if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) + clickedEntity = INVALID_ENTITY; + } + }, + { + get: (object, property) => !!object[property] ? object[property] : object.default + }) +}; \ No newline at end of file Index: binaries/data/mods/public/gui/session/minimap_panel.xml =================================================================== --- binaries/data/mods/public/gui/session/minimap_panel.xml +++ binaries/data/mods/public/gui/session/minimap_panel.xml @@ -27,7 +27,7 @@ sprite_over="stretched:session/minimap-idle-highlight.png" sprite_disabled="stretched:session/minimap-idle-disabled.png" > - findIdleUnit(g_WorkerTypes); + g_IdleUnits.findUnit(g_WorkerTypes); Index: binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels.js +++ binaries/data/mods/public/gui/session/selection_panels.js @@ -943,7 +943,7 @@ let unitIds = data.unitEntStates.map(status => status.id) let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] = - getTrainingStatus(unitIds, data.item, data.playerState); + g_BatchTraining.getTrainingStatus(unitIds, data.item, data.playerState); let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch; @@ -956,7 +956,7 @@ data.button.onPress = function() { if (!neededResources) - addTrainingToQueue(unitIds, data.item, data.playerState); + g_BatchTraining.addTrainingToQueue(unitIds, data.item, data.playerState); }; data.button.onPressRight = function() { showTemplateDetails(data.item); Index: binaries/data/mods/public/gui/session/selection_panels_right/training_panel.xml =================================================================== --- binaries/data/mods/public/gui/session/selection_panels_right/training_panel.xml +++ binaries/data/mods/public/gui/session/selection_panels_right/training_panel.xml @@ -8,10 +8,10 @@ - OnTrainMouseWheel(1); + g_BatchTraining.OnTrainMouseWheel(1); - OnTrainMouseWheel(-1); + g_BatchTraining.OnTrainMouseWheel(-1);