-
Notifications
You must be signed in to change notification settings - Fork 657
Return Fire -> Defensive firestance #8139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Ruwetuin
merged 39 commits into
beyond-all-reason:master
from
SethDGamre:return-fire-rework-2
Jul 4, 2026
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
7fcd756
AllowWeaponTarget callin now accepts Allow
SethDGamre 31a2b22
new firemode and renaming structure
SethDGamre 5e52f45
remove defensive behavior logic from firestate handler
SethDGamre d363fd3
create defensive firestate behavior gadget
SethDGamre 9c4162b
extract dps calc's from gui_info.lua to separate file
SethDGamre 67f7598
mostly base-functional defensive firestate
SethDGamre 53f2cde
factories canReclaim = false
SethDGamre 2a46047
cloak behavior done
SethDGamre 062d351
add customparam for always threats at all range override
SethDGamre 762654b
add alldefs customparam adding for always shoot threats all ranges
SethDGamre bea6617
always shoot kamikaze, and other stuff
SethDGamre c1c5b4c
rename to Fire At Will, Defend, and Hold Fire
SethDGamre 2813f67
improve kamikaze, add echo's
SethDGamre 8dd7572
ignore non-threats to self
SethDGamre c9567d9
fix aa priority bug
SethDGamre e19f5e7
make a modoption
SethDGamre 3cee359
add Spring.Echo's for debugging threat ranges
SethDGamre 09a8a4b
oneshot math
SethDGamre 9dee9b8
improve modoption name and description
SethDGamre 48aa7ab
rename modoption
SethDGamre 3ca70a7
remove debug echos
SethDGamre 4faa885
DPS -> time to kill based on # of shots rework
SethDGamre 6b4d5a5
add drone carriers to always threat list
SethDGamre 5724b8c
rename defense -> defend variables
SethDGamre 6fb5bbf
working, best optimized so far, rename defense->defend
SethDGamre 4a9db55
various improvements
SethDGamre d24a7f8
separate out threat calcs into a separate file
SethDGamre 7b0e00a
code cleanup
SethDGamre aea5e3f
more cleanup
SethDGamre 01c3914
unhallucinate gadgets.lua
SethDGamre 3d3293a
reduce direct alterations of gui_ordermenu.lua
SethDGamre 66b5ba1
revert canReclaim changes to factories, will be separate PR
SethDGamre e7276ac
more descriptive tooltips
SethDGamre 7c2521a
gadgets.lua priority -> result rename, fixed inaccurate comment
SethDGamre 6637ca4
improve tooltip for defend
SethDGamre 7daeb90
Add factory firestate inheritance
SethDGamre 3df04b0
more improvements to threat calcs and qualifiers
SethDGamre 6cc0f32
polish up firestate gui, add keybind support
SethDGamre e929c9c
Merge branch 'master' into return-fire-rework-2
SethDGamre File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| --DEFEND FIRESTATE REWORK: Remove guard; defend targeting is always required | ||
| if not Spring.GetModOptions().experimental_defend_firestate then | ||
| return | ||
| end | ||
|
|
||
| local gadget = gadget ---@type Gadget | ||
|
|
||
| function gadget:GetInfo() | ||
| return { | ||
| name = "Firestate Defend", | ||
| desc = "Limits defend firestate to nearby targets", | ||
| author = "SethDGamre", | ||
| date = "2026.06.28", | ||
| license = "GNU GPL, v2 or later", | ||
| layer = 0, | ||
| enabled = true | ||
| } | ||
| end | ||
|
|
||
| if not gadgetHandler:IsSyncedCode() then | ||
| return false | ||
| end | ||
|
|
||
| --increase safety margin buffer so a pawn can walk through a minefield that's exposed | ||
|
|
||
| local Firestates = VFS.Include("modules/firestates.lua") | ||
| local WeaponThreat = VFS.Include("modules/weaponthreat.lua") | ||
| local CMD_FIRE_STATE = CMD.FIRE_STATE | ||
| local ALWAYS_SHOOT = WeaponThreat.ALWAYS_SHOOT | ||
| local NO_THREAT = WeaponThreat.NO_THREAT | ||
| local HP_CHECK_INTERVAL_FRAMES = Game.gameSpeed * 3 | ||
| local MIN_RADAR_DEFPRIORITY = 10000000 -- this is the floor of what a radar covered unit will generate for defpriorirty. If below this, it's certainly in LOS. For performance | ||
| local UNIT_DEF_ID = 1 | ||
| local IS_DEFEND = 2 | ||
| local NEVER_HESITATE = 3 | ||
| local WEAPON_DEF_IDS = 4 | ||
| local LAST_HEALTH = 5 | ||
| local HP_CHECK_FRAME = 6 | ||
| local RADAR_AGGRO = 7 | ||
| local CLOAKED = 8 | ||
| local ALWAYS_HARMLESS = 9 | ||
| local defThreatRanges = {} | ||
| local neverHesitateAttackers = {} | ||
| local alwaysHarmlessUnitDefs = {} | ||
| local weaponWatchRefCount = {} | ||
| local watchedWeaponsByUnitDef = {} | ||
| local metaData = {} | ||
| local gameFrame = 0 | ||
|
|
||
| local spGetUnitDefID = Spring.GetUnitDefID | ||
| local spGetUnitRulesParam = Spring.GetUnitRulesParam | ||
| local spGetUnitIsCloaked = Spring.GetUnitIsCloaked | ||
| local spGetAllUnits = Spring.GetAllUnits | ||
| local spGetUnitHealth = Spring.GetUnitHealth | ||
| local spGetUnitSeparation = Spring.GetUnitSeparation | ||
|
|
||
| local function addWeaponWatches(weaponDefIDs) | ||
| for index = 1, #weaponDefIDs do | ||
| local weaponDefID = weaponDefIDs[index] | ||
| local refCount = (weaponWatchRefCount[weaponDefID] or 0) + 1 | ||
| weaponWatchRefCount[weaponDefID] = refCount | ||
| if refCount == 1 then | ||
| Script.SetWatchAllowTarget(weaponDefID, true) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| local function removeWeaponWatches(weaponDefIDs) | ||
| for index = 1, #weaponDefIDs do | ||
| local weaponDefID = weaponDefIDs[index] | ||
| local refCount = (weaponWatchRefCount[weaponDefID] or 0) - 1 | ||
| if refCount <= 0 then | ||
| weaponWatchRefCount[weaponDefID] = nil | ||
| Script.SetWatchAllowTarget(weaponDefID, false) | ||
| else | ||
| weaponWatchRefCount[weaponDefID] = refCount | ||
| end | ||
| end | ||
| end | ||
|
|
||
| local function setDefendWatch(unitID, isDefend) | ||
| local meta = metaData[unitID] | ||
| if not meta then | ||
| return | ||
| end | ||
|
|
||
| if isDefend then | ||
| if meta[IS_DEFEND] then | ||
| meta[LAST_HEALTH] = spGetUnitHealth(unitID) | ||
| return | ||
| end | ||
|
|
||
| local unitDefID = meta[UNIT_DEF_ID] | ||
| local weaponDefIDs = watchedWeaponsByUnitDef[unitDefID] | ||
| meta[IS_DEFEND] = true | ||
| meta[NEVER_HESITATE] = neverHesitateAttackers[unitDefID] or false | ||
| meta[WEAPON_DEF_IDS] = weaponDefIDs | ||
| meta[LAST_HEALTH] = spGetUnitHealth(unitID) | ||
|
|
||
| if weaponDefIDs then | ||
| addWeaponWatches(weaponDefIDs) | ||
| end | ||
| elseif meta[IS_DEFEND] then | ||
| if meta[WEAPON_DEF_IDS] then | ||
| removeWeaponWatches(meta[WEAPON_DEF_IDS]) | ||
| end | ||
| meta[IS_DEFEND] = nil | ||
| meta[WEAPON_DEF_IDS] = nil | ||
| meta[RADAR_AGGRO] = nil | ||
| meta[HP_CHECK_FRAME] = nil | ||
| end | ||
| end | ||
|
|
||
| local function checkDefendUnitHealth(attackerID, meta) | ||
| local nextCheckFrame = meta[HP_CHECK_FRAME] | ||
| if nextCheckFrame and gameFrame < nextCheckFrame then | ||
| return | ||
| end | ||
|
|
||
| local currentHealth = spGetUnitHealth(attackerID) | ||
| if not currentHealth then | ||
| return | ||
| end | ||
|
|
||
| local lastHealth = meta[LAST_HEALTH] | ||
| if lastHealth and currentHealth < lastHealth then | ||
| meta[RADAR_AGGRO] = true | ||
| elseif lastHealth and currentHealth > lastHealth then | ||
| meta[RADAR_AGGRO] = nil | ||
| end | ||
|
|
||
| meta[LAST_HEALTH] = currentHealth | ||
| meta[HP_CHECK_FRAME] = gameFrame + HP_CHECK_INTERVAL_FRAMES | ||
| end | ||
|
|
||
| local function updateDefendWatchFromRulesParam(unitID) | ||
| local state = spGetUnitRulesParam(unitID, Firestates.RULES_PARAM) | ||
| setDefendWatch(unitID, state == Firestates.DEFEND) | ||
| end | ||
|
|
||
| local function createUnitMeta(unitDefID) | ||
| local meta = { [UNIT_DEF_ID] = unitDefID } | ||
| if alwaysHarmlessUnitDefs[unitDefID] then | ||
| meta[ALWAYS_HARMLESS] = true | ||
| end | ||
| return meta | ||
| end | ||
|
|
||
| function gadget:UnitCommand(unitID, unitDefID, unitTeamID, cmdID, cmdParams, cmdOptions, cmdTag, playerID, fromSynced, fromLua) | ||
| if cmdID == CMD_FIRE_STATE then | ||
| updateDefendWatchFromRulesParam(unitID) | ||
| end | ||
| end | ||
|
|
||
| function gadget:UnitCreated(unitID, unitDefID, unitTeam) | ||
| metaData[unitID] = createUnitMeta(unitDefID) | ||
| end | ||
|
|
||
| function gadget:UnitFinished(unitID, unitDefID, unitTeam) | ||
| updateDefendWatchFromRulesParam(unitID) | ||
| end | ||
|
|
||
| function gadget:UnitCloaked(unitID, unitDefID, unitTeam) | ||
| local meta = metaData[unitID] | ||
| if meta then | ||
| meta[CLOAKED] = true | ||
| end | ||
| end | ||
|
|
||
| function gadget:UnitDecloaked(unitID, unitDefID, unitTeam) | ||
| local meta = metaData[unitID] | ||
| if meta then | ||
| meta[CLOAKED] = nil | ||
| end | ||
| end | ||
|
|
||
| function gadget:UnitDestroyed(unitID, unitDefID, unitTeam) | ||
| local meta = metaData[unitID] | ||
| if meta and meta[WEAPON_DEF_IDS] then | ||
| removeWeaponWatches(meta[WEAPON_DEF_IDS]) | ||
| end | ||
| metaData[unitID] = nil | ||
| end | ||
|
|
||
| function gadget:GameFrame(frame) | ||
| gameFrame = frame | ||
| end | ||
|
|
||
| function gadget:AllowWeaponTarget(attackerID, targetID, attackerWeaponNum, attackerWeaponDefID, defPriority) | ||
| local targetMeta = metaData[targetID] | ||
| if not targetMeta then | ||
| return false | ||
| end | ||
|
|
||
| local attackerMeta = metaData[attackerID] | ||
| local isDefend = attackerMeta and attackerMeta[IS_DEFEND] | ||
|
|
||
| if targetMeta[ALWAYS_HARMLESS] then | ||
| return not isDefend | ||
| end | ||
|
|
||
| if not isDefend then | ||
| return true | ||
| end | ||
|
|
||
| local rangesForWeapon = defThreatRanges[attackerWeaponDefID] | ||
| if not rangesForWeapon then | ||
| return true | ||
| end | ||
|
|
||
| local threatRange = rangesForWeapon[targetMeta[UNIT_DEF_ID]] | ||
| if not threatRange or threatRange == NO_THREAT then | ||
| return false | ||
| end | ||
|
|
||
| if attackerMeta[CLOAKED] then | ||
| return false | ||
| end | ||
|
|
||
| if attackerMeta[NEVER_HESITATE] then | ||
| return true | ||
| end | ||
|
|
||
| if (defPriority or 0) > MIN_RADAR_DEFPRIORITY then | ||
| checkDefendUnitHealth(attackerID, attackerMeta) | ||
| return attackerMeta[RADAR_AGGRO] or false | ||
| end | ||
|
|
||
| if threatRange == ALWAYS_SHOOT then | ||
| return true | ||
| end | ||
|
|
||
| local separation = spGetUnitSeparation(attackerID, targetID) | ||
| return not separation or separation <= threatRange | ||
| end | ||
|
|
||
| function gadget:Initialize() | ||
| defThreatRanges, watchedWeaponsByUnitDef, neverHesitateAttackers, alwaysHarmlessUnitDefs = WeaponThreat.buildDefendData() | ||
|
|
||
| for _, unitID in ipairs(spGetAllUnits()) do | ||
| local unitDefID = spGetUnitDefID(unitID) | ||
| local meta = createUnitMeta(unitDefID) | ||
| metaData[unitID] = meta | ||
| updateDefendWatchFromRulesParam(unitID) | ||
| if spGetUnitIsCloaked(unitID) then | ||
| meta[CLOAKED] = true | ||
| end | ||
| end | ||
| end | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See notes on AllowWeaponTarget in unit_aa_targeting_priority.lua.