oma-lander/main.lua
2026-04-18 23:20:40 +01:00

428 lines
14 KiB
Lua

local World = require("game.world")
local Lander = require("game.lander")
local Terrain = require("game.terrain")
local Camera = require("game.camera")
local Particles = require("game.particles")
local Stars = require("game.stars")
local HUD = require("game.hud")
local HighScores = require("game.highscores")
local Palette = require("rendering.palette")
local Fonts = require("rendering.fonts")
local Sounds = require("audio.sounds")
local thrustSoundTimer = 0
local fuelWarnTimer = 0
local STATE_PAUSE = 3.0
local DIFFICULTIES = {
{name = "CADET", gravity = 8, fuel = 1000, description = "TRAINING - LOW GRAVITY"},
{name = "PILOT", gravity = 12, fuel = 750, description = "STANDARD MISSION"},
{name = "COMMANDER", gravity = 16, fuel = 600, description = "EXPERIENCED - HEAVIER PULL"},
{name = "ASTRONAUT", gravity = 22, fuel = 500, description = "ELITE - EXTREME GRAVITY"},
}
local difficultyIndex = 2
local function applyDifficulty(d)
World.gravity = d.gravity
World.startFuel = d.fuel
World.difficultyName = d.name
end
local function startGame()
applyDifficulty(DIFFICULTIES[difficultyIndex])
World.state = "playing"
World.score = 0
World.fuel = World.startFuel
World.time = 0
World.stateTimer = 0
Terrain.generate()
Lander.init()
Camera.reset()
Particles.clear()
end
local function nextAttempt()
World.stateTimer = 0
Terrain.generate()
Lander.init()
Camera.reset()
Particles.clear()
World.state = "playing"
end
local function checkLanding()
local l = Lander.get()
local colPts = Lander.getCollisionPoints()
for _, pt in ipairs(colPts) do
local terrainY = Terrain.getHeightAt(pt.x)
if pt.y >= terrainY then
-- Contact! Evaluate landing
local pad = Terrain.getPadAt(l.x)
local vspeed = math.abs(l.vy)
local hspeed = math.abs(l.vx)
local tilt = math.abs(l.angle)
if pad and vspeed < 8 and hspeed < 15 and tilt < math.rad(5) then
-- A perfect landing — tightest tolerances
local pts = 50 * pad.mult
World.addScore(pts)
World.fuel = World.fuel + 50
World.landingResult = "CONGRATULATIONS — A PERFECT LANDING " .. pts .. " PTS"
World.landingPoints = pts
World.state = "landed"
Lander.land()
Sounds.play("land_good")
elseif pad and vspeed < 15 and hspeed < 30 and tilt < math.rad(15) then
-- Good landing
local pts = 25 * pad.mult
World.addScore(pts)
World.fuel = World.fuel + 25
World.landingResult = "GOOD LANDING " .. pts .. " PTS"
World.landingPoints = pts
World.state = "landed"
Lander.land()
Sounds.play("land_good")
elseif pad and vspeed < 25 and hspeed < 45 and tilt < math.rad(25) then
-- Rough but survivable on-pad landing
local pts = 10 * pad.mult
World.addScore(pts)
World.landingResult = "ROUGH LANDING " .. pts .. " PTS"
World.landingPoints = pts
World.state = "landed"
Lander.land()
Sounds.play("land_hard")
elseif (not pad) and vspeed < 20 and hspeed < 40 and tilt < math.rad(20) then
-- Missed the pad but touched down intact
World.addScore(5)
World.landingResult = "YOU MISSED THE LANDING AREA 5 PTS"
World.landingPoints = 5
World.state = "landed"
Lander.land()
Sounds.play("land_hard")
else
-- Destroyed
World.landingResult = "CRAFT DESTROYED"
World.landingPoints = 0
World.state = "crashed"
Particles.spawnCrash(l.x, l.y, l.vx, l.vy)
Lander.die()
Sounds.play("crash")
end
World.stateTimer = 0
return
end
end
end
local function drawTitleScreen()
local p = Palette.get()
local sw, sh = World.screenW, World.screenH
local t = love.timer.getTime()
-- Background
love.graphics.setColor(p.bg)
love.graphics.rectangle("fill", 0, 0, sw, sh)
Stars.draw(sw, sh)
-- Title
local titleY = sh * 0.15
love.graphics.setFont(Fonts.large)
love.graphics.setColor(p.bright)
love.graphics.printf("OMA-LANDER", 0, titleY, sw, "center")
-- Controls
love.graphics.setFont(Fonts.small)
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.4)
love.graphics.printf("ARROWS / WASD: ROTATE & THRUST SPACE: ABORT", 0, sh * 0.38, sw, "center")
love.graphics.printf("LAND GENTLY ON MARKED PADS FOR POINTS", 0, sh * 0.42, 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 SELECT MISSION", 0, sh * 0.52, sw, "center")
-- High scores
local allScores = HighScores.getScores()
if #allScores > 0 then
HighScores.drawTable(sw, sh, p, Fonts)
end
end
local function drawDifficultyScreen()
local p = Palette.get()
local sw, sh = World.screenW, World.screenH
local t = love.timer.getTime()
love.graphics.setColor(p.bg)
love.graphics.rectangle("fill", 0, 0, sw, sh)
Stars.draw(sw, sh)
love.graphics.setFont(Fonts.large)
love.graphics.setColor(p.bright)
love.graphics.printf("SELECT MISSION", 0, sh * 0.12, sw, "center")
love.graphics.setFont(Fonts.medium)
local lineH = Fonts.medium:getHeight() + 12
local menuTop = sh * 0.30
for i, d in ipairs(DIFFICULTIES) do
local y = menuTop + (i - 1) * lineH
local selected = (i == difficultyIndex)
if selected then
local pulse = 0.7 + math.sin(t * 4) * 0.3
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse)
love.graphics.printf("> " .. d.name .. " <", 0, y, sw, "center")
else
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.5)
love.graphics.printf(d.name, 0, y, sw, "center")
end
end
-- Description of selected mission
love.graphics.setFont(Fonts.small)
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.7)
local d = DIFFICULTIES[difficultyIndex]
local descY = menuTop + #DIFFICULTIES * lineH + 20
love.graphics.printf(d.description, 0, descY, sw, "center")
love.graphics.printf(string.format("GRAVITY %d FUEL %d", d.gravity, d.fuel),
0, descY + Fonts.small:getHeight() + 4, sw, "center")
-- Instructions
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.4)
love.graphics.printf("UP / DOWN: CHANGE MISSION ENTER: BEGIN ESC: BACK",
0, sh - Fonts.small:getHeight() - 12, sw, "center")
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()
HighScores.init()
Sounds.init()
Stars.init()
World.highScore = HighScores.getHighest()
World.state = "title"
end
function love.resize(w, h)
World.resize(w, h)
Fonts.init()
end
function love.update(dt)
World.ensureScale()
if World.state == "title" or World.state == "difficulty_select" then
return
end
if World.state == "playing" then
World.time = World.time + dt
Lander.update(dt)
Camera.update(Lander.get(), Terrain, dt)
Particles.update(dt)
-- Thrust sound
local l = Lander.get()
if l.thrusting then
thrustSoundTimer = thrustSoundTimer + dt
if thrustSoundTimer > 0.2 then
thrustSoundTimer = 0
Sounds.play("thrust")
end
-- Thrust particles — spawn at engine-bell exit transformed by lander angle
local sa, ca = math.sin(l.angle), math.cos(l.angle)
local fx = l.x - sa * 12
local fy = l.y + ca * 12
Particles.spawnThrust(fx, fy, l.angle)
else
thrustSoundTimer = 0.15
end
-- Fuel warning
if World.fuel < 150 and World.fuel > 0 then
fuelWarnTimer = fuelWarnTimer + dt
if fuelWarnTimer > 0.5 then
fuelWarnTimer = 0
Sounds.play("fuel_warn")
end
end
-- Check terrain collision
checkLanding()
elseif World.state == "landed" or World.state == "crashed" then
World.stateTimer = World.stateTimer + dt
Particles.update(dt)
Camera.update(Lander.get(), Terrain, dt)
if World.stateTimer >= STATE_PAUSE then
if World.fuel <= 0 then
World.state = "game_over"
World.stateTimer = 0
else
nextAttempt()
end
end
elseif World.state == "game_over" then
World.stateTimer = World.stateTimer + dt
elseif World.state == "high_score_entry" then
HighScores.updateEntry(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 == "difficulty_select" then
drawDifficultyScreen()
return
end
if World.state == "high_score_entry" then
love.graphics.setColor(p.bg)
love.graphics.rectangle("fill", 0, 0, sw, sh)
Stars.draw(sw, sh)
HighScores.drawEntry(sw, sh, p, Fonts)
return
end
-- Draw game world
love.graphics.setColor(p.bg)
love.graphics.rectangle("fill", 0, 0, sw, sh)
-- Stars (screen space, behind everything)
Stars.draw(sw, sh)
-- World space
Camera.applyTransform()
local minX, minY, maxX, maxY = Camera.getVisibleRect()
Terrain.draw(minX, maxX)
Lander.draw()
Particles.draw()
Camera.popTransform()
-- HUD (screen space)
local l = Lander.get()
local altitude = Camera.getAltitude(l.y, Terrain, l.x)
HUD.draw(altitude, l.vx, l.vy)
-- Landing/crash result text
if World.state == "landed" or World.state == "crashed" then
love.graphics.setFont(Fonts.medium)
local flash = World.state == "landed" or
(math.floor(World.stateTimer * 4) % 2 == 0)
if flash then
if World.state == "crashed" then
love.graphics.setColor(p.warning)
else
love.graphics.setColor(p.bright)
end
love.graphics.printf(World.landingResult, 0, sh * 0.45, sw, "center")
end
end
-- Game over
if World.state == "game_over" then
local t = love.timer.getTime()
love.graphics.setColor(0, 0, 0, 0.5)
love.graphics.rectangle("fill", 0, 0, sw, sh)
if World.stateTimer > 0.5 then
love.graphics.setFont(Fonts.large)
local pulse = 0.7 + math.sin(t * 3) * 0.3
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse)
love.graphics.printf("OUT OF FUEL", 0, sh * 0.3, sw, "center")
end
if World.stateTimer > 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("FINAL SCORE: " .. string.format("%d", World.score), 0, sh * 0.3 + Fonts.large:getHeight() + 8, sw, "center")
end
if World.stateTimer > 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, sh * 0.3 + 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 World.state = "difficulty_select" end
if key == "escape" then love.event.quit() end
return
end
if World.state == "difficulty_select" then
if key == "up" or key == "w" then
difficultyIndex = ((difficultyIndex - 2) % #DIFFICULTIES) + 1
elseif key == "down" or key == "s" then
difficultyIndex = (difficultyIndex % #DIFFICULTIES) + 1
elseif key == "return" then
startGame()
elseif key == "escape" then
World.state = "title"
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"
end
return
end
if World.state == "playing" then
if key == "space" then
Lander.abort()
Sounds.play("abort")
end
end
if World.state == "game_over" then
if key == "return" and World.stateTimer > 2.0 then
if HighScores.isHighScore(World.score) then
World.state = "high_score_entry"
HighScores.startEntry(World.score)
else
World.state = "title"
end
return
end
if key == "escape" then
World.state = "title"
return
end
end
if key == "escape" then
love.event.quit()
end
end