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>
123 lines
3.5 KiB
Lua
123 lines
3.5 KiB
Lua
local Sounds = {}
|
|
|
|
local sources = {}
|
|
local SAMPLE_RATE = 44100
|
|
|
|
local function makeSoundData(duration, generator)
|
|
local samples = math.floor(SAMPLE_RATE * duration)
|
|
local sd = love.sound.newSoundData(samples, SAMPLE_RATE, 16, 1)
|
|
for i = 0, samples - 1 do
|
|
local t = i / SAMPLE_RATE
|
|
local p = i / samples
|
|
local val = generator(t, p)
|
|
sd:setSample(i, math.max(-1, math.min(1, val)))
|
|
end
|
|
return sd
|
|
end
|
|
|
|
local function makeSource(sd)
|
|
return love.audio.newSource(sd, "static")
|
|
end
|
|
|
|
-- Sound generators
|
|
|
|
local function genBeat1(t, p)
|
|
local env = (1 - p) ^ 4
|
|
return math.sin(2 * math.pi * 80 * t) * env * 0.5
|
|
end
|
|
|
|
local function genBeat2(t, p)
|
|
local env = (1 - p) ^ 4
|
|
return math.sin(2 * math.pi * 100 * t) * env * 0.5
|
|
end
|
|
|
|
local function genThrust(t, p)
|
|
local noise = (math.random() * 2 - 1) * 0.3
|
|
local low = math.sin(2 * math.pi * 40 * t) * 0.2
|
|
return (noise + low) * 0.4
|
|
end
|
|
|
|
local function genFire(t, p)
|
|
local freq = 1500 - p * 1100
|
|
local env = (1 - p) ^ 2
|
|
return math.sin(2 * math.pi * freq * t) * env * 0.35
|
|
end
|
|
|
|
local function genExplodeLarge(t, p)
|
|
local env = (1 - p) ^ 1.5
|
|
local sine = math.sin(2 * math.pi * (50 + 30 * (1-p)) * t) * 0.5
|
|
local noise = (math.random() * 2 - 1) * 0.5
|
|
return (sine + noise) * env * 0.5
|
|
end
|
|
|
|
local function genExplodeMedium(t, p)
|
|
local env = (1 - p) ^ 2
|
|
local sine = math.sin(2 * math.pi * (80 + 40 * (1-p)) * t) * 0.4
|
|
local noise = (math.random() * 2 - 1) * 0.4
|
|
return (sine + noise) * env * 0.45
|
|
end
|
|
|
|
local function genExplodeSmall(t, p)
|
|
local env = (1 - p) ^ 3
|
|
local sine = math.sin(2 * math.pi * 150 * t) * 0.3
|
|
local noise = (math.random() * 2 - 1) * 0.5
|
|
return (sine + noise) * env * 0.4
|
|
end
|
|
|
|
local function genSaucerLarge(t, p)
|
|
local freq = 200 + math.sin(2 * math.pi * 5 * t) * 50
|
|
return math.sin(2 * math.pi * freq * t) * 0.25
|
|
end
|
|
|
|
local function genSaucerSmall(t, p)
|
|
local freq = 350 + math.sin(2 * math.pi * 8 * t) * 80
|
|
return math.sin(2 * math.pi * freq * t) * 0.25
|
|
end
|
|
|
|
local function genExtraLife(t, p)
|
|
local freq
|
|
if p < 0.33 then freq = 400
|
|
elseif p < 0.66 then freq = 600
|
|
else freq = 800 end
|
|
local env = 0.7
|
|
if p < 0.05 then env = p / 0.05 * 0.7 end
|
|
if p > 0.9 then env = (1-p) / 0.1 * 0.7 end
|
|
return math.sin(2 * math.pi * freq * t) * env * 0.35
|
|
end
|
|
|
|
local function genShipExplode(t, p)
|
|
local freq = 500 * (1 - p * 0.7)
|
|
local env = (1 - p) ^ 0.8
|
|
local sine = math.sin(2 * math.pi * freq * t) * 0.4
|
|
local noise = (math.random() * 2 - 1) * 0.3 * p
|
|
local pulse = 0.7 + math.sin(2 * math.pi * 4 * t) * 0.3
|
|
return (sine + noise) * env * pulse * 0.5
|
|
end
|
|
|
|
function Sounds.init()
|
|
sources = {}
|
|
local defs = {
|
|
beat1 = {0.08, genBeat1},
|
|
beat2 = {0.08, genBeat2},
|
|
thrust = {0.3, genThrust},
|
|
fire = {0.05, genFire},
|
|
explode_large = {0.5, genExplodeLarge},
|
|
explode_medium = {0.3, genExplodeMedium},
|
|
explode_small = {0.15, genExplodeSmall},
|
|
saucer_large = {0.4, genSaucerLarge},
|
|
saucer_small = {0.4, genSaucerSmall},
|
|
extra_life = {0.3, genExtraLife},
|
|
ship_explode = {1.0, genShipExplode},
|
|
}
|
|
for name, def in pairs(defs) do
|
|
sources[name] = makeSource(makeSoundData(def[1], def[2]))
|
|
end
|
|
end
|
|
|
|
function Sounds.play(name)
|
|
local src = sources[name]
|
|
if not src then return end
|
|
src:clone():play()
|
|
end
|
|
|
|
return Sounds
|