Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
77243b7
Add missile widget from ZeroK-widgets
claude Jul 1, 2026
91c1f7b
Remove team messaging feature from missile widget
claude Jul 1, 2026
e7ecd5e
Add slow missile (Zeno) to command center widget
claude Jul 1, 2026
31f64c1
Add Missiles tab to command menu above Orders tab
claude Jul 1, 2026
b84a180
Remove floating window UI from missile widget
claude Jul 1, 2026
4e7be09
Add safety checks to prevent crashes after missile launch
claude Jul 1, 2026
dd3fe28
Hide Missiles tab until missile units are built
claude Jul 1, 2026
6d7a261
Add missile count display to command buttons
claude Jul 1, 2026
3d83d6b
Add missile build progress display to command buttons
claude Jul 1, 2026
d5f3a7d
Add visual progress bar support for missile commands
claude Jul 1, 2026
5606e56
Consolidate progress bar logic into missile widget
claude Jul 1, 2026
dd43a39
Add proper visual progress bar support via helper widget
claude Jul 1, 2026
608fac6
Merge progress bar logic into missile widget
claude Jul 1, 2026
3437dab
Add tab badge support with missile unit icon and count
claude Jul 1, 2026
da09486
Implement all 5 widget improvements
claude Jul 1, 2026
7173b99
Fix #4: Centralize missile command IDs to prevent duplication
claude Jul 1, 2026
812e693
Fix remaining 7 code review issues
claude Jul 1, 2026
908d1bb
Fix widget name: 'missle' → 'missile'
claude Jul 1, 2026
f083735
Improve missile configuration with metadata dictionary
claude Jul 1, 2026
757d44d
Refactor missile config: rename missileCmdIDs to missileCmds and embe…
claude Jul 1, 2026
5a70f50
Fix missile widget: crashes, tab layout, tooltips, count/progress bar
amnykon Jul 1, 2026
40349a3
Fix stockpile build progress source for missile buttons
amnykon Jul 1, 2026
fe9a8e8
Keep missiles tab visible and usable with no selection
amnykon Jul 1, 2026
1bfac14
Add second tab row for the missiles tab
amnykon Jul 2, 2026
afe7a9e
Fix missile launch order and command descriptor
amnykon Jul 2, 2026
7f56fb6
Missiles tab badge: per-type icons with count and build progress
amnykon Jul 2, 2026
dbfbda8
Remove misleading (N) hotkey label from tabs without their own hotkey
amnykon Jul 3, 2026
6078092
Missile launch preview: relocate blast ring to terrain that blocks th…
amnykon Jul 4, 2026
1bb6c0e
Missile block preview: learn crest distance for a truer dive angle
amnykon Jul 4, 2026
47e54b8
Missile widget cleanups: fix names, dedupe stockpile, rename tab to L…
amnykon Jul 4, 2026
990893d
Merge upstream/master into missile-orders
amnykon Jul 4, 2026
ca90b4d
Missile launch preview: share Attack AoE's exact impact simulation
amnykon Jul 4, 2026
3836ec0
Missile menu: drop second tab row (topRow) for the Launch tab
claude Jul 7, 2026
b225774
Integral menu: add hidden-tab reveal API (OpenTab/CloseHiddenTab)
claude Jul 7, 2026
ea710ab
Core selector: silo button opens the missiles Launch tab
claude Jul 8, 2026
1680e39
Core selector: show missile count on the silo button
claude Jul 8, 2026
9a4566e
Core selector: single launch button with a 2-column missile grid
claude Jul 8, 2026
6a880c4
Core selector: launch button opens the launcher menu on click
claude Jul 8, 2026
ed0733e
Missile launch tab: make it a hidden tab, remove dead per-cell code
claude Jul 8, 2026
899a89e
Core selector: grow the launch button by half a button per grid row
claude Jul 8, 2026
bf1506f
I guess it is time for an AI policy.
GoogleFrog Jul 8, 2026
1c8eda9
Integral menu: keep the Launch tab button hidden, drop the dead secon…
amnykon Jul 8, 2026
f20aae1
Core selector: fix launch grid cell sizing and resize timing
amnykon Jul 8, 2026
5d1bc8e
Core selector: fix launch count label z-order/jitter and scale it wit…
amnykon Jul 8, 2026
4c60a8f
Launch button: appear promptly when a missile starts building
amnykon Jul 8, 2026
1898071
Missile launcher: core-selector button, build/Zenith, config-driven t…
amnykon Jul 11, 2026
3d45ca7
Merge branch 'master' into missile-orders
amnykon Jul 11, 2026
1daeda9
Missile launcher: launch hotkey, open-state highlight, capacity-safe …
amnykon Jul 11, 2026
3a2e595
Revert seemingly irrelevant integral changes.
GoogleFrog Jul 11, 2026
82a87a9
More revert.
GoogleFrog Jul 11, 2026
046d4bb
Missile launcher: register commands, keep selection after firing
amnykon Jul 11, 2026
412d126
Merge remote-tracking branch 'origin/missile-orders' into missile-orders
amnykon Jul 11, 2026
3d6ee9e
Missile launcher: deselect on tab close, launch-button polish
amnykon Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions LuaRules/Configs/customcmds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ local commands = {
FIELD_FAC_UNIT_TYPE = 38694,
FIELD_FAC_QUEUELESS = 38695,

-- missile launcher (Missile Command Center widget; see Configs/missile_config.lua)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these here and hardcoded?

MISSILE_ZENITH = 39610,
MISSILE_TRINITY = 39611,
MISSILE_REEF = 39612,
MISSILE_SCYLLA = 39613,
MISSILE_EOS = 39614,
MISSILE_SEISMIC = 39615,
MISSILE_SHOCKLEY = 39616,
MISSILE_INFERNO = 39617,
MISSILE_ZENO = 39618,

-- terraform
RAMP = 39734,
LEVEL = 39736,
Expand Down
68 changes: 67 additions & 1 deletion LuaUI/Configs/integral_menu_config.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
local buildCmdFactory, buildCmdEconomy, buildCmdDefence, buildCmdSpecial, buildCmdUnits, cmdPosDef, factoryUnitPosDef = include("Configs/integral_menu_commands_processed.lua", nil, VFS.RAW_FIRST)

-- Launch tab layout, derived from the shared missile config so ids, positions and
-- tooltips live in one place (Configs/missile_config.lua) alongside the widget behaviour.
local missileConfig = include("Configs/missile_config.lua", nil, VFS.RAW_FIRST)
local missileCmds = {}
for _, m in ipairs(missileConfig) do
missileCmds[#missileCmds + 1] = {id = m.cmd, name = m.label, icon = m.unit, col = m.col, row = m.row, tooltip = m.tooltip}
end

local missileCmdPos = {}
for _, missile in ipairs(missileCmds) do
missileCmdPos[missile.id] = {col = missile.col, row = missile.row}
end

local function isMissileCommand(cmdID)
return missileCmdPos[cmdID] ~= nil
end

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Tooltips
Expand Down Expand Up @@ -483,15 +500,64 @@ local factoryButtonLayoutOverride = {
}
}

for _, missile in ipairs(missileCmds) do
local unitDef = UnitDefNames[missile.icon]
local icon = unitDef and ("#" .. unitDef.id) or (imageDir .. 'Bold/attack.png')
commandDisplayConfig[missile.id] = {
texture = icon,
tooltip = missile.tooltip,
drawName = true, -- show the stockpile count / build progress string (set by the missile widget)
}
end

-- Owning any launcher-relevant unit (a launcher, its ammo, or a silo) surfaces the tab.
local missileUnitNames = {["staticmissilesilo"] = true}
for _, m in ipairs(missileConfig) do
for _, l in ipairs(m.launch) do
missileUnitNames[l.unit] = true
end
end

local function hasMissileUnits()
local teamUnits = Spring.GetTeamUnits(Spring.GetMyTeamID()) or {}
for _, unitID in ipairs(teamUnits) do
local unitDefID = Spring.GetUnitDefID(unitID)
if unitDefID then
local unitDef = UnitDefs[unitDefID]
if unitDef and missileUnitNames[unitDef.name] then
return true
end
end
end
return false
end

local commandPanels = {
{
humanName = "Launch",
name = "missiles",
inclusionFunction = function(cmdID)
if not hasMissileUnits() then return false end
local pos = missileCmdPos[cmdID]
return pos ~= nil, pos
end,
loiterable = true,
hiddenTab = true,
buttonLayoutConfig = buttonLayoutConfig.command,
gridHotkeys = true,
-- No returnOnClick: firing should not close the launcher, so multiple missiles
-- can be fired in a row. It is dismissed explicitly (right-click / select a
-- unit / toggle the launch button).
},
{
humanName = "Orders",
name = "orders",
inclusionFunction = function(cmdID, factoryUnitDefID, forceOrdersCommand, unitMobilePanelSize)
return ((cmdID >= 0 or unitMobilePanelSize == 1) and
not buildCmdEconomy[cmdID] and not buildCmdFactory[cmdID] and
not buildCmdSpecial[cmdID] and not buildCmdDefence[cmdID] and
not plateCommandID[cmdID])
not plateCommandID[cmdID] and
not isMissileCommand(cmdID))
end,
loiterable = true,
buttonLayoutConfig = buttonLayoutConfig.command,
Expand Down
85 changes: 85 additions & 0 deletions LuaUI/Configs/missile_config.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
-- Missile launcher configuration. Shared by the Missile Command Center widget
-- (launch/build behaviour) and integral_menu_config.lua (Launch tab layout), so a
-- missile type is described in exactly one place. One entry per launch button, in
-- display order. Command ids are assigned automatically from CMD_BASE below.
--
-- Entry fields:
-- key - internal id (widget command table key).
-- unit - unit def name; supplies the button icon.
-- label - display name.
-- col, row - position in the Launch tab grid.
-- tooltip - button tooltip.
-- cmdType - "ICON_MAP" (default) or "ICON_UNIT_OR_MAP" (can target a unit).
-- siloBuild - unit this type is built as at a missile silo (enables Alt-click build).
-- zenith - true for the Zenith meteor controller (special, non-stockpile behaviour).
-- controllerScope - "separate": this button only exists when Eos/Scylla are NOT combined.
-- launch - list of launchable unit types, each:
-- unit - unit def name that carries/fires this missile.
-- cmd - "ATTACK" or "MANUALFIRE".
-- weaponId - weapon index on that unit.
-- stockpile - "silo" (sits on a silo pad) or "engine" (GetUnitStockpile); omit for Zenith.
-- scope - "combine": active only when Eos and Scylla are combined.

local CMD_BASE = 39610

local missiles = {
{ key = "zenith", unit = "zenith", label = "Zenith", col = 1, row = 1,
zenith = true, cmdType = "ICON_UNIT_OR_MAP",
tooltip = "Zenith (Meteor Controller)\nRains meteors on the target for a few seconds.",
launch = { { unit = "zenith", cmd = "ATTACK", weaponId = 1 } },
},
{ key = "trinity", unit = "staticnuke", label = "Trinity", col = 2, row = 1,
tooltip = "Launch Trinity (Strategic Nuke)\nLong-range nuclear missile.",
launch = { { unit = "staticnuke", cmd = "ATTACK", weaponId = 1, stockpile = "engine" } },
},
{ key = "reef", unit = "shipcarrier", label = "Reef Missile", col = 3, row = 1,
cmdType = "ICON_UNIT_OR_MAP",
tooltip = "Launch Disarm Missile\nDisables units temporarily.",
launch = { { unit = "shipcarrier", cmd = "MANUALFIRE", weaponId = 2, stockpile = "engine" } },
},
{ key = "scylla", unit = "subtacmissile", label = "Scylla", col = 4, row = 1,
controllerScope = "separate",
tooltip = "Launch Scylla (Tactical Nuke)\nSubmarine-launched tactical nuke.",
launch = { { unit = "subtacmissile", cmd = "ATTACK", weaponId = 1, stockpile = "engine" } },
},
{ key = "eos", unit = "tacnuke", label = "Eos", col = 1, row = 2,
siloBuild = "tacnuke",
tooltip = "Launch Eos (Tactical Nuke)\nTactical nuclear missile with high damage.\nAlt-click the map to build one.",
launch = {
{ unit = "tacnuke", cmd = "ATTACK", weaponId = 1, stockpile = "silo" },
-- Scylla folds in here only when the Combine Eos and Scylla option is on.
{ unit = "subtacmissile", cmd = "ATTACK", weaponId = 1, stockpile = "engine", scope = "combine" },
},
},
{ key = "seismic", unit = "seismic", label = "Seismic", col = 2, row = 2,
siloBuild = "seismic",
tooltip = "Launch Seismic\nArea denial seismic missile, slows units.\nAlt-click the map to build one.",
launch = { { unit = "seismic", cmd = "ATTACK", weaponId = 1, stockpile = "silo" } },
},
{ key = "shockley", unit = "empmissile", label = "Shockley", col = 3, row = 2,
siloBuild = "empmissile",
tooltip = "Launch Shockley (EMP)\nElectromagnetic pulse missile disables units.\nAlt-click the map to build one.",
launch = { { unit = "empmissile", cmd = "ATTACK", weaponId = 1, stockpile = "silo" } },
},
{ key = "inferno", unit = "napalmmissile", label = "Inferno", col = 4, row = 2,
siloBuild = "napalmmissile",
tooltip = "Launch Inferno (Napalm)\nNapalm missile with persistent damage.\nAlt-click the map to build one.",
launch = { { unit = "napalmmissile", cmd = "ATTACK", weaponId = 1, stockpile = "silo" } },
},
{ key = "zeno", unit = "missileslow", label = "Zeno", col = 5, row = 2,
siloBuild = "missileslow",
tooltip = "Launch Zeno (Slow Missile)\nSlow homing missile with lingering damage.\nAlt-click the map to build one.",
launch = { { unit = "missileslow", cmd = "ATTACK", weaponId = 1, stockpile = "silo" } },
},
}

-- Command ids come from the shared registry (LuaRules/Configs/customcmds.lua) so they
-- are reserved ZK commands, keyed MISSILE_<KEY>. Fall back to the sequential range if the
-- registry is not available (both resolve to the same 39610.. block).
local registeredCmds = Spring.Utilities and Spring.Utilities.CMD
for i = 1, #missiles do
local m = missiles[i]
m.cmd = (registeredCmds and registeredCmds["MISSILE_" .. string.upper(m.key)]) or (CMD_BASE + i - 1)
end

return missiles
94 changes: 82 additions & 12 deletions LuaUI/Widgets/gui_attack_aoe.lua
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,25 @@ end
--initialization
--------------------------------------------------------------------------------

-- Vlaunch (starburst) flight parameters, derived purely from the weapon def.
-- Used by the AoE preview here and exposed via WG for the missile launch preview,
-- so both compute impact points from an identical trajectory model.
local function BuildVlaunch(weaponDef)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why vlaunch in particular? The next time someone wants a different type of weapondef are they going to make their own builder? Instead, getWeaponInfo should be split into the bits that need unitDef and the bits that don't (or have a fallback).

if (weaponDef.uptime or 0) <= 0 then
return nil
end
-- In the first frame the projectile moves startVelocity + 2*Acceleration
local startSpeed = math.min(weaponDef.startvelocity + weaponDef.weaponAcceleration, weaponDef.projectilespeed)
return {
upFrames = math.floor(weaponDef.uptime * 30 + 0.5) - 2,
accel = weaponDef.weaponAcceleration,
turnRate = weaponDef.turnRate,
startSpeed = startSpeed,
startHeight = startHeights[weaponDef.name] or 0,
endSpeed = weaponDef.projectilespeed,
}
end

local function getWeaponInfo(weaponDef, unitDef)
local retData

Expand Down Expand Up @@ -277,18 +296,7 @@ local function getWeaponInfo(weaponDef, unitDef)
else
retData.aoe = 0
end
if (weaponDef.uptime or 0) > 0 then
-- In the first frame the projectile moves startVelocity + 2*Acceleration
local startSpeed = math.min(weaponDef.startvelocity + weaponDef.weaponAcceleration, weaponDef.projectilespeed)
retData.vlaunch = {
upFrames = math.floor(weaponDef.uptime * 30 + 0.5) - 2,
accel = weaponDef.weaponAcceleration,
turnRate = weaponDef.turnRate,
startSpeed = startSpeed,
startHeight = startHeights[weaponDef.name] or 0,
endSpeed = weaponDef.projectilespeed,
}
end
retData.vlaunch = BuildVlaunch(weaponDef)
retData.cost = cost
retData.mobile = not unitDef.isImmobile
retData.waterWeapon = waterWeapon
Expand Down Expand Up @@ -487,6 +495,39 @@ local function DrawAoE(tx, ty, tz, aoe, ee, alphaMult, offset, circleMode)
glLineWidth(1)
end

-- Shared blast-radius preview, exposed via WG so other widgets (e.g. the missile
-- launch UI) draw their AoE footprint with THIS falloff code instead of duplicating
-- it. Nested rings at (tx,ty,tz) whose alpha decays toward the edge by
-- edgeEffectiveness `ee`. `color` (optional {r,g,b[,a]}) overrides the ring colour;
-- `alphaMult` (optional) scales overall opacity (e.g. a pulse). Line width is derived
-- from the live camera distance, so callers need no per-frame setup of their own.
local function DrawAoEPreview(tx, ty, tz, aoe, ee, color, alphaMult)
if not aoe or aoe <= 0 then
return
end
ee = ee or 1
local cx, cy, cz = GetCameraPosition()
local dx, dy, dz = cx - tx, cy - ty, cz - tz
local camDist = sqrt(dx*dx + dy*dy + dz*dz)
if camDist < 1 then
camDist = 1
end
local r = (color and color[1]) or aoeColor[1]
local g = (color and color[2]) or aoeColor[2]
local b = (color and color[3]) or aoeColor[3]
local baseAlpha = ((color and color[4]) or aoeColor[4]) * (alphaMult or 1)

glLineWidth(math.max(0.05, aoeLineWidthMult * aoe / camDist))
for i = 1, numAoECircles do
local proportion = i / (numAoECircles + 1)
local alpha = baseAlpha * (1 - proportion) / (1 - proportion * ee)
glColor(r, g, b, alpha)
DrawCircle(tx, ty, tz, aoe * proportion)
end
glColor(1, 1, 1, 1)
glLineWidth(1)
end

--------------------------------------------------------------------------------
--dgun/noexplode
--------------------------------------------------------------------------------
Expand Down Expand Up @@ -939,6 +980,33 @@ local function CalculateVlaunchImpact(info, fx, fy, fz, tx, ty, tz)
return false
end

--------------------------------------------------------------------------------
-- Shared vlaunch impact query (used by the missile launch preview widget)
--------------------------------------------------------------------------------

local vlaunchInfoCache = {}

-- Returns the terrain impact point (x, y, z) of a vlaunch (starburst) shot fired
-- from (fx, fy, fz) at (tx, ty, tz), or nil if it reaches the target
-- unobstructed or the weapon is not a vlaunch weapon.
local function GetVlaunchImpact(weaponDefID, fx, fy, fz, tx, ty, tz)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicate and locally-dead code. Also in my tested the visuals were bugged.

local info = vlaunchInfoCache[weaponDefID]
if info == nil then
local wd = WeaponDefs[weaponDefID]
local vlaunch = wd and BuildVlaunch(wd)
info = (vlaunch and {vlaunch = vlaunch, range = wd.range}) or false
vlaunchInfoCache[weaponDefID] = info
end
if not info then
return nil
end
local hx, hy, hz = CalculateVlaunchImpact(info, fx, fy, fz, tx, ty, tz)
if hx then
return hx, hy, hz
end
return nil
end

--------------------------------------------------------------------------------
--Main draw
--------------------------------------------------------------------------------
Expand Down Expand Up @@ -1027,10 +1095,12 @@ function widget:Initialize()
aoeDefInfo[unitDefID], dgunInfo[unitDefID], extraDrawRangeDefInfo[unitDefID] = SetupUnit(unitDef)
end
SetupDisplayLists()
WG.AttackAoE = {GetVlaunchImpact = GetVlaunchImpact, DrawAoEPreview = DrawAoEPreview}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not how these tables are usually constructed so will be hard to search. Use externalFunctions.

end

function widget:Shutdown()
DeleteDisplayLists()
WG.AttackAoE = nil
end

function widget:DrawWorld()
Expand Down
Loading