Faithful recreation of Atari Asteroids (1979) with vector wireframe aesthetic. Auto-detects Omarchy system theme and font on launch. Features: - Inertia physics (zero friction), rotate/thrust/fire/hyperspace controls - 3 asteroid sizes that split on destroy (large→medium→small) - Large and small UFO saucers with AI (random vs aimed shooting) - Screen wrapping for ship/asteroids/saucers, bullets expire at edges - Ship death fragments, explosion particles - Iconic heartbeat that speeds up as wave clears - Wave progression (4→11 asteroids, speed ramps) - 3 lives, extra life every 10k points - Persistent high scores with 3-letter initial entry - Procedural sound effects (beat, thrust, fire, explosions, saucer drone) - Full-screen scaling, system font detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
166 lines
4.5 KiB
Lua
166 lines
4.5 KiB
Lua
local World = require("game.world")
|
|
local Palette = require("rendering.palette")
|
|
|
|
local Asteroids = {}
|
|
|
|
local active = {}
|
|
local shapes = { large = {}, medium = {}, small = {} }
|
|
|
|
local SIZES = {
|
|
large = { radius = 45, minSpeed = 40, maxSpeed = 80, points = 20 },
|
|
medium = { radius = 22, minSpeed = 60, maxSpeed = 120, points = 50 },
|
|
small = { radius = 11, minSpeed = 80, maxSpeed = 160, points = 100 },
|
|
}
|
|
|
|
local NUM_VARIANTS = 5
|
|
local NUM_VERTICES = 12
|
|
|
|
local function generateShape(baseRadius)
|
|
local verts = {}
|
|
for i = 1, NUM_VERTICES do
|
|
local angle = (i - 1) / NUM_VERTICES * math.pi * 2
|
|
angle = angle + (math.random() - 0.5) * 0.3
|
|
local r = baseRadius * (0.7 + math.random() * 0.6)
|
|
table.insert(verts, {math.cos(angle) * r, math.sin(angle) * r})
|
|
end
|
|
return verts
|
|
end
|
|
|
|
function Asteroids.init()
|
|
for size, info in pairs(SIZES) do
|
|
shapes[size] = {}
|
|
for i = 1, NUM_VARIANTS do
|
|
shapes[size][i] = generateShape(info.radius)
|
|
end
|
|
end
|
|
active = {}
|
|
end
|
|
|
|
function Asteroids.spawnWave(wave)
|
|
local Waves = require("data.waves")
|
|
local config = Waves.get(wave)
|
|
local count = config.asteroidCount
|
|
local speedMult = config.speedMultiplier
|
|
|
|
for i = 1, count do
|
|
-- Spawn at edges, not near centre
|
|
local x, y
|
|
repeat
|
|
x = math.random(0, World.GAME_W)
|
|
y = math.random(0, World.GAME_H)
|
|
until math.abs(x - World.GAME_W/2) > 150 or math.abs(y - World.GAME_H/2) > 150
|
|
|
|
local angle = math.random() * math.pi * 2
|
|
local speed = (SIZES.large.minSpeed + math.random() * (SIZES.large.maxSpeed - SIZES.large.minSpeed)) * speedMult
|
|
|
|
table.insert(active, {
|
|
x = x, y = y,
|
|
vx = math.cos(angle) * speed,
|
|
vy = math.sin(angle) * speed,
|
|
rot = 0,
|
|
rotSpeed = (math.random() - 0.5) * 3,
|
|
size = "large",
|
|
shapeIdx = math.random(1, NUM_VARIANTS),
|
|
radius = SIZES.large.radius,
|
|
})
|
|
end
|
|
end
|
|
|
|
function Asteroids.update(dt)
|
|
for _, a in ipairs(active) do
|
|
a.x = World.wrapX(a.x + a.vx * dt)
|
|
a.y = World.wrapY(a.y + a.vy * dt)
|
|
a.rot = a.rot + a.rotSpeed * dt
|
|
end
|
|
end
|
|
|
|
function Asteroids.destroy(idx)
|
|
local a = active[idx]
|
|
if not a then return nil end
|
|
|
|
local result = {
|
|
x = a.x, y = a.y,
|
|
size = a.size,
|
|
points = SIZES[a.size].points,
|
|
}
|
|
|
|
-- Spawn children
|
|
local childSize = nil
|
|
if a.size == "large" then childSize = "medium"
|
|
elseif a.size == "medium" then childSize = "small"
|
|
end
|
|
|
|
if childSize then
|
|
local info = SIZES[childSize]
|
|
for c = 1, 2 do
|
|
local angle = math.random() * math.pi * 2
|
|
local speed = info.minSpeed + math.random() * (info.maxSpeed - info.minSpeed)
|
|
table.insert(active, {
|
|
x = a.x, y = a.y,
|
|
vx = math.cos(angle) * speed,
|
|
vy = math.sin(angle) * speed,
|
|
rot = 0,
|
|
rotSpeed = (math.random() - 0.5) * 3,
|
|
size = childSize,
|
|
shapeIdx = math.random(1, NUM_VARIANTS),
|
|
radius = info.radius,
|
|
})
|
|
end
|
|
end
|
|
|
|
table.remove(active, idx)
|
|
return result
|
|
end
|
|
|
|
function Asteroids.count()
|
|
return #active
|
|
end
|
|
|
|
function Asteroids.getAll()
|
|
return active
|
|
end
|
|
|
|
function Asteroids.clear()
|
|
active = {}
|
|
end
|
|
|
|
local function drawAsteroidAt(a, ox, oy)
|
|
local shape = shapes[a.size][a.shapeIdx]
|
|
if not shape then return end
|
|
|
|
local pts = {}
|
|
local cos_r = math.cos(a.rot)
|
|
local sin_r = math.sin(a.rot)
|
|
|
|
for _, v in ipairs(shape) do
|
|
local rx = v[1] * cos_r - v[2] * sin_r + ox
|
|
local ry = v[1] * sin_r + v[2] * cos_r + oy
|
|
table.insert(pts, rx)
|
|
table.insert(pts, ry)
|
|
end
|
|
-- Close the loop
|
|
table.insert(pts, pts[1])
|
|
table.insert(pts, pts[2])
|
|
love.graphics.line(pts)
|
|
end
|
|
|
|
function Asteroids.draw()
|
|
local p = Palette.get()
|
|
local lw = 1 / World.scale
|
|
|
|
love.graphics.setColor(p.asteroid)
|
|
love.graphics.setLineWidth(lw * 1.5)
|
|
|
|
local W, H = World.GAME_W, World.GAME_H
|
|
|
|
for _, a in ipairs(active) do
|
|
drawAsteroidAt(a, a.x, a.y)
|
|
-- Wrap ghosts
|
|
if a.x < a.radius then drawAsteroidAt(a, a.x + W, a.y) end
|
|
if a.x > W - a.radius then drawAsteroidAt(a, a.x - W, a.y) end
|
|
if a.y < a.radius then drawAsteroidAt(a, a.x, a.y + H) end
|
|
if a.y > H - a.radius then drawAsteroidAt(a, a.x, a.y - H) end
|
|
end
|
|
end
|
|
|
|
return Asteroids
|