469 lines
14 KiB
Lua
469 lines
14 KiB
Lua
local World = require("game.world")
|
|
local Ship = require("game.ship")
|
|
local AsteroidsMod = require("game.asteroids")
|
|
local Bullets = require("game.bullets")
|
|
local Saucers = require("game.saucers")
|
|
local Particles = require("game.particles")
|
|
local HUD = require("game.hud")
|
|
local HighScores = require("game.highscores")
|
|
local Palette = require("rendering.palette")
|
|
local Fonts = require("rendering.fonts")
|
|
local Waves = require("data.waves")
|
|
local Sounds = require("audio.sounds")
|
|
|
|
-- Beat system
|
|
local beat = {
|
|
timer = 0,
|
|
which = 1, -- alternates 1 and 2
|
|
waveStartCount = 4,
|
|
}
|
|
|
|
-- Thrust sound state
|
|
local thrustPlaying = false
|
|
local thrustTimer = 0
|
|
|
|
-- Wave transition
|
|
local waveDelay = 0
|
|
local WAVE_DELAY_TIME = 2.0
|
|
|
|
-- Respawn
|
|
local respawnDelay = 0
|
|
local RESPAWN_DELAY_TIME = 2.0
|
|
|
|
-- Title screen asteroids (decorative)
|
|
local titleAsteroidsInited = false
|
|
|
|
local function startGame()
|
|
World.state = "playing"
|
|
World.wave = 1
|
|
World.score = 0
|
|
World.lives = 3
|
|
World.nextExtraLife = 10000
|
|
World.gameOverTimer = 0
|
|
|
|
Ship.init()
|
|
AsteroidsMod.clear()
|
|
Bullets.clear()
|
|
Saucers.clear()
|
|
Particles.clear()
|
|
|
|
AsteroidsMod.spawnWave(1)
|
|
beat.waveStartCount = AsteroidsMod.count()
|
|
beat.timer = 0
|
|
beat.which = 1
|
|
waveDelay = 0
|
|
respawnDelay = 0
|
|
|
|
Sounds.play("beat1")
|
|
end
|
|
|
|
local function getBeatInterval()
|
|
local total = math.max(beat.waveStartCount, 1)
|
|
local remaining = math.max(AsteroidsMod.count(), 0)
|
|
local ratio = remaining / total
|
|
return 0.1 + 0.7 * ratio
|
|
end
|
|
|
|
local function drawGameWorld()
|
|
love.graphics.push()
|
|
love.graphics.translate(World.offsetX, World.offsetY)
|
|
love.graphics.scale(World.scale)
|
|
|
|
local p = Palette.get()
|
|
local top = World.visibleTop()
|
|
|
|
-- Background
|
|
love.graphics.setColor(p.bg)
|
|
love.graphics.rectangle("fill", 0, top, World.GAME_W, World.visibleH)
|
|
|
|
AsteroidsMod.draw()
|
|
Saucers.draw()
|
|
Bullets.draw()
|
|
Ship.draw()
|
|
Particles.draw()
|
|
|
|
HUD.draw()
|
|
|
|
love.graphics.pop()
|
|
end
|
|
|
|
local function drawTitleScreen()
|
|
local p = Palette.get()
|
|
local sw, sh = World.screenW, World.screenH
|
|
local t = love.timer.getTime()
|
|
|
|
-- Background with drifting asteroids
|
|
love.graphics.push()
|
|
love.graphics.translate(World.offsetX, World.offsetY)
|
|
love.graphics.scale(World.scale)
|
|
|
|
local top = World.visibleTop()
|
|
love.graphics.setColor(p.bg)
|
|
love.graphics.rectangle("fill", 0, top, World.GAME_W, World.visibleH)
|
|
AsteroidsMod.draw()
|
|
|
|
love.graphics.pop()
|
|
|
|
-- Title text
|
|
local centerY = sh * 0.18
|
|
|
|
love.graphics.setFont(Fonts.large)
|
|
love.graphics.setColor(p.bright)
|
|
love.graphics.printf("OMA-ROIDS", 0, centerY, sw, "center")
|
|
|
|
-- Decorative wireframe lines
|
|
love.graphics.setColor(p.asteroid[1], p.asteroid[2], p.asteroid[3], 0.2)
|
|
love.graphics.setLineWidth(1)
|
|
local bx = sw * 0.2
|
|
local by1 = centerY - 10
|
|
local by2 = centerY + Fonts.large:getHeight() + 10
|
|
love.graphics.line(bx, by1, sw - bx, by1)
|
|
love.graphics.line(bx, by2, sw - bx, by2)
|
|
|
|
-- Controls
|
|
love.graphics.setFont(Fonts.small)
|
|
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.4)
|
|
local ctrlY = sh * 0.42
|
|
love.graphics.printf("ARROWS / WASD: MOVE SPACE: FIRE SHIFT: HYPERSPACE", 0, ctrlY, sw, "center")
|
|
|
|
-- Press Enter
|
|
local pulse = 0.3 + math.sin(t * 3) * 0.3
|
|
love.graphics.setFont(Fonts.medium)
|
|
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse + 0.2)
|
|
love.graphics.printf("PRESS ENTER TO START", 0, sh * 0.54, sw, "center")
|
|
|
|
-- High score table
|
|
local allScores = HighScores.getScores()
|
|
if #allScores > 0 then
|
|
HighScores.drawTable(sw, sh, p, Fonts)
|
|
end
|
|
end
|
|
|
|
-- Collision helpers
|
|
local function circleCollision(x1, y1, r1, x2, y2, r2)
|
|
local _, _, dist = World.wrappedDist(x1, y1, x2, y2)
|
|
return dist < r1 + r2
|
|
end
|
|
|
|
function love.load()
|
|
love.mouse.setVisible(false)
|
|
love.graphics.setBackgroundColor(0, 0, 0)
|
|
love.graphics.setLineStyle("smooth")
|
|
Palette.loadFromSystem()
|
|
World.resize(love.graphics.getDimensions())
|
|
Fonts.init(World.scale)
|
|
HighScores.init()
|
|
Sounds.init()
|
|
AsteroidsMod.init()
|
|
World.highScore = HighScores.getHighest()
|
|
World.state = "title"
|
|
|
|
-- Spawn some decorative asteroids for title screen
|
|
AsteroidsMod.spawnWave(1)
|
|
end
|
|
|
|
function love.resize(w, h)
|
|
World.resize(w, h)
|
|
Fonts.init(World.scale)
|
|
end
|
|
|
|
function love.update(dt)
|
|
World.ensureScale()
|
|
|
|
if World.state == "title" then
|
|
AsteroidsMod.update(dt)
|
|
return
|
|
end
|
|
|
|
if World.state == "playing" then
|
|
local s = Ship.get()
|
|
|
|
-- Ship update
|
|
if s.alive then
|
|
Ship.update(dt)
|
|
|
|
-- Thrust sound
|
|
if s.thrustOn then
|
|
thrustTimer = thrustTimer + dt
|
|
if thrustTimer > 0.25 then
|
|
thrustTimer = 0
|
|
Sounds.play("thrust")
|
|
end
|
|
else
|
|
thrustTimer = 0.2 -- play immediately on next thrust
|
|
end
|
|
end
|
|
|
|
AsteroidsMod.update(dt)
|
|
Bullets.update(dt)
|
|
Saucers.update(dt, s.x, s.y)
|
|
Particles.update(dt)
|
|
|
|
-- Beat timer
|
|
beat.timer = beat.timer + dt
|
|
local interval = getBeatInterval()
|
|
if beat.timer >= interval then
|
|
beat.timer = 0
|
|
if beat.which == 1 then
|
|
Sounds.play("beat1")
|
|
beat.which = 2
|
|
else
|
|
Sounds.play("beat2")
|
|
beat.which = 1
|
|
end
|
|
end
|
|
|
|
-- === COLLISION DETECTION ===
|
|
local asteroids = AsteroidsMod.getAll()
|
|
local bullets = Bullets.getAll()
|
|
|
|
-- Bullets vs asteroids
|
|
for bi = #bullets, 1, -1 do
|
|
local b = bullets[bi]
|
|
for ai = #asteroids, 1, -1 do
|
|
local a = asteroids[ai]
|
|
if circleCollision(b.x, b.y, 2, a.x, a.y, a.radius) then
|
|
local result = AsteroidsMod.destroy(ai)
|
|
if result then
|
|
World.addScore(result.points)
|
|
local expSize = result.size == "large" and 12 or (result.size == "medium" and 8 or 5)
|
|
Particles.spawnExplosion(result.x, result.y, expSize, 80)
|
|
Sounds.play("explode_" .. result.size)
|
|
end
|
|
Bullets.remove(bi)
|
|
asteroids = AsteroidsMod.getAll() -- refresh after destroy
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Bullets vs saucer
|
|
local saucer = Saucers.get()
|
|
if saucer then
|
|
for bi = #bullets, 1, -1 do
|
|
local b = bullets[bi]
|
|
if b.owner == "player" and circleCollision(b.x, b.y, 2, saucer.x, saucer.y, saucer.radius) then
|
|
local points, sx, sy = Saucers.destroy()
|
|
World.addScore(points)
|
|
Particles.spawnExplosion(sx, sy, 15, 120)
|
|
Sounds.play("explode_large")
|
|
Bullets.remove(bi)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Ship vs asteroids
|
|
if s.alive and not s.invulnerable then
|
|
local sx, sy, sr = Ship.getCollider()
|
|
for ai = #asteroids, 1, -1 do
|
|
local a = asteroids[ai]
|
|
if circleCollision(sx, sy, sr, a.x, a.y, a.radius) then
|
|
Ship.die()
|
|
Particles.spawnShipDeath(s.x, s.y, s.angle, s.vx, s.vy)
|
|
Sounds.play("ship_explode")
|
|
World.lives = World.lives - 1
|
|
respawnDelay = RESPAWN_DELAY_TIME
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Ship vs saucer
|
|
saucer = Saucers.get()
|
|
if s.alive and not s.invulnerable and saucer then
|
|
local sx, sy, sr = Ship.getCollider()
|
|
if circleCollision(sx, sy, sr, saucer.x, saucer.y, saucer.radius) then
|
|
Ship.die()
|
|
Particles.spawnShipDeath(s.x, s.y, s.angle, s.vx, s.vy)
|
|
Sounds.play("ship_explode")
|
|
World.lives = World.lives - 1
|
|
local points, sx2, sy2 = Saucers.destroy()
|
|
World.addScore(points)
|
|
Particles.spawnExplosion(sx2, sy2, 15, 120)
|
|
respawnDelay = RESPAWN_DELAY_TIME
|
|
end
|
|
end
|
|
|
|
-- Saucer bullets vs ship
|
|
if s.alive and not s.invulnerable then
|
|
local sx, sy, sr = Ship.getCollider()
|
|
for bi = #bullets, 1, -1 do
|
|
local b = bullets[bi]
|
|
if b.owner == "saucer" and circleCollision(b.x, b.y, 2, sx, sy, sr) then
|
|
Ship.die()
|
|
Particles.spawnShipDeath(s.x, s.y, s.angle, s.vx, s.vy)
|
|
Sounds.play("ship_explode")
|
|
World.lives = World.lives - 1
|
|
Bullets.remove(bi)
|
|
respawnDelay = RESPAWN_DELAY_TIME
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Respawn logic
|
|
if not s.alive then
|
|
respawnDelay = respawnDelay - dt
|
|
if respawnDelay <= 0 then
|
|
if World.lives > 0 then
|
|
-- Check centre is safe
|
|
local safe = true
|
|
for _, a in ipairs(AsteroidsMod.getAll()) do
|
|
local _, _, dist = World.wrappedDist(World.GAME_W/2, World.GAME_H/2, a.x, a.y)
|
|
if dist < a.radius + 80 then safe = false; break end
|
|
end
|
|
if safe then
|
|
Ship.respawn()
|
|
end
|
|
else
|
|
-- Game over
|
|
World.state = "game_over"
|
|
World.gameOverTimer = 0
|
|
Sounds.play("ship_explode")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Wave clear check
|
|
if AsteroidsMod.count() == 0 and not Saucers.isActive() then
|
|
waveDelay = waveDelay + dt
|
|
if waveDelay >= WAVE_DELAY_TIME then
|
|
waveDelay = 0
|
|
World.wave = World.wave + 1
|
|
AsteroidsMod.spawnWave(World.wave)
|
|
beat.waveStartCount = AsteroidsMod.count()
|
|
end
|
|
else
|
|
waveDelay = 0
|
|
end
|
|
|
|
elseif World.state == "game_over" then
|
|
World.gameOverTimer = World.gameOverTimer + dt
|
|
Particles.update(dt)
|
|
AsteroidsMod.update(dt)
|
|
|
|
elseif World.state == "high_score_entry" then
|
|
HighScores.updateEntry(dt)
|
|
AsteroidsMod.update(dt)
|
|
end
|
|
end
|
|
|
|
function love.draw()
|
|
love.graphics.clear(0, 0, 0)
|
|
local p = Palette.get()
|
|
local sw, sh = World.screenW, World.screenH
|
|
|
|
if World.state == "title" then
|
|
drawTitleScreen()
|
|
return
|
|
end
|
|
|
|
if World.state == "high_score_entry" then
|
|
love.graphics.push()
|
|
love.graphics.translate(World.offsetX, World.offsetY)
|
|
love.graphics.scale(World.scale)
|
|
local top = World.visibleTop()
|
|
love.graphics.setColor(p.bg)
|
|
love.graphics.rectangle("fill", 0, top, World.GAME_W, World.visibleH)
|
|
AsteroidsMod.draw()
|
|
love.graphics.pop()
|
|
|
|
HighScores.drawEntry(sw, sh, p, Fonts)
|
|
return
|
|
end
|
|
|
|
drawGameWorld()
|
|
|
|
if World.state == "game_over" then
|
|
local t = love.timer.getTime()
|
|
local midY = sh * 0.35
|
|
|
|
if World.gameOverTimer > 0.5 then
|
|
love.graphics.setFont(Fonts.large)
|
|
local textPulse = 0.7 + math.sin(t * 3) * 0.3
|
|
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], textPulse)
|
|
love.graphics.printf("GAME OVER", 0, midY, sw, "center")
|
|
end
|
|
|
|
if World.gameOverTimer > 1.5 then
|
|
love.graphics.setFont(Fonts.medium)
|
|
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.7)
|
|
love.graphics.printf("SCORE: " .. string.format("%06d", World.score), 0, midY + Fonts.large:getHeight() + 8, sw, "center")
|
|
end
|
|
|
|
if World.gameOverTimer > 2.5 then
|
|
local pulse = 0.3 + math.sin(t * 3) * 0.3
|
|
love.graphics.setFont(Fonts.small)
|
|
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse + 0.2)
|
|
love.graphics.printf("PRESS ENTER", 0, midY + Fonts.large:getHeight() + Fonts.medium:getHeight() + 24, sw, "center")
|
|
end
|
|
end
|
|
end
|
|
|
|
function love.keypressed(key)
|
|
if World.state == "title" then
|
|
if key == "return" then startGame() end
|
|
if key == "escape" then love.event.quit() end
|
|
return
|
|
end
|
|
|
|
if World.state == "high_score_entry" then
|
|
local result = HighScores.keypressedEntry(key)
|
|
if result == "done" then
|
|
World.highScore = HighScores.getHighest()
|
|
World.state = "title"
|
|
AsteroidsMod.clear()
|
|
AsteroidsMod.spawnWave(1) -- decorative title asteroids
|
|
end
|
|
return
|
|
end
|
|
|
|
if World.state == "playing" then
|
|
local s = Ship.get()
|
|
if key == "space" and s.alive then
|
|
if Bullets.fire(
|
|
s.x + math.cos(s.angle) * 16,
|
|
s.y + math.sin(s.angle) * 16,
|
|
s.angle, s.vx, s.vy, "player"
|
|
) then
|
|
Sounds.play("fire")
|
|
end
|
|
end
|
|
|
|
if (key == "lshift" or key == "rshift") and s.alive then
|
|
local died = Ship.hyperspace()
|
|
if died then
|
|
Ship.die()
|
|
Particles.spawnShipDeath(s.x, s.y, s.angle, s.vx, s.vy)
|
|
Sounds.play("ship_explode")
|
|
World.lives = World.lives - 1
|
|
respawnDelay = RESPAWN_DELAY_TIME
|
|
end
|
|
end
|
|
end
|
|
|
|
if World.state == "game_over" then
|
|
if key == "return" and World.gameOverTimer > 2.0 then
|
|
if HighScores.isHighScore(World.score) then
|
|
World.state = "high_score_entry"
|
|
HighScores.startEntry(World.score)
|
|
else
|
|
World.state = "title"
|
|
AsteroidsMod.clear()
|
|
AsteroidsMod.spawnWave(1)
|
|
end
|
|
return
|
|
end
|
|
if key == "escape" then
|
|
World.state = "title"
|
|
AsteroidsMod.clear()
|
|
AsteroidsMod.spawnWave(1)
|
|
return
|
|
end
|
|
end
|
|
|
|
if key == "escape" then
|
|
love.event.quit()
|
|
end
|
|
end
|