Changeset View
Standalone View
binaries/data/mods/public/gui/session/input.js
Property | Old Value | New Value |
---|---|---|
svn:eol-style | native \ No newline at end of property | null |
const SDL_BUTTON_LEFT = 1; | const SDL_BUTTON_LEFT = 1; | ||||
elexis: `x, y` pairs -.- | |||||
Done Inline Actionsmissing quotes, double negation sounds unneeded here (the trick is usually used to avoid warnings if a property is absent and return false) elexis: missing quotes, double negation sounds unneeded here (the trick is usually used to avoid… | |||||
Done Inline ActionsThese two functions beforeGUI/afterGUI always expect a return true/false (because of c++ I think) nani: These two functions beforeGUI/afterGUI always expect a return true/false (because of c++ I… | |||||
Done Inline ActionsGUIManager.cpp if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev)) That is defined in the lovely NativeWrapperDefns.h. So bool handled is the return value. The return value is captured as a JS::Value in jsRet and then converted to the specified type in FromJSVal. FromJSVal<bool> is defined in ScriptConversions.cpp and it contains WARN_IF_NOT(v.isBoolean(), v);. I guess that's the reason why we need a bool? (I don't recall the JS Interface being so picky) elexis: `GUIManager.cpp`
```
if (top()->GetScriptInterface()->CallFunction(global… | |||||
Done Inline Actionswondering whether one could make that return x operator y; (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators) elexis: wondering whether one could make that `return x operator y;` (https://developer.mozilla.org/en… | |||||
Done Inline ActionsAnything is possible but why? nani: Anything is possible but why? | |||||
Done Inline ActionsShorter formulas can be easier to understand, here it's just equal. if (X) return false; return Y; <=> return X ? false : Y; <=> return !X ? Y : false; <=> return !X && Y; <=> return !g_InputEvents.isStateAfterGUI() && g_InputEvents.ProcessMessage({ "type": ev.type, "ev": ev }); So handleInputBeforeGui is truthy if !isStateAfterGUI && ProcessMessage, exactly like the functionname suggests. The reader doesn't have to infer this anymore, but can read it directly. Authors have the choice to leave code as concise, simple and clean as possible, and thus always try to resolve the equations, or they consciously chose to not even try but keep what works. That's the path to the dark side, spaghetti mess, and creating new works by copying and modifying previous works. elexis: Shorter formulas can be easier to understand, here it's just equal.
It's easy to dismiss… | |||||
Done Inline ActionsDefine bandboxObject just above the first use. elexis: Define bandboxObject just above the first use.
I suspect the value won't differ from `bandbox`… | |||||
Done Inline Actionswhere does that space come from? The linter? The parentheses are unneeded as far as I know. Also we use === only when we need to distinguish == from === (leaving us with a codebase that has`== in almost any case and only === for few cases where falsy values need to be differentiated, instead of people chosing === and ==` depending on the angle of the sun) elexis: where does that space come from? The linter? The parentheses are unneeded as far as I know. | |||||
Done Inline ActionsIdk from where that space came from. nani: Idk from where that space came from. | |||||
Done Inline ActionsSounds like the editor elexis: Sounds like the editor | |||||
Not Done Inline ActionsAvoid one-letter variables, because it makes the reader question what the content contains, what the reason was to for the author to chose this one particular letter (was it r for result?) instead of informing the reader what it means; and when the reader reads the later references to that one letter, is required to remember the definition instead of being able to evaluate the line by itself based on the variable name. (Yes those are only 2 lines, but it's an antipattern). Same for the (v, i) below. Either don't change the line, or remove all it's defects to minimize the total number of patches to the same line and maximize the quality per patch. Also you can unify the two if-statements using result = x.y && x.y(z). This function looks like it could be refactored, so that modders can insert new blocks in between the function without replacing the function, i.e. by moving every block to a separate object property and then handling that object here. Probably we don't want to do that now due to the complexity, so all of the syntax candy could be removed from this patch, making this patch shorter and then added to the refactoring one afterwards. Could. elexis: Avoid **one-letter variables**, because it makes the reader question what the content contains… | |||||
Done Inline ActionsI leave this for another patch. nani: I leave this for another patch.
Maybe all the var -> let change should too be another patch. | |||||
Not Done Inline Actionslet target; elexis: let target; | |||||
Done Inline ActionsI would have said remove the {}, but since you actually do change logic in this patch, I would rather advocate to split the parts that don't change anything, so that the reviewers of the proposal and the auditors of the commit are confronted with as little code as possible (trying to find the needle (bug) in the haystack is easier the less hay there is) elexis: I would have said remove the {}, but since you actually do change logic in this patch, I would… | |||||
Not Done Inline ActionsI'm not sure if it's a good idea to ever put something other than numbers into a Vector2D, since the entire prototype consists of functions that expect numbers. Better pass undefined if you want to express the absence of a value. elexis: I'm not sure if it's a good idea to ever put something other than numbers into a Vector2D… | |||||
Done Inline ActionsThe original were undefined too so I must follow as I only change them to a vector2d format nani: The original were undefined too so I must follow as I only change them to a vector2d format | |||||
Not Done Inline ActionsDoesn't change that Vector2D is designed for numbers. So determineAction now has the choice to assume or ensure that it will never PickEntityAtPoint with pos = undefined. If it would be called, then previously it would throw a warning, now it would throw an error, or a condition is added to account for pos = undefined. So unless it becomes clear that determineAction can never be call PickEntityAtPoint with an pos = undefined, then we even discovered a defect in the previous code by making that a Vector2D. Looking at fromMinimap, it actually that does become clear. If I'm not mistaken, we only need to pass a position variable if the user clicked on the minimap, or undefined if he didn't (women don't play). elexis: Doesn't change that Vector2D is designed for numbers.
Just pass `undefined` instead of… | |||||
Done Inline ActionsNice nani: Nice | |||||
Not Done Inline ActionsDidn't that object have the pattern { "ent": ent }? Then it would be a simple ents.map(ent => +ent) without Object.keys(). But again, better remove the syntax changes out and only do the logic changes here. If you want to please the linter with all the var/let and whitespace changes, make a patch that only contains these, so as to make it a very simple task to read. elexis: Didn't that object have the pattern { "ent": ent }? Then it would be a simple `ents.map(ent =>… | |||||
Not Done Inline Actionsthe two lines can be merged elexis: the two lines can be merged | |||||
Not Done Inline Actions
elexis: 1. Oh noooo, I remember this function contains an anti-treasure
2. Comments above code, so that… | |||||
Not Done Inline Actionsanti-treasure ? nani: anti-treasure ?
| |||||
Not Done Inline ActionsThe price of discovering it is reduced lifetime elexis: The price of discovering it is reduced lifetime | |||||
Not Done Inline Actionsnull is useful to distinguish undefined in a condition. Regardless, !hoveredObject is even shorter. I thought it was an unused variable, but it's a global. I didn't check the number of references to mouseIsOverObject, but it should be correted if it's not many. Even better than making it a global would be to make it a property of the prototype instance. One may consider storing hoveredObject right away. At least it sounds slightly easier to keep in mind than mouseIsOverObject. hoveredObject refers to one thing, mouseIsOverObject refers to one thing and a property of that thing. elexis: `null` is useful to distinguish `undefined` in a condition.
Why use the value range `(object… | |||||
Not Done Inline Actionsunnecessary variable, inline elexis: unnecessary variable, inline | |||||
Done Inline Actions(x,y) don't exist anymore, also it's not clear whether those are screen coordinates or a map position. Should state that the position is the location that the user has clicked on the minimap. pos -> position (U dont rite like dis fo a reasn) Perhaps elexis: (x,y) don't exist anymore, also it's not clear whether those are screen coordinates or a map… | |||||
Not Done Inline Actions(Hiding these globals here at the bottom is terrible, someone with commit access should really move them to the top) elexis: (Hiding these globals here at the bottom is terrible, someone with commit access should really… | |||||
const SDL_BUTTON_MIDDLE = 2; | const SDL_BUTTON_MIDDLE = 2; | ||||
Done Inline ActionsIf the states are always used as complete strings (x ? "PLACEMENT.Y" : "PLACEMENT.X") then one can do a stringsearch for "PLACEMENT.X" and find all matches, making it a bit easier to trace it down. (Same goes for filenames. People often used "foo" + bar + ".png", so when someone searches for that image file, it's impossible to tell which places can use it, except the code is foo ? "foobar.png" : "foobaz.png") elexis: If the states are always used as complete strings (x ? "PLACEMENT.Y" : "PLACEMENT.X") then one… | |||||
Done Inline ActionsIndeed nani: Indeed | |||||
const SDL_BUTTON_RIGHT = 3; | const SDL_BUTTON_RIGHT = 3; | ||||
Done Inline ActionsNotice that this might mean that the array is created everytime the function is called (unless the JIT compiler is smart). In fact if there is a winning argument for having the array ["x", "y"], one could make it ["x", "y"].some(g_InputEvents.hasBaseState) (or .every), without the checks inside that function. Still think a simple || is the path through least complexity elexis: Notice that this might mean that the array is created everytime the function is called (unless… | |||||
Done Inline ActionsI suppose you are right. But the benefit of this built in method is that it checks if those states really exist (with warnIfStateNotDefined) so in the case someone wrote "PLAICNG" instead of the correct "PLACING" it will give a warning and a stack trace for better debugging. nani: I suppose you are right. But the benefit of this built in method is that it checks if those… | |||||
Done Inline ActionsWhether or not it warns is independent of whether the function supports arrays elexis: Whether or not it warns is independent of whether the function supports arrays | |||||
Done Inline ActionshasBaseState prototype provides with the desired solution (being able to compare against multiple states or only just one) and makes guarantees that the comparison is between states and not some random strings. nani: `hasBaseState` prototype provides with the desired solution (being able to compare against… | |||||
const SDLK_LEFTBRACKET = 91; | const SDLK_LEFTBRACKET = 91; | ||||
const SDLK_RIGHTBRACKET = 93; | const SDLK_RIGHTBRACKET = 93; | ||||
const SDLK_RSHIFT = 303; | const SDLK_RSHIFT = 303; | ||||
const SDLK_LSHIFT = 304; | const SDLK_LSHIFT = 304; | ||||
const SDLK_RCTRL = 305; | const SDLK_RCTRL = 305; | ||||
const SDLK_LCTRL = 306; | const SDLK_LCTRL = 306; | ||||
const SDLK_RALT = 307; | const SDLK_RALT = 307; | ||||
const SDLK_LALT = 308; | const SDLK_LALT = 308; | ||||
// TODO: these constants should be defined somewhere else instead, in | // TODO: these constants should be defined somewhere else instead, in | ||||
// case any other code wants to use them too | // case any other code wants to use them too | ||||
const ACTION_NONE = 0; | const ACTION_NONE = "ACTION_NONE"; | ||||
const ACTION_GARRISON = 1; | const ACTION_GARRISON = "ACTION_GARRISON"; | ||||
const ACTION_REPAIR = 2; | const ACTION_REPAIR = "ACTION_REPAIR"; | ||||
const ACTION_GUARD = 3; | const ACTION_GUARD = "ACTION_GUARD"; | ||||
const ACTION_PATROL = 4; | const ACTION_PATROL = "ACTION_PATROL"; | ||||
var preSelectedAction = ACTION_NONE; | |||||
Done Inline ActionsWhy is this moved to a separate function far away from the caller when all the other code remains inlined? elexis: Why is this moved to a separate function far away from the caller when all the other code… | |||||
Done Inline Actions
nani: - Because it doesn't affect the flow of the FSM (change of state )
- What other code ? | |||||
Done Inline ActionsThe function is called from L215.
Usually one moves code to a new function to break a long function into smaller parts, so as to make them easier to understand separately. If it's really only the absence of an FSM state that favors moving it, there are some other functions that also don't change the FSM state. elexis: The function is called from L215.
The function could be defined in L118 or L460, but it's at… | |||||
var preSelectedAction = ACTION_NONE; | |||||
const INPUT_NORMAL = 0; | |||||
const INPUT_SELECTING = 1; | const INVALID_ENTITY = 0; | ||||
const INPUT_BANDBOXING = 2; | |||||
const INPUT_BUILDING_PLACEMENT = 3; | var g_Mouse = new Vector2D(0, 0); | ||||
const INPUT_BUILDING_CLICK = 4; | var mouseIsOverObject = false; | ||||
const INPUT_BUILDING_DRAG = 5; | |||||
const INPUT_BATCHTRAINING = 6; | /** | ||||
const INPUT_PRESELECTEDACTION = 7; | * Containing the ingame position which span the line. | ||||
const INPUT_BUILDING_WALL_CLICK = 8; | */ | ||||
const INPUT_BUILDING_WALL_PATHING = 9; | var g_FreehandSelection_InputLine = []; | ||||
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. | ||||
*/ | |||||
var inputState = INPUT_NORMAL; | const g_FreehandSelection_ResolutionInputLineSquared = 1; | ||||
const INVALID_ENTITY = 0; | /** | ||||
* Minimum length a dragged line should have to use the freehand selection. | |||||
var mouseX = 0; | */ | ||||
var mouseY = 0; | const g_FreehandSelection_MinLengthOfLine = 8; | ||||
var mouseIsOverObject = false; | |||||
/** | |||||
/** | * To start the freehandSelection function you need a minimum number of units. | ||||
* Containing the ingame position which span the line. | * Minimum must be 2, for better performance you could set it higher. | ||||
*/ | */ | ||||
var g_FreehandSelection_InputLine = []; | const g_FreehandSelection_MinNumberOfUnits = 2; | ||||
/** | /** | ||||
* Minimum squared distance when a mouse move is called a drag. | * Number of pixels the mouse can move before the action is considered a drag. | ||||
*/ | */ | ||||
const g_FreehandSelection_ResolutionInputLineSquared = 1; | const g_MaxDragDelta = 4; | ||||
/** | /** | ||||
* Minimum length a dragged line should have to use the freehand selection. | * Used for remembering mouse coordinates at start of drag operations. | ||||
*/ | */ | ||||
const g_FreehandSelection_MinLengthOfLine = 8; | var g_DragStart; | ||||
/** | /** | ||||
* To start the freehandSelection function you need a minimum number of units. | * Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. | ||||
* Minimum must be 2, for better performance you could set it higher. | * 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. | ||||
const g_FreehandSelection_MinNumberOfUnits = 2; | */ | ||||
var clickedEntity = INVALID_ENTITY; | |||||
/** | |||||
* Number of pixels the mouse can move before the action is considered a drag. | // Same double-click behaviour for hotkey presses | ||||
*/ | const doublePressTime = 500; | ||||
const g_MaxDragDelta = 4; | var doublePressTimer = 0; | ||||
var prevHotkey = 0; | |||||
/** | |||||
* Used for remembering mouse coordinates at start of drag operations. | function updateCursorAndTooltip() | ||||
*/ | { | ||||
var g_DragStart; | let cursorSet = false; | ||||
let tooltipSet = false; | |||||
/** | let informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); | ||||
* Store the clicked entity on mousedown or mouseup for single/double/triple clicks to select entities. | if (!mouseIsOverObject && g_InputEvents.hasBaseState(["NORMAL", "PRESELECTEDACTION"])) | ||||
* 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. | let action = determineAction(g_Mouse.x, g_Mouse.y); | ||||
*/ | if (action) | ||||
var clickedEntity = INVALID_ENTITY; | { | ||||
if (action.cursor) | |||||
// Same double-click behaviour for hotkey presses | { | ||||
const doublePressTime = 500; | Engine.SetCursor(action.cursor); | ||||
var doublePressTimer = 0; | cursorSet = true; | ||||
var prevHotkey = 0; | } | ||||
if (action.tooltip) | |||||
function updateCursorAndTooltip() | { | ||||
{ | tooltipSet = true; | ||||
var cursorSet = false; | informationTooltip.caption = action.tooltip; | ||||
var tooltipSet = false; | informationTooltip.hidden = false; | ||||
var informationTooltip = Engine.GetGUIObjectByName("informationTooltip"); | } | ||||
if (!mouseIsOverObject && (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION)) | } | ||||
{ | } | ||||
let action = determineAction(mouseX, mouseY); | |||||
if (action) | if (!cursorSet) | ||||
{ | Engine.ResetCursor(); | ||||
if (action.cursor) | |||||
{ | if (!tooltipSet) | ||||
Engine.SetCursor(action.cursor); | informationTooltip.hidden = true; | ||||
cursorSet = true; | |||||
} | let placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); | ||||
if (action.tooltip) | if (placementSupport.tooltipMessage) | ||||
{ | placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip"; | ||||
tooltipSet = true; | |||||
informationTooltip.caption = action.tooltip; | placementTooltip.caption = placementSupport.tooltipMessage || ""; | ||||
informationTooltip.hidden = false; | placementTooltip.hidden = !placementSupport.tooltipMessage; | ||||
} | } | ||||
} | |||||
} | function updateBuildingPlacementPreview() | ||||
{ | |||||
if (!cursorSet) | // The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or | ||||
Engine.ResetCursor(); | // 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 (!tooltipSet) | |||||
informationTooltip.hidden = true; | if (placementSupport.mode === "building") | ||||
{ | |||||
var placementTooltip = Engine.GetGUIObjectByName("placementTooltip"); | if (placementSupport.template && placementSupport.position) | ||||
if (placementSupport.tooltipMessage) | { | ||||
placementTooltip.sprite = placementSupport.tooltipError ? "BackgroundErrorTooltip" : "BackgroundInformationTooltip"; | let result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { | ||||
"template": placementSupport.template, | |||||
placementTooltip.caption = placementSupport.tooltipMessage || ""; | "x": placementSupport.position.x, | ||||
placementTooltip.hidden = !placementSupport.tooltipMessage; | "z": placementSupport.position.z, | ||||
} | "angle": placementSupport.angle, | ||||
"actorSeed": placementSupport.actorSeed | |||||
function updateBuildingPlacementPreview() | }); | ||||
{ | |||||
// The preview should be recomputed every turn, so that it responds to obstructions/fog/etc moving underneath it, or | // Show placement info tooltip if invalid position | ||||
// in the case of the wall previews, in response to new tower foundations getting constructed for it to snap to. | placementSupport.tooltipError = !result.success; | ||||
// See onSimulationUpdate in session.js. | placementSupport.tooltipMessage = ""; | ||||
if (placementSupport.mode === "building") | if (!result.success) | ||||
{ | { | ||||
if (placementSupport.template && placementSupport.position) | if (result.message && result.parameters) | ||||
{ | { | ||||
var result = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { | let message = result.message; | ||||
"template": placementSupport.template, | if (result.translateMessage) | ||||
"x": placementSupport.position.x, | if (result.pluralMessage) | ||||
"z": placementSupport.position.z, | message = translatePlural(result.message, result.pluralMessage, result.pluralCount); | ||||
"angle": placementSupport.angle, | else | ||||
"actorSeed": placementSupport.actorSeed | message = translate(message); | ||||
}); | let parameters = result.parameters; | ||||
if (result.translateParameters) | |||||
// Show placement info tooltip if invalid position | translateObjectKeys(parameters, result.translateParameters); | ||||
placementSupport.tooltipError = !result.success; | placementSupport.tooltipMessage = sprintf(message, parameters); | ||||
placementSupport.tooltipMessage = ""; | } | ||||
return false; | |||||
if (!result.success) | } | ||||
{ | |||||
if (result.message && result.parameters) | if (placementSupport.attack && placementSupport.attack.Ranged) | ||||
{ | { | ||||
var message = result.message; | // building can be placed here, and has an attack | ||||
if (result.translateMessage) | // show the range advantage in the tooltip | ||||
if (result.pluralMessage) | let cmd = { | ||||
message = translatePlural(result.message, result.pluralMessage, result.pluralCount); | "x": placementSupport.position.x, | ||||
else | "z": placementSupport.position.z, | ||||
message = translate(message); | "range": placementSupport.attack.Ranged.maxRange, | ||||
var parameters = result.parameters; | "elevationBonus": placementSupport.attack.Ranged.elevationBonus, | ||||
if (result.translateParameters) | }; | ||||
translateObjectKeys(parameters, result.translateParameters); | let averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); | ||||
placementSupport.tooltipMessage = sprintf(message, parameters); | let range = Math.round(cmd.range); | ||||
} | placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + | ||||
return false; | sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); | ||||
} | } | ||||
return true; | |||||
if (placementSupport.attack && placementSupport.attack.Ranged) | } | ||||
{ | } | ||||
// building can be placed here, and has an attack | else if (placementSupport.mode === "wall") | ||||
// show the range advantage in the tooltip | { | ||||
var cmd = { | if (placementSupport.wallSet && placementSupport.position) | ||||
"x": placementSupport.position.x, | { | ||||
"z": placementSupport.position.z, | // Fetch an updated list of snapping candidate entities | ||||
"range": placementSupport.attack.Ranged.maxRange, | placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities( | ||||
"elevationBonus": placementSupport.attack.Ranged.elevationBonus, | placementSupport.wallSet.templates.tower, | ||||
}; | placementSupport.wallSnapEntitiesIncludeOffscreen, | ||||
var averageRange = Math.round(Engine.GuiInterfaceCall("GetAverageRangeForBuildings", cmd) - cmd.range); | true, // require exact template match | ||||
var range = Math.round(cmd.range); | true // include foundations | ||||
placementSupport.tooltipMessage = sprintf(translatePlural("Basic range: %(range)s meter", "Basic range: %(range)s meters", range), { "range": range }) + "\n" + | ); | ||||
sprintf(translatePlural("Average bonus range: %(range)s meter", "Average bonus range: %(range)s meters", averageRange), { "range": averageRange }); | |||||
} | return Engine.GuiInterfaceCall("SetWallPlacementPreview", { | ||||
return true; | "wallSet": placementSupport.wallSet, | ||||
} | "start": placementSupport.position, | ||||
} | "end": placementSupport.wallEndPosition, | ||||
else if (placementSupport.mode === "wall") | "snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment | ||||
{ | }); | ||||
if (placementSupport.wallSet && placementSupport.position) | } | ||||
{ | } | ||||
// Fetch an updated list of snapping candidate entities | |||||
placementSupport.wallSnapEntities = Engine.PickSimilarPlayerEntities( | return false; | ||||
placementSupport.wallSet.templates.tower, | } | ||||
placementSupport.wallSnapEntitiesIncludeOffscreen, | |||||
true, // require exact template match | /** | ||||
true // include foundations | * Determine the context-sensitive action that should be performed when the mouse is at (x,y) | ||||
); | */ | ||||
function determineAction(x, y, fromMinimap) | |||||
return Engine.GuiInterfaceCall("SetWallPlacementPreview", { | { | ||||
"wallSet": placementSupport.wallSet, | let selection = g_Selection.toList(); | ||||
"start": placementSupport.position, | |||||
"end": placementSupport.wallEndPosition, | // No action if there's no selection | ||||
"snapEntities": placementSupport.wallSnapEntities, // snapping entities (towers) for starting a wall segment | if (!selection.length) | ||||
}); | { | ||||
} | preSelectedAction = ACTION_NONE; | ||||
} | return undefined; | ||||
} | |||||
return false; | |||||
} | // If the selection doesn't exist, no action | ||||
let entState = GetEntityState(selection[0]); | |||||
/** | if (!entState) | ||||
* Determine the context-sensitive action that should be performed when the mouse is at (x,y) | return undefined; | ||||
*/ | |||||
function determineAction(x, y, fromMinimap) | // If the selection isn't friendly units, no action | ||||
{ | let allOwnedByPlayer = selection.every(ent => | ||||
var selection = g_Selection.toList(); | { | ||||
let entState = GetEntityState(ent); | |||||
// No action if there's no selection | return entState && entState.player == g_ViewedPlayer; | ||||
if (!selection.length) | }); | ||||
{ | |||||
preSelectedAction = ACTION_NONE; | if (!g_DevSettings.controlAll && !allOwnedByPlayer) | ||||
return undefined; | return undefined; | ||||
} | |||||
let target = undefined; | |||||
// If the selection doesn't exist, no action | if (!fromMinimap) | ||||
var entState = GetEntityState(selection[0]); | { | ||||
if (!entState) | let ent = Engine.PickEntityAtPoint(x, y); | ||||
return undefined; | if (ent != INVALID_ENTITY) | ||||
target = ent; | |||||
// If the selection isn't friendly units, no action | } | ||||
var allOwnedByPlayer = selection.every(ent => { | |||||
var entState = GetEntityState(ent); | // decide between the following ordered actions | ||||
return entState && entState.player == g_ViewedPlayer; | // if two actions are possible, the first one is taken | ||||
}); | // so the most specific should appear first | ||||
let actions = Object.keys(g_UnitActions).slice(); | |||||
if (!g_DevSettings.controlAll && !allOwnedByPlayer) | actions.sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness); | ||||
return undefined; | |||||
let actionInfo = undefined; | |||||
var target = undefined; | if (preSelectedAction != ACTION_NONE) | ||||
if (!fromMinimap) | { | ||||
{ | for (let action of actions) | ||||
var ent = Engine.PickEntityAtPoint(x, y); | if (g_UnitActions[action].preSelectedActionCheck) | ||||
if (ent != INVALID_ENTITY) | { | ||||
target = ent; | let r = g_UnitActions[action].preSelectedActionCheck(target, selection); | ||||
} | if (r) | ||||
return r; | |||||
// decide between the following ordered actions | } | ||||
// if two actions are possible, the first one is taken | |||||
// so the most specific should appear first | return { "type": "none", "cursor": "", "target": target }; | ||||
var actions = Object.keys(g_UnitActions).slice(); | } | ||||
actions.sort((a, b) => g_UnitActions[a].specificness - g_UnitActions[b].specificness); | |||||
for (let action of actions) | |||||
var actionInfo = undefined; | if (g_UnitActions[action].hotkeyActionCheck) | ||||
if (preSelectedAction != ACTION_NONE) | { | ||||
{ | let r = g_UnitActions[action].hotkeyActionCheck(target, selection); | ||||
for (var action of actions) | if (r) | ||||
if (g_UnitActions[action].preSelectedActionCheck) | return r; | ||||
{ | } | ||||
var r = g_UnitActions[action].preSelectedActionCheck(target, selection); | |||||
if (r) | for (let action of actions) | ||||
return r; | if (g_UnitActions[action].actionCheck) | ||||
} | { | ||||
let r = g_UnitActions[action].actionCheck(target, selection); | |||||
return { "type": "none", "cursor": "", "target": target }; | if (r) | ||||
} | return r; | ||||
} | |||||
for (var action of actions) | |||||
if (g_UnitActions[action].hotkeyActionCheck) | return { "type": "none", "cursor": "", "target": target }; | ||||
{ | } | ||||
var r = g_UnitActions[action].hotkeyActionCheck(target, selection); | |||||
if (r) | function tryPlaceBuilding(queued) | ||||
return r; | { | ||||
} | if (placementSupport.mode !== "building") | ||||
{ | |||||
for (var action of actions) | error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'"); | ||||
if (g_UnitActions[action].actionCheck) | return false; | ||||
{ | } | ||||
var r = g_UnitActions[action].actionCheck(target, selection); | |||||
if (r) | if (!updateBuildingPlacementPreview()) | ||||
return r; | { | ||||
} | // invalid location - don't build it | ||||
// TODO: play a sound? | |||||
return { "type": "none", "cursor": "", "target": target }; | return false; | ||||
} | } | ||||
function tryPlaceBuilding(queued) | let selection = g_Selection.toList(); | ||||
{ | |||||
if (placementSupport.mode !== "building") | Engine.PostNetworkCommand({ | ||||
{ | "type": "construct", | ||||
error("tryPlaceBuilding expected 'building', got '" + placementSupport.mode + "'"); | "template": placementSupport.template, | ||||
return false; | "x": placementSupport.position.x, | ||||
} | "z": placementSupport.position.z, | ||||
"angle": placementSupport.angle, | |||||
if (!updateBuildingPlacementPreview()) | "actorSeed": placementSupport.actorSeed, | ||||
{ | "entities": selection, | ||||
// invalid location - don't build it | "autorepair": true, | ||||
// TODO: play a sound? | "autocontinue": true, | ||||
return false; | "queued": queued | ||||
} | }); | ||||
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); | |||||
var selection = g_Selection.toList(); | |||||
if (!queued) | |||||
Engine.PostNetworkCommand({ | placementSupport.Reset(); | ||||
"type": "construct", | else | ||||
"template": placementSupport.template, | placementSupport.RandomizeActorSeed(); | ||||
"x": placementSupport.position.x, | |||||
Done Inline ActionsNot sure if Coding conventions have a strong opinion about this. I only saw → https://trac.wildfiregames.com/wiki/Coding_Conventions "For any alignment within a line of code (as opposed to indentation at the start), use spaces, not tabs." Stan: Not sure if Coding conventions have a strong opinion about this. I only saw → https://trac. | |||||
"z": placementSupport.position.z, | return true; | ||||
"angle": placementSupport.angle, | } | ||||
"actorSeed": placementSupport.actorSeed, | |||||
"entities": selection, | function tryPlaceWall(queued) | ||||
"autorepair": true, | { | ||||
"autocontinue": true, | if (placementSupport.mode !== "wall") | ||||
"queued": queued | { | ||||
}); | error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'"); | ||||
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); | return false; | ||||
} | |||||
if (!queued) | |||||
placementSupport.Reset(); | let wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) | ||||
else | if (!(wallPlacementInfo === false || typeof (wallPlacementInfo) === "object")) | ||||
placementSupport.RandomizeActorSeed(); | { | ||||
error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo)); | |||||
return true; | return false; | ||||
} | } | ||||
function tryPlaceWall(queued) | if (!wallPlacementInfo) | ||||
{ | return false; | ||||
if (placementSupport.mode !== "wall") | |||||
{ | let selection = g_Selection.toList(); | ||||
error("tryPlaceWall expected 'wall', got '" + placementSupport.mode + "'"); | let cmd = { | ||||
return false; | "type": "construct-wall", | ||||
} | "autorepair": true, | ||||
"autocontinue": true, | |||||
var wallPlacementInfo = updateBuildingPlacementPreview(); // entities making up the wall (wall segments, towers, ...) | "queued": queued, | ||||
if (!(wallPlacementInfo === false || typeof(wallPlacementInfo) === "object")) | "entities": selection, | ||||
{ | "wallSet": placementSupport.wallSet, | ||||
error("Invalid updateBuildingPlacementPreview return value: " + uneval(wallPlacementInfo)); | "pieces": wallPlacementInfo.pieces, | ||||
return false; | "startSnappedEntity": wallPlacementInfo.startSnappedEnt, | ||||
} | "endSnappedEntity": wallPlacementInfo.endSnappedEnt, | ||||
}; | |||||
if (!wallPlacementInfo) | |||||
return false; | // 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 | |||||
var selection = g_Selection.toList(); | // (this is somewhat non-ideal and hardcode-ish) | ||||
var cmd = { | let hasWallSegment = false; | ||||
"type": "construct-wall", | for (let piece of cmd.pieces) | ||||
"autorepair": true, | { | ||||
"autocontinue": true, | if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( | ||||
"queued": queued, | { | ||||
"entities": selection, | hasWallSegment = true; | ||||
"wallSet": placementSupport.wallSet, | break; | ||||
"pieces": wallPlacementInfo.pieces, | } | ||||
"startSnappedEntity": wallPlacementInfo.startSnappedEnt, | } | ||||
"endSnappedEntity": wallPlacementInfo.endSnappedEnt, | |||||
}; | if (hasWallSegment) | ||||
{ | |||||
// make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end | Engine.PostNetworkCommand(cmd); | ||||
// point are too close together for the algorithm to place a wall segment inbetween, and only the towers are being previewed | Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); | ||||
// (this is somewhat non-ideal and hardcode-ish) | } | ||||
var hasWallSegment = false; | |||||
for (let piece of cmd.pieces) | return true; | ||||
{ | } | ||||
if (piece.template != cmd.wallSet.templates.tower) // TODO: hardcode-ish :( | |||||
{ | /** | ||||
hasWallSegment = true; | * Updates the bandbox object with new positions and visibility. | ||||
break; | * @returns {array} The coordinates of the vertices of the bandbox. | ||||
} | */ | ||||
} | function updateBandbox(bandbox, ev, hidden) | ||||
{ | |||||
if (hasWallSegment) | let bandboxObject = Engine.GetGUIObjectByName(bandbox) | ||||
{ | let scale = +Engine.ConfigDB_GetValue("user", "gui.scale"); | ||||
Engine.PostNetworkCommand(cmd); | let vMin = Vector2D.min(g_DragStart, ev); | ||||
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); | let vMax = Vector2D.max(g_DragStart, ev); | ||||
} | |||||
bandboxObject.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale); | |||||
return true; | bandboxObject.hidden = hidden; | ||||
} | |||||
return [vMin.x, vMin.y, vMax.x, vMax.y]; | |||||
/** | } | ||||
* Updates the bandbox object with new positions and visibility. | |||||
* @returns {array} The coordinates of the vertices of the bandbox. | // Define some useful unit filters for getPreferredEntities | ||||
*/ | var unitFilters = { | ||||
function updateBandbox(bandbox, ev, hidden) | "isUnit": entity => | ||||
{ | { | ||||
let scale = +Engine.ConfigDB_GetValue("user", "gui.scale"); | var entState = GetEntityState(entity); | ||||
let vMin = Vector2D.min(g_DragStart, ev); | return entState && hasClass(entState, "Unit"); | ||||
let vMax = Vector2D.max(g_DragStart, ev); | }, | ||||
"isDefensive": entity => | |||||
bandbox.size = new GUISize(vMin.x / scale, vMin.y / scale, vMax.x / scale, vMax.y / scale); | { | ||||
bandbox.hidden = hidden; | var entState = GetEntityState(entity); | ||||
return entState && hasClass(entState, "Defensive"); | |||||
return [vMin.x, vMin.y, vMax.x, vMax.y]; | }, | ||||
} | "isMilitary": entity => | ||||
{ | |||||
// Define some useful unit filters for getPreferredEntities | var entState = GetEntityState(entity); | ||||
var unitFilters = { | return entState && | ||||
"isUnit": entity => { | g_MilitaryTypes.some(c => hasClass(entState, c)); | ||||
var entState = GetEntityState(entity); | }, | ||||
return entState && hasClass(entState, "Unit"); | "isNonMilitary": entity => | ||||
}, | { | ||||
"isDefensive": entity => { | var entState = GetEntityState(entity); | ||||
var entState = GetEntityState(entity); | return entState && | ||||
Not Done Inline Actions@param {Array<number>} ents the list of entities Stan: @param {Array<number>} ents the list of entities | |||||
return entState && hasClass(entState, "Defensive"); | hasClass(entState, "Unit") && | ||||
}, | !g_MilitaryTypes.some(c => hasClass(entState, c)); | ||||
"isMilitary": entity => { | }, | ||||
var entState = GetEntityState(entity); | "isIdle": entity => | ||||
return entState && | { | ||||
g_MilitaryTypes.some(c => hasClass(entState, c)); | var entState = GetEntityState(entity); | ||||
}, | |||||
"isNonMilitary": entity => { | return entState && | ||||
var entState = GetEntityState(entity); | hasClass(entState, "Unit") && | ||||
return entState && | entState.unitAI && | ||||
hasClass(entState, "Unit") && | entState.unitAI.isIdle && | ||||
!g_MilitaryTypes.some(c => hasClass(entState, c)); | !hasClass(entState, "Domestic"); | ||||
}, | }, | ||||
"isIdle": entity => { | "isWounded": entity => | ||||
var entState = GetEntityState(entity); | { | ||||
let entState = GetEntityState(entity); | |||||
return entState && | return entState && | ||||
hasClass(entState, "Unit") && | hasClass(entState, "Unit") && | ||||
entState.unitAI && | entState.maxHitpoints && | ||||
entState.unitAI.isIdle && | 100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold"); | ||||
!hasClass(entState, "Domestic"); | }, | ||||
}, | "isAnything": entity => | ||||
"isWounded": entity => { | { | ||||
let entState = GetEntityState(entity); | return true; | ||||
return entState && | } | ||||
hasClass(entState, "Unit") && | }; | ||||
entState.maxHitpoints && | |||||
100 * entState.hitpoints <= entState.maxHitpoints * Engine.ConfigDB_GetValue("user", "gui.session.woundedunithotkeythreshold"); | // Choose, inside a list of entities, which ones will be selected. | ||||
}, | // We may use several entity filters, until one returns at least one element. | ||||
"isAnything": entity => { | function getPreferredEntities(ents) | ||||
return true; | { | ||||
} | // Default filters | ||||
}; | var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; | ||||
// Choose, inside a list of entities, which ones will be selected. | // Handle hotkeys | ||||
// We may use several entity filters, until one returns at least one element. | if (Engine.HotkeyIsPressed("selection.militaryonly")) | ||||
function getPreferredEntities(ents) | filters = [unitFilters.isMilitary]; | ||||
Done Inline ActionsI guess you meant Before ? Stan: I guess you meant Before ? | |||||
{ | if (Engine.HotkeyIsPressed("selection.nonmilitaryonly")) | ||||
// Default filters | filters = [unitFilters.isNonMilitary]; | ||||
var filters = [unitFilters.isUnit, unitFilters.isDefensive, unitFilters.isAnything]; | if (Engine.HotkeyIsPressed("selection.idleonly")) | ||||
filters = [unitFilters.isIdle]; | |||||
// Handle hotkeys | if (Engine.HotkeyIsPressed("selection.woundedonly")) | ||||
if (Engine.HotkeyIsPressed("selection.militaryonly")) | filters = [unitFilters.isWounded]; | ||||
filters = [unitFilters.isMilitary]; | |||||
if (Engine.HotkeyIsPressed("selection.nonmilitaryonly")) | var preferredEnts = []; | ||||
filters = [unitFilters.isNonMilitary]; | for (var i = 0; i < filters.length; ++i) | ||||
if (Engine.HotkeyIsPressed("selection.idleonly")) | { | ||||
filters = [unitFilters.isIdle]; | preferredEnts = ents.filter(filters[i]); | ||||
if (Engine.HotkeyIsPressed("selection.woundedonly")) | if (preferredEnts.length) | ||||
filters = [unitFilters.isWounded]; | break; | ||||
} | |||||
var preferredEnts = []; | return preferredEnts; | ||||
for (var i = 0; i < filters.length; ++i) | } | ||||
{ | |||||
preferredEnts = ents.filter(filters[i]); | /** | ||||
if (preferredEnts.length) | * State machine processing: | ||||
break; | * This is for states which should override the normal GUI processing. | ||||
} | * The events will be processed here before being passed on, and | ||||
return preferredEnts; | * propagation will stop if this function returns true. | ||||
} | */ | ||||
function handleInputBeforeGui(ev, hoveredObject) | |||||
function handleInputBeforeGui(ev, hoveredObject) | { | ||||
{ | if (GetSimState().cinemaPlaying) | ||||
Done Inline Actionsnever seen this syntax, just use quotes like everywhere else? elexis: never seen this syntax, just use quotes like everywhere else? | |||||
Done Inline Actionsnani: Is needed.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_i… | |||||
Done Inline ActionsAh, good to know about it! Not sure whether refactoring to not require them isn't preferable (I did't look at the code so I can't predict) elexis: Ah, good to know about it! Not sure whether refactoring to not require them isn't preferable (I… | |||||
if (GetSimState().cinemaPlaying) | return false; | ||||
return false; | |||||
// Capture mouse position so we can use it for displaying cursors, | |||||
// Capture mouse position so we can use it for displaying cursors, | // and key states | ||||
// and key states | switch (ev.type) | ||||
switch (ev.type) | { | ||||
{ | case "mousebuttonup": | ||||
case "mousebuttonup": | case "mousebuttondown": | ||||
case "mousebuttondown": | case "mousemotion": | ||||
case "mousemotion": | g_Mouse.set(ev.x, ev.y); | ||||
mouseX = ev.x; | break; | ||||
mouseY = ev.y; | } | ||||
break; | |||||
} | // Remember whether the mouse is over a GUI object or not | ||||
mouseIsOverObject = hoveredObject != null; | |||||
// Remember whether the mouse is over a GUI object or not | |||||
mouseIsOverObject = (hoveredObject != null); | if (g_InputEvents.isStateAfterGUI()) | ||||
return false; | |||||
// Close the menu when interacting with the game world | |||||
if (!mouseIsOverObject && (ev.type =="mousebuttonup" || ev.type == "mousebuttondown") | return !!g_InputEvents.ProcessMessage({ type: ev.type, ev: ev }) | ||||
&& (ev.button == SDL_BUTTON_LEFT || ev.button == SDL_BUTTON_RIGHT)) | } | ||||
closeMenu(); | |||||
function handleInputAfterGui(ev) | |||||
// State-machine processing: | { | ||||
// | if (GetSimState().cinemaPlaying) | ||||
// (This is for states which should override the normal GUI processing - events will | return false; | ||||
// be processed here before being passed on, and propagation will stop if this function | |||||
// returns true) | if (ev.hotkey === undefined) | ||||
// | ev.hotkey = null; | ||||
// 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 | // Handle the time-warp testing features, restricted to single-player | ||||
if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked) | |||||
switch (inputState) | { | ||||
{ | if (ev.type == "hotkeydown" && ev.hotkey == "session.timewarp.fastforward") | ||||
case INPUT_BANDBOXING: | Engine.SetSimRate(20.0); | ||||
var bandbox = Engine.GetGUIObjectByName("bandbox"); | else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward") | ||||
switch (ev.type) | Engine.SetSimRate(1.0); | ||||
{ | else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind") | ||||
case "mousemotion": | Engine.RewindTimeWarp(); | ||||
var rect = updateBandbox(bandbox, ev, false); | } | ||||
var ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer); | if (ev.hotkey == "session.highlightguarding") | ||||
var preferredEntities = getPreferredEntities(ents); | { | ||||
g_Selection.setHighlightList(preferredEntities); | g_ShowGuarding = (ev.type == "hotkeydown"); | ||||
updateAdditionalHighlight(); | |||||
return false; | } | ||||
else if (ev.hotkey == "session.highlightguarded") | |||||
case "mousebuttonup": | { | ||||
if (ev.button == SDL_BUTTON_LEFT) | g_ShowGuarded = (ev.type == "hotkeydown"); | ||||
{ | updateAdditionalHighlight(); | ||||
var rect = updateBandbox(bandbox, ev, true); | } | ||||
// Get list of entities limited to preferred entities | if (!g_InputEvents.isStateAfterGUI()) | ||||
var ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer)); | return false; | ||||
// Remove the bandbox hover highlighting | return !!g_InputEvents.ProcessMessage({ type: ev.type, ev: ev }) | ||||
g_Selection.setHighlightList([]); | } | ||||
// Update the list of selected units | function doAction(action, ev) | ||||
if (Engine.HotkeyIsPressed("selection.add")) | { | ||||
{ | if (!controlsPlayer(g_ViewedPlayer)) | ||||
g_Selection.addList(ents); | return false; | ||||
} | |||||
else if (Engine.HotkeyIsPressed("selection.remove")) | // If shift is down, add the order to the unit's order queue instead | ||||
{ | // of running it immediately | ||||
g_Selection.removeList(ents); | var orderone = Engine.HotkeyIsPressed("session.orderone"); | ||||
} | var queued = Engine.HotkeyIsPressed("session.queue"); | ||||
else | var target = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); | ||||
{ | |||||
g_Selection.reset(); | if (g_UnitActions[action.type] && g_UnitActions[action.type].execute) | ||||
g_Selection.addList(ents); | { | ||||
} | let selection = g_Selection.toList(); | ||||
if (orderone) | |||||
inputState = INPUT_NORMAL; | { | ||||
return true; | // pick the first unit that can do this order. | ||||
} | let unit = selection.find(entity => | ||||
else if (ev.button == SDL_BUTTON_RIGHT) | ["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method => | ||||
{ | g_UnitActions[action.type][method] && | ||||
// Cancel selection | g_UnitActions[action.type][method](action.target || undefined, [entity]) | ||||
bandbox.hidden = true; | )); | ||||
if (unit) | |||||
g_Selection.setHighlightList([]); | { | ||||
selection = [unit]; | |||||
inputState = INPUT_NORMAL; | g_Selection.removeList(selection); | ||||
return true; | } | ||||
} | } | ||||
break; | return g_UnitActions[action.type].execute(target, action, selection, queued); | ||||
} | } | ||||
break; | |||||
error("Invalid action.type " + action.type); | |||||
case INPUT_UNIT_POSITION: | return false; | ||||
switch (ev.type) | } | ||||
{ | |||||
case "mousemotion": | function positionUnitsFreehandSelectionMouseMove(ev) | ||||
return positionUnitsFreehandSelectionMouseMove(ev); | { | ||||
case "mousebuttonup": | // Converting the input line into a List of points. | ||||
return positionUnitsFreehandSelectionMouseUp(ev); | // For better performance the points must have a minimum distance to each other. | ||||
} | let target = Vector2D.from3D(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); | ||||
break; | if (!g_FreehandSelection_InputLine.length || | ||||
target.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= | |||||
case INPUT_BUILDING_CLICK: | g_FreehandSelection_ResolutionInputLineSquared) | ||||
switch (ev.type) | g_FreehandSelection_InputLine.push(target); | ||||
{ | return false; | ||||
case "mousemotion": | } | ||||
// If the mouse moved far enough from the original click location, | |||||
// then switch to drag-orientation mode | function positionUnitsFreehandSelectionMouseUp(ev) | ||||
let maxDragDelta = 16; | { | ||||
if (g_DragStart.distanceTo(ev) >= maxDragDelta) | g_InputEvents.SwitchToNextState("NORMAL") | ||||
{ | let inputLine = g_FreehandSelection_InputLine; | ||||
inputState = INPUT_BUILDING_DRAG; | g_FreehandSelection_InputLine = []; | ||||
return false; | if (ev.button != SDL_BUTTON_RIGHT) | ||||
} | return true; | ||||
break; | |||||
let lengthOfLine = 0; | |||||
case "mousebuttonup": | for (let i = 1; i < inputLine.length; ++i) | ||||
if (ev.button == SDL_BUTTON_LEFT) | lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); | ||||
{ | |||||
// If shift is down, let the player continue placing another of the same building | let selection = g_Selection.toList().filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); | ||||
var queued = Engine.HotkeyIsPressed("session.queue"); | |||||
if (tryPlaceBuilding(queued)) | // Checking the line for a minimum length to save performance. | ||||
{ | if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) | ||||
if (queued) | { | ||||
inputState = INPUT_BUILDING_PLACEMENT; | let action = determineAction(ev.x, ev.y); | ||||
else | return !!action && doAction(action, ev); | ||||
inputState = INPUT_NORMAL; | } | ||||
} | |||||
else | // Even distribution of the units on the line. | ||||
{ | let p0 = inputLine[0]; | ||||
inputState = INPUT_BUILDING_PLACEMENT; | let entityDistribution = [p0]; | ||||
} | let distanceBetweenEnts = lengthOfLine / (selection.length - 1); | ||||
return true; | let freeDist = -distanceBetweenEnts; | ||||
} | |||||
break; | for (let i = 1; i < inputLine.length; ++i) | ||||
{ | |||||
case "mousebuttondown": | let p1 = inputLine[i]; | ||||
if (ev.button == SDL_BUTTON_RIGHT) | freeDist += inputLine[i - 1].distanceTo(p1); | ||||
{ | |||||
// Cancel building | while (freeDist >= 0) | ||||
placementSupport.Reset(); | { | ||||
inputState = INPUT_NORMAL; | p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); | ||||
return true; | entityDistribution.push(p0); | ||||
} | freeDist -= distanceBetweenEnts; | ||||
break; | } | ||||
} | } | ||||
break; | |||||
// Rounding errors can lead to missing or too many points. | |||||
case INPUT_BUILDING_WALL_CLICK: | entityDistribution = entityDistribution.slice(0, selection.length); | ||||
// User is mid-click in choosing a starting point for building a wall. The build process can still be cancelled at this point | entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1])); | ||||
// by right-clicking; releasing the left mouse button will 'register' the starting point and commence endpoint choosing mode. | |||||
switch (ev.type) | if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) + | ||||
{ | Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) > | ||||
case "mousebuttonup": | Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) + | ||||
if (ev.button === SDL_BUTTON_LEFT) | Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0])) | ||||
{ | entityDistribution.reverse(); | ||||
inputState = INPUT_BUILDING_WALL_PATHING; | |||||
return true; | Engine.PostNetworkCommand({ | ||||
} | "type": Engine.HotkeyIsPressed("session.attackmove") ? "attack-walk-custom" : "walk-custom", | ||||
Not Done Inline Actions
Stan: * @param {string} buildTemplate Template name of the entity the user wants to build
* @param… | |||||
break; | "entities": selection, | ||||
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)), | |||||
case "mousebuttondown": | "targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, | ||||
if (ev.button == SDL_BUTTON_RIGHT) | "queued": Engine.HotkeyIsPressed("session.queue") | ||||
{ | }); | ||||
// Cancel building | |||||
placementSupport.Reset(); | // Add target markers with a minimum distance of 5 to each other. | ||||
updateBuildingPlacementPreview(); | let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts); | ||||
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker) | |||||
inputState = INPUT_NORMAL; | DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y }); | ||||
return true; | |||||
} | Engine.GuiInterfaceCall("PlaySound", { | ||||
break; | "name": "order_walk", | ||||
} | "entity": selection[0] | ||||
break; | }); | ||||
Not Done Inline Actions(let) elexis: (let) | |||||
Not Done Inline ActionsAall those var where already there, can change them tho. nani: Aall those `var` where already there, can change them tho. | |||||
case INPUT_BUILDING_WALL_PATHING: | return true; | ||||
// 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 | function handleMinimapEvent(target) | ||||
// user to continue building walls. | { | ||||
switch (ev.type) | // Partly duplicated from handleInputAfterGui(), but with the input being | ||||
{ | // world coordinates instead of screen coordinates. | ||||
case "mousemotion": | |||||
placementSupport.wallEndPosition = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); | if (!g_InputEvents.hasBaseState("NORMAL")) | ||||
return false; | |||||
// 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. | let fromMinimap = true; | ||||
// | let action = determineAction(undefined, undefined, fromMinimap); | ||||
// TODO: Note that here, we need to fetch all similar entities, including any offscreen ones, to support the case | if (!action) | ||||
// where the snap entity for the starting point has moved offscreen, or has been deleted/destroyed, or was a | return false; | ||||
// 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 | let selection = g_Selection.toList(); | ||||
Not Done Inline ActionsThe downside to the patch is that touching many lines means rewriting many lines. Where to stop? The more we change, the more we have to review. The more we need to review, the more we need to understand the logic that we mess with. elexis: The downside to the patch is that touching many lines means rewriting many lines. Where to stop? | |||||
Not Done Inline ActionsThis is from the previous code I only replaced swiches for objects. nani: This is from the previous code I only replaced swiches for objects. | |||||
// in them. Might be useful to query only for entities within a certain range around the starting point and ending | |||||
// points. | let queued = Engine.HotkeyIsPressed("session.queue"); | ||||
if (g_UnitActions[action.type] && g_UnitActions[action.type].execute) | |||||
placementSupport.wallSnapEntitiesIncludeOffscreen = true; | return g_UnitActions[action.type].execute(target, action, selection, queued); | ||||
var result = updateBuildingPlacementPreview(); // includes an update of the snap entity candidates | error("Invalid action.type " + action.type); | ||||
return false; | |||||
if (result && result.cost) | } | ||||
{ | |||||
var neededResources = Engine.GuiInterfaceCall("GetNeededResources", { "cost": result.cost }); | function getEntityLimitAndCount(playerState, entType) | ||||
placementSupport.tooltipMessage = [ | { | ||||
getEntityCostTooltip(result), | let ret = { | ||||
getNeededResourcesTooltip(neededResources) | "entLimit": undefined, | ||||
].filter(tip => tip).join("\n"); | "entCount": undefined, | ||||
} | "entLimitChangers": undefined, | ||||
"canBeAddedCount": undefined | |||||
break; | }; | ||||
if (!playerState.entityLimits) | |||||
case "mousebuttondown": | return ret; | ||||
if (ev.button == SDL_BUTTON_LEFT) | let template = GetTemplateData(entType); | ||||
{ | let entCategory = template.trainingRestrictions && template.trainingRestrictions.category || | ||||
var queued = Engine.HotkeyIsPressed("session.queue"); | template.buildRestrictions && template.buildRestrictions.category; | ||||
if (tryPlaceWall(queued)) | |||||
{ | if (entCategory && playerState.entityLimits[entCategory] !== undefined) | ||||
if (queued) | { | ||||
{ | ret.entLimit = playerState.entityLimits[entCategory] || 0; | ||||
// continue building, just set a new starting position where we left off | ret.entCount = playerState.entityCounts[entCategory] || 0; | ||||
placementSupport.position = placementSupport.wallEndPosition; | ret.entLimitChangers = playerState.entityLimitChangers[entCategory]; | ||||
placementSupport.wallEndPosition = undefined; | ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0); | ||||
} | |||||
inputState = INPUT_BUILDING_WALL_CLICK; | return ret; | ||||
} | } | ||||
else | |||||
{ | // Called by GUI when user clicks construction button | ||||
placementSupport.Reset(); | // @param buildTemplate Template name of the entity the user wants to build | ||||
inputState = INPUT_NORMAL; | function startBuildingPlacement(buildTemplate, playerState) | ||||
} | { | ||||
} | if (getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) | ||||
else | return; | ||||
placementSupport.tooltipMessage = translate("Cannot build wall here!"); | |||||
// TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI | |||||
updateBuildingPlacementPreview(); | // to start building a structure, then the highlight selection rings are kept during the construction of the building. | ||||
return true; | // Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing. | ||||
} | |||||
else if (ev.button == SDL_BUTTON_RIGHT) | placementSupport.Reset(); | ||||
{ | |||||
// reset to normal input mode | // find out if we're building a wall, and change the entity appropriately if so | ||||
Not Done Inline Actions@param {Vector2D} position position to the training queue for all entities in the selection to be added at. Stan: @param {Vector2D} position position to the training queue for all entities in the selection to… | |||||
placementSupport.Reset(); | let templateData = GetTemplateData(buildTemplate); | ||||
updateBuildingPlacementPreview(); | if (templateData.wallSet) | ||||
{ | |||||
inputState = INPUT_NORMAL; | placementSupport.mode = "wall"; | ||||
return true; | placementSupport.wallSet = templateData.wallSet; | ||||
} | } | ||||
break; | else | ||||
} | { | ||||
break; | placementSupport.mode = "building"; | ||||
placementSupport.template = buildTemplate; | |||||
case INPUT_BUILDING_DRAG: | } | ||||
switch (ev.type) | |||||
{ | if (templateData.attack && | ||||
case "mousemotion": | templateData.attack.Ranged && | ||||
let maxDragDelta = 16; | templateData.attack.Ranged.maxRange) | ||||
if (g_DragStart.distanceTo(ev) >= maxDragDelta) | { | ||||
{ | // add attack information to display a good tooltip | ||||
// Rotate in the direction of the mouse | placementSupport.attack = templateData.attack; | ||||
placementSupport.angle = placementSupport.position.horizAngleTo(Engine.GetTerrainAtScreenPoint(ev.x, ev.y)); | } | ||||
} | |||||
else | g_InputEvents.SwitchToNextState(`PLACEMENT.${templateData.wallSet ? "WALL" : "BUILDING"}`) | ||||
{ | } | ||||
// If the mouse is near the center, snap back to the default orientation | |||||
placementSupport.SetDefaultAngle(); | // Batch training: | ||||
} | // When the user shift-clicks, we set these variables and switch to "BATCHTRAINING" | ||||
// When the user releases shift, or clicks on a different training button, we create the batched units | |||||
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { | var g_BatchTrainingEntities; | ||||
"template": placementSupport.template, | var g_BatchTrainingType; | ||||
"x": placementSupport.position.x, | var g_NumberOfBatches; | ||||
"z": placementSupport.position.z | var g_BatchTrainingEntityAllowedCount; | ||||
}); | var g_BatchSize = getDefaultBatchTrainingSize(); | ||||
if (snapData) | |||||
{ | function OnTrainMouseWheel(dir) | ||||
placementSupport.angle = snapData.angle; | { | ||||
placementSupport.position.x = snapData.x; | if (Engine.HotkeyIsPressed("session.batchtrain")) | ||||
placementSupport.position.z = snapData.z; | g_BatchSize += dir / Engine.ConfigDB_GetValue("user", "gui.session.scrollbatchratio"); | ||||
} | if (g_BatchSize < 1 || !Number.isFinite(g_BatchSize)) | ||||
g_BatchSize = 1; | |||||
updateBuildingPlacementPreview(); | } | ||||
break; | |||||
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType) | |||||
case "mousebuttonup": | { | ||||
if (ev.button == SDL_BUTTON_LEFT) | return entitiesToCheck.filter(entity => | ||||
{ | { | ||||
// If shift is down, let the player continue placing another of the same building | let state = GetEntityState(entity); | ||||
var queued = Engine.HotkeyIsPressed("session.queue"); | return state && state.production && state.production.entities.length && | ||||
if (tryPlaceBuilding(queued)) | state.production.entities.indexOf(trainEntType) != -1; | ||||
{ | }); | ||||
if (queued) | } | ||||
inputState = INPUT_BUILDING_PLACEMENT; | |||||
else | function getDefaultBatchTrainingSize() | ||||
inputState = INPUT_NORMAL; | { | ||||
} | let num = +Engine.ConfigDB_GetValue("user", "gui.session.batchtrainingsize"); | ||||
else | return Number.isInteger(num) && num > 0 ? num : 5; | ||||
{ | } | ||||
inputState = INPUT_BUILDING_PLACEMENT; | |||||
} | function getBatchTrainingSize() | ||||
return true; | { | ||||
} | return Math.max(Math.round(g_BatchSize), 1); | ||||
break; | } | ||||
Done Inline ActionsmouseX mouseY are globals? Globals should by coding convention start with g_. elexis: mouseX mouseY are globals? Globals should by coding convention start with `g_`.
Also (x,y)… | |||||
case "mousebuttondown": | function updateDefaultBatchSize() | ||||
if (ev.button == SDL_BUTTON_RIGHT) | { | ||||
{ | g_BatchSize = getDefaultBatchTrainingSize(); | ||||
// Cancel building | } | ||||
placementSupport.Reset(); | |||||
inputState = INPUT_NORMAL; | // Add the unit shown at position to the training queue for all entities in the selection | ||||
return true; | function addTrainingByPosition(position) | ||||
} | { | ||||
break; | let playerState = GetSimState().players[Engine.GetPlayerID()]; | ||||
} | let selection = g_Selection.toList(); | ||||
break; | |||||
if (!playerState || !selection.length) | |||||
case INPUT_MASSTRIBUTING: | return; | ||||
if (ev.type == "hotkeyup" && ev.hotkey == "session.masstribute") | |||||
{ | let trainableEnts = getAllTrainableEntitiesFromSelection(); | ||||
g_FlushTributing(); | |||||
inputState = INPUT_NORMAL; | let entToTrain = trainableEnts[position]; | ||||
} | // When we have no building to train or the position is invalid | ||||
break; | if (!entToTrain) | ||||
return; | |||||
case INPUT_BATCHTRAINING: | |||||
if (ev.type == "hotkeyup" && ev.hotkey == "session.batchtrain") | addTrainingToQueue(selection, entToTrain, playerState); | ||||
{ | return; | ||||
flushTrainingBatch(); | } | ||||
inputState = INPUT_NORMAL; | |||||
} | // Called by GUI when user clicks training button | ||||
Not Done Inline Actionsunneeded parentheses, also using null only in order to distinguish from the default undefined makes the distinction between the two values more clear. elexis: unneeded parentheses, also using `null` only in order to distinguish from the default… | |||||
break; | function addTrainingToQueue(selection, trainEntType, playerState) | ||||
} | { | ||||
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); | |||||
return false; | |||||
Done Inline Actionsg_InputBreforeGui.onEvent elexis: g_InputBreforeGui.onEvent | |||||
} | let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; | ||||
function handleInputAfterGui(ev) | let decrement = Engine.HotkeyIsPressed("selection.remove"); | ||||
{ | let template; | ||||
Done Inline Actionsreturn <condition>; elexis: return <condition>; | |||||
Done Inline ActionsShorter but more confusing, doesn't differentiate between action and behaviour. nani: Shorter but more confusing, doesn't differentiate between action and behaviour. | |||||
if (GetSimState().cinemaPlaying) | if (!decrement) | ||||
return false; | template = GetTemplateData(trainEntType); | ||||
if (ev.hotkey === undefined) | // Batch training only possible if we can train at least 2 units | ||||
ev.hotkey = null; | if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) | ||||
{ | |||||
// Handle the time-warp testing features, restricted to single-player | if (g_InputEvents.hasBaseState("BATCHTRAINING")) | ||||
if (!g_IsNetworked && Engine.GetGUIObjectByName("devTimeWarp").checked) | { | ||||
{ | // Check if we are training in the same building(s) as the last batch | ||||
if (ev.type == "hotkeydown" && ev.hotkey == "session.timewarp.fastforward") | // NOTE: We just check if the arrays are the same and if the order is the same | ||||
Engine.SetSimRate(20.0); | // If the order changed, we have a new selection and we should create a new batch. | ||||
else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.fastforward") | // If we're already creating a batch of this unit (in the same building(s)), then just extend it | ||||
Engine.SetSimRate(1.0); | // (if training limits allow) | ||||
else if (ev.type == "hotkeyup" && ev.hotkey == "session.timewarp.rewind") | if (g_BatchTrainingEntities.length == selection.length && | ||||
Engine.RewindTimeWarp(); | g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && | ||||
} | g_BatchTrainingType == trainEntType) | ||||
{ | |||||
if (ev.hotkey == "session.highlightguarding") | if (decrement) | ||||
{ | { | ||||
g_ShowGuarding = (ev.type == "hotkeydown"); | --g_NumberOfBatches; | ||||
updateAdditionalHighlight(); | if (g_NumberOfBatches <= 0) | ||||
} | g_InputEvents.SwitchToNextState("NORMAL"); | ||||
else if (ev.hotkey == "session.highlightguarded") | } | ||||
{ | else if (canBeAddedCount == undefined || | ||||
g_ShowGuarded = (ev.type == "hotkeydown"); | canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) | ||||
updateAdditionalHighlight(); | { | ||||
} | if (Engine.GuiInterfaceCall("GetNeededResources", { | ||||
"cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) | |||||
if (inputState != INPUT_NORMAL && inputState != INPUT_SELECTING) | })) | ||||
clickedEntity = INVALID_ENTITY; | return; | ||||
// State-machine processing: | ++g_NumberOfBatches; | ||||
} | |||||
switch (inputState) | g_BatchTrainingEntityAllowedCount = canBeAddedCount; | ||||
{ | return; | ||||
case INPUT_NORMAL: | } | ||||
switch (ev.type) | // Otherwise start a new one | ||||
{ | else if (!decrement) | ||||
case "mousemotion": | flushTrainingBatch(); | ||||
// Highlight the first hovered entity (if any) | // fall through to create the new batch | ||||
var ent = Engine.PickEntityAtPoint(ev.x, ev.y); | } | ||||
if (ent != INVALID_ENTITY) | |||||
g_Selection.setHighlightList([ent]); | // Don't start a new batch if decrementing or unable to afford it. | ||||
else | if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { | ||||
g_Selection.setHighlightList([]); | "cost": | ||||
multiplyEntityCosts(template, getBatchTrainingSize()) | |||||
return false; | })) | ||||
return; | |||||
case "mousebuttondown": | |||||
if (ev.button == SDL_BUTTON_LEFT) | g_BatchTrainingEntities = selection; | ||||
{ | g_BatchTrainingType = trainEntType; | ||||
g_DragStart = new Vector2D(ev.x, ev.y); | g_BatchTrainingEntityAllowedCount = canBeAddedCount; | ||||
inputState = INPUT_SELECTING; | g_NumberOfBatches = 1; | ||||
// If a single click occured, reset the clickedEntity. | g_InputEvents.SwitchToNextState("BATCHTRAINING"); | ||||
// Also set it if we're double/triple clicking and missed the unit earlier. | } | ||||
if (ev.clicks == 1 || clickedEntity == INVALID_ENTITY) | else | ||||
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); | { | ||||
return true; | // Non-batched - just create a single entity in each building | ||||
} | // (but no more than entity limit allows) | ||||
else if (ev.button == SDL_BUTTON_RIGHT) | let buildingsForTraining = appropriateBuildings; | ||||
{ | if (canBeAddedCount !== undefined) | ||||
g_DragStart = new Vector2D(ev.x, ev.y); | buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); | ||||
inputState = INPUT_UNIT_POSITION_START; | Engine.PostNetworkCommand({ | ||||
} | "type": "train", | ||||
break; | "template": trainEntType, | ||||
"count": 1, | |||||
case "hotkeydown": | "entities": buildingsForTraining | ||||
if (ev.hotkey.indexOf("selection.group.") == 0) | }); | ||||
{ | g_InputEvents.SwitchToNextState("NORMAL"); | ||||
let now = Date.now(); | } | ||||
if (now - doublePressTimer < doublePressTime && ev.hotkey == prevHotkey) | } | ||||
{ | |||||
if (ev.hotkey.indexOf("selection.group.select.") == 0) | /** | ||||
{ | * Returns the number of units that will be present in a batch if the user clicks | ||||
var sptr = ev.hotkey.split("."); | * the training button depending on the batch training modifier hotkey | ||||
performGroup("snap", sptr[3]); | */ | ||||
} | function getTrainingStatus(selection, trainEntType, playerState) | ||||
} | { | ||||
else | let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); | ||||
{ | let nextBatchTrainingCount = 0; | ||||
var sptr = ev.hotkey.split("."); | |||||
performGroup(sptr[2], sptr[3]); | let canBeAddedCount; | ||||
if (g_InputEvents.BaseState() == "BATCHTRAINING" && g_BatchTrainingType == trainEntType) | |||||
doublePressTimer = now; | { | ||||
prevHotkey = ev.hotkey; | nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize(); | ||||
} | canBeAddedCount = g_BatchTrainingEntityAllowedCount; | ||||
} | } | ||||
break; | else | ||||
} | canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; | ||||
break; | |||||
// We need to calculate count after the next increment if it's possible | |||||
case INPUT_PRESELECTEDACTION: | if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && | ||||
switch (ev.type) | Engine.HotkeyIsPressed("session.batchtrain")) | ||||
{ | nextBatchTrainingCount += getBatchTrainingSize(); | ||||
case "mousemotion": | |||||
// Highlight the first hovered entity (if any) | nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); | ||||
var ent = Engine.PickEntityAtPoint(ev.x, ev.y); | |||||
if (ent != INVALID_ENTITY) | // If training limits don't allow us to train batchTrainingCount in each appropriate building | ||||
g_Selection.setHighlightList([ent]); | // train as many full batches as we can and remainer in one more building. | ||||
else | let buildingsCountToTrainFullBatch = appropriateBuildings.length; | ||||
g_Selection.setHighlightList([]); | let remainderToTrain = 0; | ||||
if (canBeAddedCount !== undefined && | |||||
return false; | canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) | ||||
{ | |||||
case "mousebuttondown": | buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); | ||||
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) | remainderToTrain = canBeAddedCount % nextBatchTrainingCount; | ||||
{ | } | ||||
var action = determineAction(ev.x, ev.y); | |||||
if (!action) | return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; | ||||
break; | } | ||||
if (!Engine.HotkeyIsPressed("session.queue")) | |||||
{ | function flushTrainingBatch() | ||||
preSelectedAction = ACTION_NONE; | { | ||||
inputState = INPUT_NORMAL; | let batchedSize = g_NumberOfBatches * getBatchTrainingSize(); | ||||
} | let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); | ||||
return doAction(action, ev); | // If training limits don't allow us to train batchedSize in each appropriate building | ||||
} | if (g_BatchTrainingEntityAllowedCount !== undefined && | ||||
else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) | g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) | ||||
{ | { | ||||
preSelectedAction = ACTION_NONE; | // Train as many full batches as we can | ||||
inputState = INPUT_NORMAL; | let buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / batchedSize); | ||||
break; | Engine.PostNetworkCommand({ | ||||
} | "type": "train", | ||||
// else | "entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), | ||||
default: | "template": g_BatchTrainingType, | ||||
// Slight hack: If selection is empty, reset the input state | "count": batchedSize | ||||
if (g_Selection.toList().length == 0) | }); | ||||
{ | |||||
preSelectedAction = ACTION_NONE; | // Train remainer in one more building | ||||
inputState = INPUT_NORMAL; | Engine.PostNetworkCommand({ | ||||
break; | "type": "train", | ||||
} | "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], | ||||
} | "template": g_BatchTrainingType, | ||||
break; | "count": g_BatchTrainingEntityAllowedCount % batchedSize | ||||
}); | |||||
case INPUT_SELECTING: | } | ||||
switch (ev.type) | else | ||||
{ | Engine.PostNetworkCommand({ | ||||
case "mousemotion": | "type": "train", | ||||
// If the mouse moved further than a limit, switch to bandbox mode | "entities": appropriateBuildings, | ||||
if (g_DragStart.distanceTo(ev) >= g_MaxDragDelta) | "template": g_BatchTrainingType, | ||||
{ | "count": batchedSize | ||||
inputState = INPUT_BANDBOXING; | }); | ||||
return false; | } | ||||
} | |||||
function performGroup(action, groupId) | |||||
var ent = Engine.PickEntityAtPoint(ev.x, ev.y); | { | ||||
if (ent != INVALID_ENTITY) | switch (action) | ||||
g_Selection.setHighlightList([ent]); | { | ||||
else | case "snap": | ||||
g_Selection.setHighlightList([]); | case "select": | ||||
return false; | case "add": | ||||
let toSelect = []; | |||||
case "mousebuttonup": | g_Groups.update(); | ||||
if (ev.button == SDL_BUTTON_LEFT) | for (let ent in g_Groups.groups[groupId].ents) | ||||
{ | toSelect.push(+ent); | ||||
if (clickedEntity == INVALID_ENTITY) | |||||
clickedEntity = Engine.PickEntityAtPoint(ev.x, ev.y); | if (action != "add") | ||||
// Abort if we didn't click on an entity or if the entity was removed before the mousebuttonup event. | g_Selection.reset(); | ||||
if (clickedEntity == INVALID_ENTITY || !GetEntityState(clickedEntity)) | |||||
{ | g_Selection.addList(toSelect); | ||||
clickedEntity = INVALID_ENTITY; | |||||
if (!Engine.HotkeyIsPressed("selection.add") && !Engine.HotkeyIsPressed("selection.remove")) | if (action == "snap" && toSelect.length) | ||||
{ | { | ||||
g_Selection.reset(); | let entState = GetEntityState(toSelect[0]); | ||||
resetIdleUnit(); | let position = entState.position; | ||||
} | if (position && entState.visibility != "hidden") | ||||
inputState = INPUT_NORMAL; | Engine.CameraMoveTo(position.x, position.z); | ||||
return true; | } | ||||
} | break; | ||||
case "save": | |||||
// If camera following and we select different unit, stop | case "breakUp": | ||||
if (Engine.GetFollowedEntity() != clickedEntity) | g_Groups.groups[groupId].reset(); | ||||
Engine.CameraFollow(0); | |||||
if (action == "save") | |||||
var ents = []; | g_Groups.addEntities(groupId, g_Selection.toList()); | ||||
if (ev.clicks == 1) | |||||
ents = [clickedEntity]; | updateGroups(); | ||||
else | break; | ||||
{ | } | ||||
// Double click or triple click has occurred | } | ||||
var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); | |||||
var matchRank = true; | var lastIdleUnit = 0; | ||||
var templateToMatch; | var currIdleClassIndex = 0; | ||||
var lastIdleClasses = []; | |||||
// Check for double click or triple click | |||||
if (ev.clicks == 2) | function resetIdleUnit() | ||||
{ | { | ||||
// Select similar units regardless of rank | lastIdleUnit = 0; | ||||
templateToMatch = GetEntityState(clickedEntity).identity.selectionGroupName; | currIdleClassIndex = 0; | ||||
if (templateToMatch) | lastIdleClasses = []; | ||||
matchRank = false; | } | ||||
else | |||||
// No selection group name defined, so fall back to exact match | function findIdleUnit(classes) | ||||
templateToMatch = GetEntityState(clickedEntity).template; | { | ||||
let append = Engine.HotkeyIsPressed("selection.add"); | |||||
} | let selectall = Engine.HotkeyIsPressed("selection.offscreen"); | ||||
else | |||||
// Triple click | // Reset the last idle unit, etc., if the selection type has changed. | ||||
// Select units matching exact template name (same rank) | if (selectall || classes.length != lastIdleClasses.length || !classes.every((v, i) => v === lastIdleClasses[i])) | ||||
templateToMatch = GetEntityState(clickedEntity).template; | resetIdleUnit(); | ||||
lastIdleClasses = classes; | |||||
// TODO: Should we handle "control all units" here as well? | |||||
ents = Engine.PickSimilarPlayerEntities(templateToMatch, showOffscreen, matchRank, false); | var data = { | ||||
} | "viewedPlayer": g_ViewedPlayer, | ||||
"excludeUnits": append ? g_Selection.toList() : [], | |||||
// Update the list of selected units | // If the current idle class index is not 0, put the class at that index first. | ||||
if (Engine.HotkeyIsPressed("selection.add")) | "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) | ||||
g_Selection.addList(ents); | }; | ||||
else if (Engine.HotkeyIsPressed("selection.remove")) | if (!selectall) | ||||
g_Selection.removeList(ents); | { | ||||
else | data.limit = 1; | ||||
{ | data.prevUnit = lastIdleUnit; | ||||
g_Selection.reset(); | } | ||||
g_Selection.addList(ents); | |||||
} | var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); | ||||
if (!idleUnits.length) | |||||
inputState = INPUT_NORMAL; | { | ||||
return true; | // TODO: display a message or play a sound to indicate no more idle units, or something | ||||
} | // Reset for next cycle | ||||
break; | resetIdleUnit(); | ||||
} | return; | ||||
break; | } | ||||
case INPUT_UNIT_POSITION_START: | if (!append) | ||||
switch (ev.type) | g_Selection.reset(); | ||||
{ | g_Selection.addList(idleUnits); | ||||
case "mousemotion": | |||||
// If the mouse moved further than a limit, switch to unit position mode | if (selectall) | ||||
if (g_DragStart.distanceToSquared(ev) >= Math.square(g_MaxDragDelta)) | return; | ||||
{ | |||||
inputState = INPUT_UNIT_POSITION; | lastIdleUnit = idleUnits[0]; | ||||
return false; | var entityState = GetEntityState(lastIdleUnit); | ||||
} | var position = entityState.position; | ||||
break; | if (position) | ||||
case "mousebuttonup": | Engine.CameraMoveTo(position.x, position.z); | ||||
inputState = INPUT_NORMAL; | // Move the idle class index to the first class an idle unit was found for. | ||||
if (ev.button == SDL_BUTTON_RIGHT) | var indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); | ||||
{ | currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; | ||||
let action = determineAction(ev.x, ev.y); | } | ||||
if (action) | |||||
return doAction(action, ev); | function clearSelection() | ||||
} | { | ||||
break; | preSelectedAction = ACTION_NONE; | ||||
} | if (g_InputEvents.hasBaseState("PLACEMENT")) | ||||
break; | { | ||||
g_InputEvents.SwitchToNextState("NORMAL"); | |||||
case INPUT_BUILDING_PLACEMENT: | } | ||||
switch (ev.type) | else | ||||
{ | g_Selection.reset(); | ||||
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; | |||||
} | |||||
function doAction(action, ev) | |||||
{ | |||||
if (!controlsPlayer(g_ViewedPlayer)) | |||||
return false; | |||||
// 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); | |||||
if (g_UnitActions[action.type] && g_UnitActions[action.type].execute) | |||||
{ | |||||
let selection = g_Selection.toList(); | |||||
if (orderone) | |||||
{ | |||||
// pick the first unit that can do this order. | |||||
let unit = selection.find(entity => | |||||
["preSelectedActionCheck", "hotkeyActionCheck", "actionCheck"].some(method => | |||||
g_UnitActions[action.type][method] && | |||||
g_UnitActions[action.type][method](action.target || undefined, [entity]) | |||||
)); | |||||
if (unit) | |||||
{ | |||||
selection = [unit]; | |||||
g_Selection.removeList(selection); | |||||
} | |||||
} | |||||
return g_UnitActions[action.type].execute(target, action, selection, queued); | |||||
} | |||||
error("Invalid action.type " + action.type); | |||||
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.distanceToSquared(g_FreehandSelection_InputLine[g_FreehandSelection_InputLine.length - 1]) >= | |||||
g_FreehandSelection_ResolutionInputLineSquared) | |||||
g_FreehandSelection_InputLine.push(target); | |||||
return false; | |||||
} | |||||
function positionUnitsFreehandSelectionMouseUp(ev) | |||||
{ | |||||
inputState = INPUT_NORMAL; | |||||
let inputLine = g_FreehandSelection_InputLine; | |||||
g_FreehandSelection_InputLine = []; | |||||
if (ev.button != SDL_BUTTON_RIGHT) | |||||
return true; | |||||
let lengthOfLine = 0; | |||||
for (let i = 1; i < inputLine.length; ++i) | |||||
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); | |||||
let selection = g_Selection.toList().filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); | |||||
// Checking the line for a minimum length to save performance. | |||||
if (lengthOfLine < g_FreehandSelection_MinLengthOfLine || selection.length < g_FreehandSelection_MinNumberOfUnits) | |||||
{ | |||||
let action = determineAction(ev.x, ev.y); | |||||
return !!action && doAction(action, ev); | |||||
} | |||||
// Even distribution of the units on the line. | |||||
let p0 = inputLine[0]; | |||||
let entityDistribution = [p0]; | |||||
let distanceBetweenEnts = lengthOfLine / (selection.length - 1); | |||||
let freeDist = -distanceBetweenEnts; | |||||
for (let i = 1; i < inputLine.length; ++i) | |||||
{ | |||||
let p1 = inputLine[i]; | |||||
freeDist += inputLine[i - 1].distanceTo(p1); | |||||
while (freeDist >= 0) | |||||
{ | |||||
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); | |||||
entityDistribution.push(p0); | |||||
freeDist -= distanceBetweenEnts; | |||||
} | |||||
} | |||||
// Rounding errors can lead to missing or too many points. | |||||
entityDistribution = entityDistribution.slice(0, selection.length); | |||||
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1])); | |||||
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) + | |||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) > | |||||
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) + | |||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0])) | |||||
entityDistribution.reverse(); | |||||
Engine.PostNetworkCommand({ | |||||
"type": Engine.HotkeyIsPressed("session.attackmove") ? "attack-walk-custom" : "walk-custom", | |||||
"entities": selection, | |||||
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)), | |||||
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, | |||||
"queued": Engine.HotkeyIsPressed("session.queue") | |||||
}); | |||||
// Add target markers with a minimum distance of 5 to each other. | |||||
let entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts); | |||||
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker) | |||||
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y }); | |||||
Engine.GuiInterfaceCall("PlaySound", { | |||||
"name": "order_walk", | |||||
"entity": selection[0] | |||||
}); | |||||
return true; | |||||
} | |||||
function handleMinimapEvent(target) | |||||
{ | |||||
// Partly duplicated from handleInputAfterGui(), but with the input being | |||||
// world coordinates instead of screen coordinates. | |||||
if (inputState != INPUT_NORMAL) | |||||
return false; | |||||
var fromMinimap = true; | |||||
var action = determineAction(undefined, undefined, fromMinimap); | |||||
if (!action) | |||||
return false; | |||||
var selection = g_Selection.toList(); | |||||
var 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); | |||||
return false; | |||||
} | |||||
function getEntityLimitAndCount(playerState, entType) | |||||
{ | |||||
let ret = { | |||||
"entLimit": undefined, | |||||
"entCount": undefined, | |||||
"entLimitChangers": undefined, | |||||
"canBeAddedCount": undefined | |||||
}; | |||||
if (!playerState.entityLimits) | |||||
return ret; | |||||
let template = GetTemplateData(entType); | |||||
let entCategory = template.trainingRestrictions && template.trainingRestrictions.category || | |||||
template.buildRestrictions && template.buildRestrictions.category; | |||||
if (entCategory && playerState.entityLimits[entCategory] !== undefined) | |||||
{ | |||||
ret.entLimit = playerState.entityLimits[entCategory] || 0; | |||||
ret.entCount = playerState.entityCounts[entCategory] || 0; | |||||
ret.entLimitChangers = playerState.entityLimitChangers[entCategory]; | |||||
ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0); | |||||
} | |||||
return ret; | |||||
} | |||||
// Called by GUI when user clicks construction button | |||||
// @param buildTemplate Template name of the entity the user wants to build | |||||
function startBuildingPlacement(buildTemplate, playerState) | |||||
{ | |||||
if(getEntityLimitAndCount(playerState, buildTemplate).canBeAddedCount == 0) | |||||
return; | |||||
// TODO: we should clear any highlight selection rings here. If the mouse was over an entity before going onto the GUI | |||||
// to start building a structure, then the highlight selection rings are kept during the construction of the building. | |||||
// Gives the impression that somehow the hovered-over entity has something to do with the building you're constructing. | |||||
placementSupport.Reset(); | |||||
// find out if we're building a wall, and change the entity appropriately if so | |||||
var templateData = GetTemplateData(buildTemplate); | |||||
if (templateData.wallSet) | |||||
{ | |||||
placementSupport.mode = "wall"; | |||||
placementSupport.wallSet = templateData.wallSet; | |||||
inputState = INPUT_BUILDING_PLACEMENT; | |||||
} | |||||
else | |||||
{ | |||||
placementSupport.mode = "building"; | |||||
placementSupport.template = buildTemplate; | |||||
inputState = INPUT_BUILDING_PLACEMENT; | |||||
} | |||||
if (templateData.attack && | |||||
templateData.attack.Ranged && | |||||
templateData.attack.Ranged.maxRange) | |||||
{ | |||||
// add attack information to display a good tooltip | |||||
placementSupport.attack = templateData.attack; | |||||
} | |||||
} | |||||
// Batch training: | |||||
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING | |||||
// When the user releases shift, or clicks on a different training button, we create the batched units | |||||
var g_BatchTrainingEntities; | |||||
var g_BatchTrainingType; | |||||
var g_NumberOfBatches; | |||||
var g_BatchTrainingEntityAllowedCount; | |||||
var g_BatchSize = getDefaultBatchTrainingSize(); | |||||
function OnTrainMouseWheel(dir) | |||||
{ | |||||
if (Engine.HotkeyIsPressed("session.batchtrain")) | |||||
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"); | |||||
return Number.isInteger(num) && num > 0 ? num : 5; | |||||
} | |||||
function getBatchTrainingSize() | |||||
{ | |||||
return Math.max(Math.round(g_BatchSize), 1); | |||||
} | |||||
function updateDefaultBatchSize() | |||||
{ | |||||
g_BatchSize = getDefaultBatchTrainingSize(); | |||||
} | |||||
// Add the unit shown at position to the training queue for all entities in the selection | |||||
function addTrainingByPosition(position) | |||||
{ | |||||
let playerState = GetSimState().players[Engine.GetPlayerID()]; | |||||
let selection = g_Selection.toList(); | |||||
if (!playerState || !selection.length) | |||||
return; | |||||
let trainableEnts = getAllTrainableEntitiesFromSelection(); | |||||
let entToTrain = trainableEnts[position]; | |||||
// When we have no building to train or the position is invalid | |||||
if (!entToTrain) | |||||
return; | |||||
addTrainingToQueue(selection, entToTrain, playerState); | |||||
return; | |||||
} | |||||
// Called by GUI when user clicks training button | |||||
function addTrainingToQueue(selection, trainEntType, playerState) | |||||
{ | |||||
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); | |||||
let canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; | |||||
let decrement = Engine.HotkeyIsPressed("selection.remove"); | |||||
let template; | |||||
if (!decrement) | |||||
template = GetTemplateData(trainEntType); | |||||
// Batch training only possible if we can train at least 2 units | |||||
if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) | |||||
{ | |||||
if (inputState == INPUT_BATCHTRAINING) | |||||
{ | |||||
// Check if we are training in the same building(s) as the last batch | |||||
// NOTE: We just check if the arrays are the same and if the order is the same | |||||
// If the order changed, we have a new selection and we should create a new batch. | |||||
// If we're already creating a batch of this unit (in the same building(s)), then just extend it | |||||
// (if training limits allow) | |||||
if (g_BatchTrainingEntities.length == selection.length && | |||||
g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && | |||||
g_BatchTrainingType == trainEntType) | |||||
{ | |||||
if (decrement) | |||||
{ | |||||
--g_NumberOfBatches; | |||||
if (g_NumberOfBatches <= 0) | |||||
inputState = INPUT_NORMAL; | |||||
} | |||||
else if (canBeAddedCount == undefined || | |||||
canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) | |||||
{ | |||||
if (Engine.GuiInterfaceCall("GetNeededResources", { | |||||
"cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) | |||||
})) | |||||
return; | |||||
++g_NumberOfBatches; | |||||
} | |||||
g_BatchTrainingEntityAllowedCount = canBeAddedCount; | |||||
return; | |||||
} | |||||
// Otherwise start a new one | |||||
else if (!decrement) | |||||
flushTrainingBatch(); | |||||
// fall through to create the new batch | |||||
} | |||||
// Don't start a new batch if decrementing or unable to afford it. | |||||
if (decrement || Engine.GuiInterfaceCall("GetNeededResources", { "cost": | |||||
multiplyEntityCosts(template, getBatchTrainingSize()) })) | |||||
return; | |||||
inputState = INPUT_BATCHTRAINING; | |||||
g_BatchTrainingEntities = selection; | |||||
g_BatchTrainingType = trainEntType; | |||||
g_BatchTrainingEntityAllowedCount = canBeAddedCount; | |||||
g_NumberOfBatches = 1; | |||||
} | |||||
else | |||||
{ | |||||
// Non-batched - just create a single entity in each building | |||||
// (but no more than entity limit allows) | |||||
let buildingsForTraining = appropriateBuildings; | |||||
if (canBeAddedCount !== undefined) | |||||
buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); | |||||
Engine.PostNetworkCommand({ | |||||
"type": "train", | |||||
"template": trainEntType, | |||||
"count": 1, | |||||
"entities": buildingsForTraining | |||||
}); | |||||
} | |||||
} | |||||
/** | |||||
* Returns the number of units that will be present in a batch if the user clicks | |||||
* the training button depending on the batch training modifier hotkey | |||||
*/ | |||||
function getTrainingStatus(selection, trainEntType, playerState) | |||||
{ | |||||
let appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); | |||||
let nextBatchTrainingCount = 0; | |||||
let canBeAddedCount; | |||||
if (inputState == INPUT_BATCHTRAINING && g_BatchTrainingType == trainEntType) | |||||
{ | |||||
nextBatchTrainingCount = g_NumberOfBatches * getBatchTrainingSize(); | |||||
canBeAddedCount = g_BatchTrainingEntityAllowedCount; | |||||
} | |||||
else | |||||
canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; | |||||
// We need to calculate count after the next increment if it's possible | |||||
if ((canBeAddedCount == undefined || canBeAddedCount > nextBatchTrainingCount * appropriateBuildings.length) && | |||||
Engine.HotkeyIsPressed("session.batchtrain")) | |||||
nextBatchTrainingCount += getBatchTrainingSize(); | |||||
nextBatchTrainingCount = Math.max(nextBatchTrainingCount, 1); | |||||
// If training limits don't allow us to train batchTrainingCount in each appropriate building | |||||
// train as many full batches as we can and remainer in one more building. | |||||
let buildingsCountToTrainFullBatch = appropriateBuildings.length; | |||||
let remainderToTrain = 0; | |||||
if (canBeAddedCount !== undefined && | |||||
canBeAddedCount < nextBatchTrainingCount * appropriateBuildings.length) | |||||
{ | |||||
buildingsCountToTrainFullBatch = Math.floor(canBeAddedCount / nextBatchTrainingCount); | |||||
remainderToTrain = canBeAddedCount % nextBatchTrainingCount; | |||||
} | |||||
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain]; | |||||
} | |||||
function flushTrainingBatch() | |||||
{ | |||||
let batchedSize = g_NumberOfBatches * getBatchTrainingSize(); | |||||
let appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); | |||||
// If training limits don't allow us to train batchedSize in each appropriate building | |||||
if (g_BatchTrainingEntityAllowedCount !== undefined && | |||||
g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) | |||||
{ | |||||
// Train as many full batches as we can | |||||
let buildingsCountToTrainFullBatch = Math.floor( g_BatchTrainingEntityAllowedCount / batchedSize); | |||||
Engine.PostNetworkCommand({ | |||||
"type": "train", | |||||
"entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), | |||||
"template": g_BatchTrainingType, | |||||
"count": batchedSize | |||||
}); | |||||
// Train remainer in one more building | |||||
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) | |||||
{ | |||||
case "snap": | |||||
case "select": | |||||
case "add": | |||||
var toSelect = []; | |||||
g_Groups.update(); | |||||
for (var ent in g_Groups.groups[groupId].ents) | |||||
toSelect.push(+ent); | |||||
if (action != "add") | |||||
g_Selection.reset(); | |||||
g_Selection.addList(toSelect); | |||||
if (action == "snap" && toSelect.length) | |||||
{ | |||||
let entState = GetEntityState(toSelect[0]); | |||||
let position = entState.position; | |||||
if (position && entState.visibility != "hidden") | |||||
Engine.CameraMoveTo(position.x, position.z); | |||||
} | |||||
break; | |||||
case "save": | |||||
case "breakUp": | |||||
g_Groups.groups[groupId].reset(); | |||||
if (action == "save") | |||||
g_Groups.addEntities(groupId, g_Selection.toList()); | |||||
updateGroups(); | |||||
break; | |||||
} | |||||
} | |||||
var lastIdleUnit = 0; | |||||
var currIdleClassIndex = 0; | |||||
var lastIdleClasses = []; | |||||
function resetIdleUnit() | |||||
{ | |||||
lastIdleUnit = 0; | |||||
currIdleClassIndex = 0; | |||||
lastIdleClasses = []; | |||||
} | |||||
function findIdleUnit(classes) | |||||
{ | |||||
var append = Engine.HotkeyIsPressed("selection.add"); | |||||
var selectall = Engine.HotkeyIsPressed("selection.offscreen"); | |||||
// Reset the last idle unit, etc., if the selection type has changed. | |||||
if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i])) | |||||
resetIdleUnit(); | |||||
lastIdleClasses = classes; | |||||
var data = { | |||||
"viewedPlayer": g_ViewedPlayer, | |||||
"excludeUnits": append ? g_Selection.toList() : [], | |||||
// If the current idle class index is not 0, put the class at that index first. | |||||
"idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) | |||||
}; | |||||
if (!selectall) | |||||
{ | |||||
data.limit = 1; | |||||
data.prevUnit = lastIdleUnit; | |||||
} | |||||
var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); | |||||
if (!idleUnits.length) | |||||
{ | |||||
// TODO: display a message or play a sound to indicate no more idle units, or something | |||||
// Reset for next cycle | |||||
resetIdleUnit(); | |||||
return; | |||||
} | |||||
if (!append) | |||||
g_Selection.reset(); | |||||
g_Selection.addList(idleUnits); | |||||
if (selectall) | |||||
return; | |||||
lastIdleUnit = idleUnits[0]; | |||||
var entityState = GetEntityState(lastIdleUnit); | |||||
var position = entityState.position; | |||||
if (position) | |||||
Engine.CameraMoveTo(position.x, position.z); | |||||
// Move the idle class index to the first class an idle unit was found for. | |||||
var indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); | |||||
currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; | |||||
} | |||||
function clearSelection() | |||||
{ | |||||
if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING) | |||||
{ | |||||
inputState = INPUT_NORMAL; | |||||
placementSupport.Reset(); | |||||
} | |||||
else | |||||
g_Selection.reset(); | |||||
preSelectedAction = ACTION_NONE; | |||||
} | |||||
x, y pairs -.-