oma-command/game/fliers.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

296 lines
10 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 Missiles = require("game.missiles")
local Fliers = {}
local active = {}
local cooldownTimer = 0
local COOLDOWN_MIN = 5
local COOLDOWN_MAX = 8
-- Flier types
local BOMBER = "bomber"
local SATELLITE = "satellite"
local KILLER_SAT = "killer_sat"
local function canSpawnBomber(wave) return wave >= 2 end
local function canSpawnSatellite(wave) return wave >= 4 end
local function canSpawnKillerSat(wave) return wave >= 8 end
local function pickCooldown()
return COOLDOWN_MIN + math.random() * (COOLDOWN_MAX - COOLDOWN_MIN)
end
local function spawnFlier(wave)
local canBomber = canSpawnBomber(wave)
local canSat = canSpawnSatellite(wave)
local canKiller = canSpawnKillerSat(wave)
if not canBomber and not canSat then return end
-- Pick type — weighted roll
local ftype
local r = math.random()
if canKiller and r < 0.2 then
ftype = KILLER_SAT
elseif canSat and canBomber then
ftype = r < 0.6 and BOMBER or SATELLITE
elseif canSat then
ftype = SATELLITE
else
ftype = BOMBER
end
local fromLeft = math.random() < 0.5
local speed, y
if ftype == BOMBER then
speed = 20
y = World.GROUND_Y * 0.6
elseif ftype == KILLER_SAT then
speed = 45
y = World.GROUND_Y * 0.25
else
speed = 30
y = World.GROUND_Y * 0.3
end
local startX = fromLeft and -8 or (World.GAME_W + 8)
local endX = fromLeft and (World.GAME_W + 8) or -8
local dirX = fromLeft and 1 or -1
table.insert(active, {
ftype = ftype,
x = startX,
y = y,
speed = speed,
dirX = dirX,
endX = endX,
alive = true,
birth = love.timer.getTime(),
launchTimer = 1.5 + math.random() * 1.0,
hasLaunched = false,
})
end
function Fliers.init()
active = {}
cooldownTimer = pickCooldown()
end
function Fliers.update(dt)
local wave = World.wave
-- Cooldown for spawning
if #active == 0 then
if canSpawnBomber(wave) or canSpawnSatellite(wave) then
cooldownTimer = cooldownTimer - dt
if cooldownTimer <= 0 then
spawnFlier(wave)
cooldownTimer = pickCooldown()
end
end
end
for i = #active, 1, -1 do
local f = active[i]
if f.alive then
f.x = f.x + f.dirX * f.speed * dt
-- Check explosion collision (use a small radius around the flier)
if Explosions.checkCollision(f.x, f.y) then
f.alive = false
World.addScore(f.ftype == KILLER_SAT and 150 or 100)
Explosions.add(f.x, f.y)
table.remove(active, i)
cooldownTimer = pickCooldown()
else
-- Check if off screen
if (f.dirX > 0 and f.x > World.GAME_W + 10) or
(f.dirX < 0 and f.x < -10) then
table.remove(active, i)
cooldownTimer = pickCooldown()
else
-- Periodically launch missiles while on screen
f.launchTimer = f.launchTimer - dt
if f.launchTimer <= 0 then
if f.ftype == KILLER_SAT then
f.launchTimer = 1.2 + math.random() * 0.6
else
f.launchTimer = 2.0 + math.random() * 1.0
end
if f.x > 0 and f.x < World.GAME_W then
Missiles.spawnFromFlier(f.x, f.y, wave)
end
end
end
end
end
end
end
function Fliers.allDone()
return #active == 0
end
function Fliers.clear()
active = {}
cooldownTimer = pickCooldown()
end
function Fliers.draw()
local p = Palette.get(World.wave)
local lw = 1 / World.scale
local t = love.timer.getTime()
for _, f in ipairs(active) do
if f.alive then
local pulse = 0.7 + math.sin(t * 12 + f.birth) * 0.3
local x, y = f.x, f.y
local d = f.dirX -- direction multiplier
if f.ftype == BOMBER then
-- B-52 style strategic bomber
-- Fuselage
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], pulse)
love.graphics.setLineWidth(lw * 1.5)
love.graphics.line(
x + 7*d, y, -- nose tip
x + 5*d, y - 0.8, -- upper nose
x + 2*d, y - 1, -- cockpit
x - 3*d, y - 1, -- fuselage top
x - 6*d, y - 0.5, -- tail section top
x - 8*d, y - 3, -- vertical stabiliser top
x - 8*d, y - 0.5, -- tail join
x - 6*d, y + 0.5, -- fuselage bottom
x + 2*d, y + 1, -- belly
x + 5*d, y + 0.5, -- lower nose
x + 7*d, y -- back to nose
)
-- Swept wings
love.graphics.setLineWidth(lw * 1.2)
-- Upper wing
love.graphics.line(
x + 2*d, y - 1,
x - 1*d, y - 5,
x - 3*d, y - 4.5,
x - 3*d, y - 1
)
-- Lower wing
love.graphics.line(
x + 2*d, y + 1,
x - 1*d, y + 5,
x - 3*d, y + 4.5,
x - 3*d, y + 1
)
-- Engine pods under wings
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], pulse * 0.5)
love.graphics.setLineWidth(lw)
love.graphics.line(x, y - 2.5, x - 1*d, y - 3, x - 1.5*d, y - 2.5)
love.graphics.line(x, y + 2.5, x - 1*d, y + 3, x - 1.5*d, y + 2.5)
-- Tail horizontal stabiliser
love.graphics.line(
x - 6*d, y - 0.5,
x - 7*d, y - 2,
x - 8*d, y - 1.5
)
love.graphics.line(
x - 6*d, y + 0.5,
x - 7*d, y + 1.5,
x - 8*d, y + 1
)
-- Cockpit window detail
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse * 0.4)
love.graphics.line(x + 4*d, y - 0.5, x + 3*d, y - 0.8, x + 2*d, y - 0.8)
else
-- Satellite — detailed orbital platform
local rot = t * 0.8 + f.birth -- slow rotation for visual interest
-- Central body (hexagonal)
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], pulse)
love.graphics.setLineWidth(lw * 1.5)
local s = 2
love.graphics.line(
x-s, y-s*0.5,
x, y-s,
x+s, y-s*0.5,
x+s, y+s*0.5,
x, y+s,
x-s, y+s*0.5,
x-s, y-s*0.5
)
-- Solar panel arrays — two large rectangular panels
love.graphics.setLineWidth(lw * 1.2)
-- Left panel
local px = 3.5
love.graphics.line(x-s, y, x-s-1.5, y)
love.graphics.line(
x-s-1.5, y-2.5,
x-s-px-1.5, y-2.5,
x-s-px-1.5, y+2.5,
x-s-1.5, y+2.5,
x-s-1.5, y-2.5
)
-- Panel grid lines
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], pulse * 0.3)
love.graphics.setLineWidth(lw * 0.7)
love.graphics.line(x-s-3, y-2.5, x-s-3, y+2.5)
love.graphics.line(x-s-1.5, y, x-s-px-1.5, y)
-- Right panel
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], pulse)
love.graphics.setLineWidth(lw * 1.2)
love.graphics.line(x+s, y, x+s+1.5, y)
love.graphics.line(
x+s+1.5, y-2.5,
x+s+px+1.5, y-2.5,
x+s+px+1.5, y+2.5,
x+s+1.5, y+2.5,
x+s+1.5, y-2.5
)
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], pulse * 0.3)
love.graphics.setLineWidth(lw * 0.7)
love.graphics.line(x+s+3, y-2.5, x+s+3, y+2.5)
love.graphics.line(x+s+1.5, y, x+s+px+1.5, y)
-- Antenna dish on top
love.graphics.setColor(p.missile[1], p.missile[2], p.missile[3], pulse * 0.6)
love.graphics.setLineWidth(lw)
love.graphics.line(x, y-s, x+0.5, y-s-2)
love.graphics.line(x-1, y-s-2.5, x+1, y-s-1.5)
-- Blinking status light
if math.sin(t * 8 + f.birth) > 0.5 then
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], 0.8)
love.graphics.circle("fill", x, y, 0.5, 4)
end
-- Killer satellite: add menacing spikes and pulsing red core
if f.ftype == KILLER_SAT then
local killerPulse = 0.6 + math.sin(t * 14 + f.birth) * 0.4
love.graphics.setColor(1.0, 0.2, 0.2, killerPulse)
love.graphics.setLineWidth(lw * 1.3)
-- Spikes around hexagon
love.graphics.line(x, y-s, x, y-s-2)
love.graphics.line(x, y+s, x, y+s+2)
love.graphics.line(x-s, y-s*0.5, x-s-1.5, y-s-1)
love.graphics.line(x+s, y-s*0.5, x+s+1.5, y-s-1)
love.graphics.line(x-s, y+s*0.5, x-s-1.5, y+s+1)
love.graphics.line(x+s, y+s*0.5, x+s+1.5, y+s+1)
-- Angry red core
love.graphics.circle("fill", x, y, 0.9, 8)
end
end
end
end
end
return Fliers