oma-command/game/missiles.lua
nosignal 64ce7f0fcb Initial public release
OMA-COMMAND — Missile Command arcade clone in Love2D with Omarchy theme integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:32:07 +01:00

293 lines
9.3 KiB
Lua

local World = require("game.world")
local Palette = require("rendering.palette")
local Explosions = require("game.explosions")
local Cities = require("game.cities")
local Batteries = require("game.batteries")
local Waves = require("data.waves")
local Sounds = require("audio.sounds")
local Missiles = {}
local active = {}
local queue = {}
local spawnTimer = 0
-- Gather all alive targets (cities + batteries)
local function gatherTargets()
local targets = {}
local cityTargets = Cities.getTargets()
local batTargets = Batteries.getTargets()
for _, t in ipairs(cityTargets) do table.insert(targets, t) end
for _, t in ipairs(batTargets) do table.insert(targets, t) end
return targets
end
-- Determine MIRV chance based on wave
local function mirvChance(wave)
if wave <= 2 then return 0
elseif wave <= 4 then return 0.2
else return math.min(0.3 + (wave - 5) * 0.02, 0.4)
end
end
-- Determine smart bomb chance based on wave
local function smartBombChance(wave)
if wave <= 4 then return 0
elseif wave <= 6 then return 0.15
else return math.min(0.25, 0.25)
end
end
-- Create a missile record from a definition and insert into active
local function activateMissile(def)
local dx = def.targetX - def.startX
local dy = def.targetY - def.startY
local dist = math.sqrt(dx * dx + dy * dy)
if dist < 1 then return end
table.insert(active, {
startX = def.startX,
startY = def.startY,
targetX = def.targetX,
targetY = def.targetY,
targetType = def.targetType,
targetIndex = def.targetIndex,
x = def.startX,
y = def.startY,
speed = def.speed,
dirX = dx / dist,
dirY = dy / dist,
totalDist = dist,
alive = true,
birth = love.timer.getTime(),
isMIRV = def.isMIRV or false,
mirvSplit = false,
mirvSplitFrac = def.mirvSplitFrac or 0,
isSmartBomb = def.isSmartBomb or false,
})
end
function Missiles.spawnWave(wave)
active = {}
queue = {}
-- Prime timer so the first missile launches almost immediately
spawnTimer = (Waves.get(wave).spawn_interval or 2) - 0.3
local config = Waves.get(wave)
local targets = gatherTargets()
if #targets == 0 then return end
local mChance = mirvChance(wave)
local sChance = smartBombChance(wave)
for i = 1, config.missile_count do
local target = targets[math.random(#targets)]
local startX = math.random(10, 246)
local startY = math.random(0, 5)
local isMIRV = false
local isSmartBomb = false
-- Determine type: MIRV and smart bomb are mutually exclusive
if math.random() < mChance then
isMIRV = true
elseif math.random() < sChance then
isSmartBomb = true
end
table.insert(queue, {
startX = startX,
startY = startY,
targetX = target.x,
targetY = target.y,
targetType = target.type,
targetIndex = target.index,
speed = config.missile_speed,
isMIRV = isMIRV,
isSmartBomb = isSmartBomb,
mirvSplitFrac = isMIRV and (0.4 + math.random() * 0.2) or 0,
})
end
end
local function spawnOne()
if #queue == 0 then return end
local def = table.remove(queue, 1)
activateMissile(def)
end
-- Spawn a missile from a flier position toward a random target
function Missiles.spawnFromFlier(startX, startY, wave)
local targets = gatherTargets()
if #targets == 0 then return end
local target = targets[math.random(#targets)]
local config = Waves.get(wave)
activateMissile({
startX = startX,
startY = startY,
targetX = target.x,
targetY = target.y,
targetType = target.type,
targetIndex = target.index,
speed = config.missile_speed,
})
end
function Missiles.update(dt)
local config = Waves.get(World.wave)
spawnTimer = spawnTimer + dt
if #queue > 0 and spawnTimer >= config.spawn_interval then
spawnTimer = 0
spawnOne()
end
-- Collect new MIRV children to add after iteration
local newMissiles = {}
for i = #active, 1, -1 do
local m = active[i]
if m.alive then
-- Smart bomb steering
if m.isSmartBomb then
local exps = Explosions.getActive()
local steerX, steerY = 0, 0
for _, e in ipairs(exps) do
local edx = m.x - e.x
local edy = m.y - e.y
local eDist = math.sqrt(edx * edx + edy * edy)
if eDist < 20 and eDist > 0.1 then
local strength = (20 - eDist) / 20
steerX = steerX + (edx / eDist) * strength * 40
steerY = steerY + (edy / eDist) * strength * 40
end
end
-- Blend steering with original direction
local newDirX = m.dirX * m.speed + steerX
local newDirY = m.dirY * m.speed + steerY
local newLen = math.sqrt(newDirX * newDirX + newDirY * newDirY)
if newLen > 0.1 then
m.dirX = newDirX / newLen
m.dirY = newDirY / newLen
end
end
local step = m.speed * dt
m.x = m.x + m.dirX * step
m.y = m.y + m.dirY * step
-- MIRV split check
if m.isMIRV and not m.mirvSplit then
local dx = m.x - m.startX
local dy = m.y - m.startY
local traveled = math.sqrt(dx * dx + dy * dy)
if traveled >= m.totalDist * m.mirvSplitFrac then
m.mirvSplit = true
Sounds.play("mirv_split")
-- Spawn 2-3 new warheads from current position
local targets = gatherTargets()
if #targets > 0 then
local numChildren = math.random(2, 3)
for c = 1, numChildren do
local target = targets[math.random(#targets)]
table.insert(newMissiles, {
startX = m.x,
startY = m.y,
targetX = target.x,
targetY = target.y,
targetType = target.type,
targetIndex = target.index,
speed = m.speed,
})
end
end
end
end
if Explosions.checkCollision(m.x, m.y) then
m.alive = false
World.addScore(m.isSmartBomb and 125 or 25)
Explosions.add(m.x, m.y)
table.remove(active, i)
else
local dx = m.x - m.startX
local dy = m.y - m.startY
local traveled = math.sqrt(dx * dx + dy * dy)
if traveled >= m.totalDist or m.y >= m.targetY then
Explosions.add(m.targetX, m.targetY)
if m.targetType == "city" then
if Cities.destroy(m.targetIndex) then
Sounds.play("city_destroyed")
else
Sounds.play("impact")
end
elseif m.targetType == "battery" then
Batteries.destroy(m.targetIndex)
Sounds.play("impact")
end
table.remove(active, i)
end
end
end
end
-- Activate MIRV children
for _, def in ipairs(newMissiles) do
activateMissile(def)
end
end
function Missiles.allDone()
return #active == 0 and #queue == 0
end
function Missiles.clear()
active = {}
queue = {}
end
function Missiles.draw()
local p = Palette.get(World.wave)
local lw = 1 / World.scale
local t = love.timer.getTime()
for _, m in ipairs(active) do
local age = t - m.birth
-- Faint trail glow
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], 0.2)
love.graphics.setLineWidth(lw * 4)
love.graphics.line(m.startX, m.startY, m.x, m.y)
-- Main vector trail
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], 0.8)
love.graphics.setLineWidth(lw * 1.5)
love.graphics.line(m.startX, m.startY, m.x, m.y)
-- Warhead shape depends on type
local pulse = 0.6 + math.sin(age * 20) * 0.4
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse)
love.graphics.setLineWidth(lw * 1.5)
if m.isSmartBomb then
-- Small wireframe triangle for smart bombs
local s = 2
love.graphics.line(
m.x, m.y - s,
m.x + s * 0.87, m.y + s * 0.5,
m.x - s * 0.87, m.y + s * 0.5,
m.x, m.y - s
)
else
-- Standard pulsing wireframe diamond
local s = 1.5
love.graphics.line(m.x, m.y-s, m.x+s, m.y, m.x, m.y+s, m.x-s, m.y, m.x, m.y-s)
end
end
end
return Missiles