428 lines
14 KiB
Lua
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
|