Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg +++ ps/trunk/binaries/data/config/default.cfg @@ -393,6 +393,7 @@ snaptoedges = "disabled" ; Possible values: disabled, enabled. snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time. +defaultformation = "special/formations/box" ; For walking orders, automatically put units into this formation if they don't have one already. [gui.session.minimap] blinkduration = 1.7 ; The blink duration while pinging Index: ps/trunk/binaries/data/mods/public/globalscripts/Formation.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Formation.js +++ ps/trunk/binaries/data/mods/public/globalscripts/Formation.js @@ -0,0 +1,4 @@ +/** + * The 'null' formation means that units are individuals. + */ +const NULL_FORMATION = "special/formations/null"; Index: ps/trunk/binaries/data/mods/public/gui/session/AutoFormation.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/AutoFormation.js +++ ps/trunk/binaries/data/mods/public/gui/session/AutoFormation.js @@ -0,0 +1,59 @@ +/** + * Handles the logic related to the 'default formation' feature. + * When given a walking order, units that aren't in formation will be put + * in the default formation, to improve pathfinding and reactivity. + * However, when given other tasks (such as e.g. gather), they will be removed + * from any formation they are in, as those orders don't work very well with formations. + * + * Set the default formation to the null formation to disable this entirely. + * + * TODO: it would be nice to let players choose default formations for different orders, + * but that would be neater if orders where defined somewhere unique, + * instead of mostly in unit_actions.js + */ +class AutoFormation +{ + constructor() + { + this.defaultFormation = Engine.ConfigDB_GetValue("user", "gui.session.defaultformation"); + if (!this.defaultFormation) + this.setDefault(NULL_FORMATION); + } + + /** + * Set the default formation to @param formation. + * TODO: would be good to validate, particularly since some formations aren't + * usable with any arbitrary unit type, we may want to warn then. + */ + setDefault(formation) + { + this.defaultFormation = formation; + Engine.ConfigDB_CreateValue("user", "gui.session.defaultformation", this.defaultFormation); + // TODO: It's extremely terrible that we have to explicitly flush the config... + Engine.ConfigDB_SetChanges("user", true); + Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + return true; + } + + isDefault(formation) + { + return formation == this.defaultFormation; + } + + /** + * @return the default formation, or "undefined" if the null formation was chosen, + * otherwise units in formation would disband on any order, which isn't desirable. + */ + getDefault() + { + return this.defaultFormation == NULL_FORMATION ? undefined : this.defaultFormation; + } + + /** + * @return the null formation, or "undefined" if the null formation is the default. + */ + getNull() + { + return this.defaultFormation == NULL_FORMATION ? undefined : NULL_FORMATION; + } +} Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js +++ ps/trunk/binaries/data/mods/public/gui/session/input.js @@ -315,7 +315,8 @@ "entities": selection, "autorepair": true, "autocontinue": true, - "queued": queued + "queued": queued, + "formation": g_AutoFormation.getNull() }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_build", "entity": selection[0] }); @@ -358,6 +359,7 @@ "pieces": wallPlacementInfo.pieces, "startSnappedEntity": wallPlacementInfo.startSnappedEnt, "endSnappedEntity": wallPlacementInfo.endSnappedEnt, + "formation": g_AutoFormation.getNull() }; // make sure that there's at least one non-tower entity getting built, to prevent silly edge cases where the start and end @@ -1241,7 +1243,8 @@ "entities": selection, "targetPositions": entityDistribution.map(pos => pos.toFixed(2)), "targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, - "queued": Engine.HotkeyIsPressed("session.queue") + "queued": Engine.HotkeyIsPressed("session.queue"), + "formation": NULL_FORMATION, }); // Add target markers with a minimum distance of 5 to each other. Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js @@ -310,7 +310,7 @@ if (!g_FormationsInfo.has(data.item)) g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item })); - let formationOk = data.item == "special/formations/null" || canMoveSelectionIntoFormation(data.item); + let formationOk = canMoveSelectionIntoFormation(data.item); let unitIds = data.unitEntStates.map(state => state.id); let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", { "ents": unitIds, @@ -321,8 +321,21 @@ performFormation(unitIds, data.item); }; + data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item); + let formationInfo = g_FormationsInfo.get(data.item); let tooltip = translate(formationInfo.name); + + let isDefaultFormation = g_AutoFormation.isDefault(data.item); + if (data.item === NULL_FORMATION) + tooltip += "\n" + (isDefaultFormation ? + translate("Default formation is disabled.") : + translate("Right-click to disable the default formation feature.")); + else + tooltip += "\n" + (isDefaultFormation ? + translate("This is the default formation, used for movement orders.") : + translate("Right-click to set this as the default formation.")); + if (!formationOk && formationInfo.tooltip) tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red"); data.button.tooltip = tooltip; @@ -330,6 +343,7 @@ data.button.enabled = formationOk && controlsPlayer(data.player); let grayscale = formationOk ? "" : "grayscale:"; data.guiSelection.hidden = !formationSelected; + data.countDisplay.hidden = !isDefaultFormation; data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon; setPanelObjectPosition(data.button, data.i, data.rowLength); Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_helpers.js @@ -8,6 +8,8 @@ function canMoveSelectionIntoFormation(formationTemplate) { + if (formationTemplate == NULL_FORMATION) + return true; if (!(formationTemplate in g_canMoveIntoFormation)) g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", { "ents": g_Selection.toList(), @@ -332,7 +334,7 @@ Engine.PostNetworkCommand({ "type": "formation", "entities": entities, - "name": formationTemplate + "formation": formationTemplate }); } Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels_left/formation_panel.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection_panels_left/formation_panel.xml +++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels_left/formation_panel.xml @@ -7,6 +7,7 @@