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 @@ -30,6 +30,8 @@ const INPUT_BUILDING_WALL_CLICK = 8; const INPUT_BUILDING_WALL_PATHING = 9; const INPUT_MASSTRIBUTING = 10; +const INPUT_RIGHT = 11; +const INPUT_UNIT_POSITION = 12; var inputState = INPUT_NORMAL; @@ -40,6 +42,20 @@ var mouseIsOverObject = false; /** + * Containing the points which are picturing the line + */ +var g_FreehandSelection_InputLine = []; + +const g_FreehandSelection_ResolutionInputLine = 1; +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; @@ -539,6 +555,16 @@ } 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) { @@ -831,10 +857,8 @@ } else if (ev.button == SDL_BUTTON_RIGHT) { - var action = determineAction(ev.x, ev.y); - if (!action) - break; - return doAction(action, ev); + g_DragStart = new Vector2D(ev.x, ev.y); + inputState = INPUT_RIGHT; } break; @@ -996,6 +1020,29 @@ } break; + case INPUT_RIGHT: + switch (ev.type) + { + case "mousemotion": + // If the mouse moved further than a limit, switch to unit position mode + if (g_DragStart.distanceTo(ev) >= 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) { @@ -1120,6 +1167,82 @@ return false; } +function positionUnitsFreehandSelectionMouseMove(ev) +{ + // Converting the input line into a List of points + // For better performance the points must have a minimum distance to each other + let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); + if (!g_FreehandSelection_InputLine.length || + target.distanceTo(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= g_FreehandSelection_ResolutionInputLine) + g_FreehandSelection_InputLine.push(target); + return false; +} + +function positionUnitsFreehandSelectionMouseUp(ev) +{ + inputState = INPUT_NORMAL; + let inputLine = g_FreehandSelection_InputLine; + g_FreehandSelection_InputLine = []; + if (!ev.button == SDL_BUTTON_RIGHT) + return true; + + let lengthOfLine = 0; + for (let i = 1; i < inputLine.length; ++i) + lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); + + let selection = g_Selection.toList().filter(ent => GetEntityState(ent).unitAI).sort((a, b) => a - b); + + // Checking the line for a minimum length to save performance + if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) + { + let action = determineAction(ev.x, ev.y); + return action && doAction(action, ev); + } + + // Even distribution of the units on the line + let p0 = inputLine[0]; + let entityDistribution = [p0]; + let distanceBetweenEnts = lengthOfLine / (selection.length - 1); + let freeDist = -distanceBetweenEnts; + + for (let i = 1; i < inputLine.length; ++i) + { + let p1 = inputLine[i]; + freeDist += inputLine[i - 1].distanceTo(p1); + + while (freeDist >= 0) + { + p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); + entityDistribution.push(p0); + freeDist -= distanceBetweenEnts; + } + } + + // Rounding errors can lead to a missing point, or too much points + if (selection.length > entityDistribution.length) + entityDistribution.push(inputLine[inputLine.length - 1]); + if (selection.length < entityDistribution.length) + entityDistribution.splice(selection.length, entityDistribution.length - selection.length); + + let firstUnit = GetEntityState(selection[0]); + let lastUnit = GetEntityState(selection[selection.length - 1]); + let firstToFirstDist = Vector2D.from3D(firstUnit.position).distanceTo(entityDistribution[0]); + let firstToLastDist = Vector2D.from3D(firstUnit.position).distanceTo(entityDistribution[selection.length - 1]); + let lastToFirstDist = Vector2D.from3D(lastUnit.position).distanceTo(entityDistribution[0]); + let lastToLastDist = Vector2D.from3D(lastUnit.position).distanceTo(entityDistribution[selection.length - 1]); + + if (firstToFirstDist + lastToLastDist > firstToLastDist + lastToFirstDist) + entityDistribution.reverse(); + + Engine.PostNetworkCommand({ + "type": Engine.HotkeyIsPressed("session.attackmove") ? "attack-walk-custom" : "walk-custom", + "entities": selection, + "targetPositions": entityDistribution, + "queued": true + }); + return true; +} + function handleMinimapEvent(target) { // Partly duplicated from handleInputAfterGui(), but with the input being Index: binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Commands.js +++ binaries/data/mods/public/simulation/helpers/Commands.js @@ -152,6 +152,16 @@ }); }, + "walk-custom": function(player, cmd, data) + { + let selection = data.entities; + let target = cmd.targetPositions; + for (let ent in selection) + GetFormationUnitAIs([selection[ent]], player).forEach(cmpUnitAI => { + cmpUnitAI.Walk(target[ent].x, target[ent].y, cmd.queued); + }); + }, + "walk-to-range": function(player, cmd, data) { // Only used by the AI @@ -172,6 +182,17 @@ }); }, + "attack-walk-custom": function(player, cmd, data) + { + let selection = data.entities; + let target = cmd.targetPositions; + let allowCapture = cmd.allowCapture || cmd.allowCapture == null; + for (let ent in selection) + GetFormationUnitAIs([selection[ent]], player).forEach(cmpUnitAI => { + cmpUnitAI.WalkAndFight(target[ent].x, target[ent].y, cmd.targetClasses, allowCapture, cmd.queued); + }); + }, + "attack": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null;