diff --git a/lua/SimCallbacks.lua b/lua/SimCallbacks.lua index 16973f3394..7e4cdb2f83 100644 --- a/lua/SimCallbacks.lua +++ b/lua/SimCallbacks.lua @@ -263,8 +263,37 @@ end Callbacks.WeaponPriorities = import("/lua/weaponpriorities.lua").SetWeaponPriorities +---@param data { target: EntityId } +---@param selection Unit[] +Callbacks.RingExtractor = function(data, selection) + -- verify selection + if (not selection) or + TableEmpty(selection) + then + return + end + + -- verify we have engineers + local engineers = EntityCategoryFilterDown(categories.ENGINEER, selection) + if TableEmpty(engineers) then + return + end + + -- verify the extractor + local extractor = GetUnitById(data.target) --[[@as Unit]] + if (not extractor) or + (not extractor.Army) or + (not OkayToMessWithArmy(extractor.Army)) or + (not EntityCategoryContains(categories.MASSEXTRACTION, extractor)) + then + return + end + + import("/lua/sim/commands/ring-extractor.lua").RingExtractor(extractor, engineers) +end + ---@param data any ----@param selection any +---@param selection Unit[] Callbacks.SelectHighestEngineerAndAssist = function(data, selection) if selection then -- check for cheats diff --git a/lua/options/options.lua b/lua/options/options.lua index 658e577a68..4ffb7528bf 100644 --- a/lua/options/options.lua +++ b/lua/options/options.lua @@ -520,6 +520,19 @@ options = { }, }, + { + title = "Right click to ring extractors with storages", + key = 'structure_capping_feature_01', + type = 'toggle', + default = "on", + custom = { + states = { + {text = "Off", key = "off"}, + {text = "On", key = "on"} -- only-storages-extractors full-suite + }, + }, + }, + { title = "Hold alt to force attack move", key = 'alt_to_force_attack_move', diff --git a/lua/sim/commands/ring-extractor.lua b/lua/sim/commands/ring-extractor.lua new file mode 100644 index 0000000000..150379452a --- /dev/null +++ b/lua/sim/commands/ring-extractor.lua @@ -0,0 +1,99 @@ + +local TableGetn = table.getn + +local BuildOffsets = { { 2, 0 }, { 0, 2 }, { -2, 0 }, { 0, -2 } } + +---@param extractor Unit +---@param engineers Unit[] +RingExtractor = function(extractor, engineers) + + -- verify the storage + local storage = engineers[1].Blueprint.BlueprintId:sub(1, 2) .. 'b1106' + if (not __blueprints[storage]) or + (not engineers[1]:CanBuild(storage)) + then + return + end + + -- split engineers by faction + local faction = engineers[1].Blueprint.FactionCategory + local engineersOfFaction = EntityCategoryFilterDown(categories[faction], engineers) + local engineersOther = EntityCategoryFilterDown(categories.ALLUNITS - categories[faction], engineers) + + local blueprint = extractor:GetBlueprint() + local skirtSize = blueprint.Physics.SkirtSizeX + local cx, _, cz = extractor:GetPositionXYZ() + + -- we manually scan for build skirts in the surrounding area. The function brain:CanBuildStructureAt(...) does + -- not always return correct results: it may end up returning true after factories upgraded + + local x1 = cx - (skirtSize + 10) + local z1 = cz - (skirtSize + 10) + local x2 = cx + (skirtSize + 10) + local z2 = cz + (skirtSize + 10) + + -- find all units that may prevent us from building + local structures = GetUnitsInRect(x1, z1, x2, z2) + if not structures then + return + end + + structures = EntityCategoryFilterDown(categories.STRUCTURE + categories.EXPERIMENTAL, structures) + + -- populate the skirts to check + local skirts = {} + for k, unit in structures do + local blueprint = unit:GetBlueprint() + local px, _, pz = unit:GetPositionXYZ() + local sx, sz = 0.5 * blueprint.Physics.SkirtSizeX, 0.5 * blueprint.Physics.SkirtSizeZ + local rect = { + px - sx, -- top left + pz - sz, -- top left + px + sx, -- bottom right + pz + sz -- bottom right + } + + skirts[k] = rect + end + + local buildLocation = {} + local engineerTable = {} + local emptyTable = {} + + -- loop over build locations in given layer + for k, location in BuildOffsets do + buildLocation[1] = cx + location[1] + buildLocation[3] = cz + location[2] + buildLocation[2] = GetTerrainHeight(buildLocation[1], buildLocation[3]) + + local freeToBuild = true + for _, skirt in skirts do + if buildLocation[1] > skirt[1] and buildLocation[1] < skirt[3] then + if buildLocation[3] > skirt[2] and buildLocation[3] < skirt[4] then + freeToBuild = false + break + end + end + end + + if freeToBuild then + for _, engineer in engineersOfFaction do + engineerTable[1] = engineer + IssueBuildMobile(engineerTable, buildLocation, storage, emptyTable) + end + end + end + + -- assist for all other builders, spread over the number of actual builders + local builderIndex = 1 + local builderCount = TableGetn(engineersOfFaction) + for _, builder in engineersOther do + engineerTable[1] = builder + IssueGuard(engineerTable, engineersOfFaction[builderIndex]) + + builderIndex = builderIndex + 1 + if builderIndex > builderCount then + builderIndex = 1 + end + end +end diff --git a/lua/sim/defaultweapons.lua b/lua/sim/defaultweapons.lua index 8095f8ab6b..fd3daac254 100644 --- a/lua/sim/defaultweapons.lua +++ b/lua/sim/defaultweapons.lua @@ -706,8 +706,11 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { local unit = self.unit if unit.Dead then return end unit:SetBusy(false) - self:WaitForAndDestroyManips() + -- at this point salvo is always done so reset the data + self.CurrentSalvoData = nil + + self:WaitForAndDestroyManips() local bp = self.Blueprint for _, rack in bp.RackBones do if rack.HideMuzzle then @@ -994,7 +997,7 @@ DefaultProjectileWeapon = ClassWeapon(Weapon) { end end end - self.CurrentSalvoData = nil -- once the salvo is done, reset the data + self:PlayFxRackReloadSequence() local currentRackSalvoNumber = self.CurrentRackSalvoNumber if currentRackSalvoNumber <= rackBoneCount then diff --git a/lua/ui/game/commandmode.lua b/lua/ui/game/commandmode.lua index a5f199f656..29f48cc8ed 100644 --- a/lua/ui/game/commandmode.lua +++ b/lua/ui/game/commandmode.lua @@ -46,7 +46,7 @@ local MathAtan = math.atan ---@alias CommandMode 'order' | 'build' | 'buildanchored' | false ---@class CommandModeDataBase ----@field cursor? CommandCap # Similar to the field 'name' +---@field cursor? CommandCap # Similar to the field 'name' ---@field altCursor string # Allows for an alternative cursor ---@class CommandModeDataOrder : CommandModeDataBase @@ -271,9 +271,81 @@ local function CheatSpawn(command, data) }, true) end +--- Allows us to detect a double / triple click +local pStructure1 = nil +function RingExtractor(command) + -- retrieve the option in question, can have values: 'off', 'only-storages-extractors' and 'full-suite' + local option = Prefs.GetFromCurrentProfile('options.structure_capping_feature_01') + + -- bail out - we're not interested + if option == 'off' then + return + end + + -- check if we have engineers + local units = EntityCategoryFilterDown(categories.ENGINEER, command.Units) + if not units[1] then return end + + -- check if we have a building that we target + local structure = GetUnitById(command.Target.EntityId) + if not structure or IsDestroyed(structure) then return end + + -- various conditions written out for maintainability + local isShiftDown = IsKeyDown('Shift') + local isDoubleTapped = structure ~= nil and (pStructure1 == structure) + local isUpgrading = structure:GetFocus() ~= nil + + local isTech1 = structure:IsInCategory('TECH1') + local isTech2 = structure:IsInCategory('TECH2') + local isTech3 = structure:IsInCategory('TECH3') + + if structure:IsInCategory('STRUCTURE') then + if structure:IsInCategory('MASSEXTRACTION') then + local buildStorages = + ( + (isTech1 and isUpgrading and isDoubleTapped and isShiftDown) + or (isTech2 and isUpgrading and isDoubleTapped and isShiftDown) + or (isTech2 and not isUpgrading) + or isTech3 + ) + + if buildStorages then + + -- prevent consecutive calls + local gameTick = GameTick() + if structure.RingStoragesStamp then + if structure.RingStoragesStamp + 5 > gameTick then + return + end + end + + structure.RingStoragesStamp = gameTick + + print("Ringing extractor with storages") + SimCallback({ Func = 'RingExtractor', Args = { target = command.Target.EntityId } }, true) + + if (isTech1 and isUpgrading) or (isTech2 and not isUpgrading) then + structure = nil + pStructure1 = nil + end + end + end + end + + -- keep track of previous structure to identify a 2nd / 3rd click + pStructure1 = structure + + -- prevent building up state when upgrading but shift isn't pressed + if isUpgrading and not isShiftDown then + structure = nil + pStructure1 = nil + end +end + -- cached category strings for performance local categoriesFactories = categories.STRUCTURE * categories.FACTORY local categoriesShields = categories.MOBILE * categories.SHIELD +local categoriesStructure = categories.STRUCTURE --- Upgrades a tech 1 extractor that is being assisted ---@param unit UserUnit @@ -319,7 +391,7 @@ local function OnGuardUnpause(guardees, target) if not target.ThreadUnpause then local id = target:GetEntityId() target.ThreadUnpause = ForkThread( - function () + function() WaitSeconds(1.0) local target = GetUnitById(id) while target do @@ -335,7 +407,7 @@ local function OnGuardUnpause(guardees, target) SetPaused({ target }, false) break end - -- engineer is idle, died, we switch armies, ... + -- engineer is idle, died, we switch armies, ... else candidates[id] = nil end @@ -343,8 +415,8 @@ local function OnGuardUnpause(guardees, target) else target.ThreadUnpauseCandidates = nil target.ThreadUnpause = nil - break; - end + break + ;end WaitSeconds(1.0) target = GetUnitById(id) @@ -354,7 +426,7 @@ local function OnGuardUnpause(guardees, target) end -- add these to keep track - target.ThreadUnpauseCandidates = target.ThreadUnpauseCandidates or { } + target.ThreadUnpauseCandidates = target.ThreadUnpauseCandidates or {} for k, guardee in guardees do target.ThreadUnpauseCandidates[guardee:GetEntityId()] = true end @@ -425,6 +497,11 @@ function OnCommandIssued(command) SimCallback(cb, true) end + -- see if we can cap a structure + if EntityCategoryContains(categoriesStructure, command.Blueprint) then + RingExtractor(command) + end + -- called when: -- - a construction is started elseif command.CommandType == 'BuildMobile' then diff --git a/lua/ui/game/commands/context-based-templates-data.lua b/lua/ui/game/commands/context-based-templates-data.lua index be50f51721..4ad3f6f50f 100644 --- a/lua/ui/game/commands/context-based-templates-data.lua +++ b/lua/ui/game/commands/context-based-templates-data.lua @@ -1,4 +1,26 @@ +--****************************************************************************************************** +--** Copyright (c) 2023 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + ---@class ContextBasedTemplate ---@field Name string # Printed on screen when cycling the templates ---@field TemplateData BuildTemplate # A regular build template, except that it is written in Pascal Case and usually the first unit is removed diff --git a/lua/ui/game/commands/context-based-templates.lua b/lua/ui/game/commands/context-based-templates.lua index f6fd760d96..22eff14173 100644 --- a/lua/ui/game/commands/context-based-templates.lua +++ b/lua/ui/game/commands/context-based-templates.lua @@ -1,4 +1,26 @@ +--****************************************************************************************************** +--** Copyright (c) 2023 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + -- performance related imports local type = type local import = import