oma-lander/main.lua
28allday 515f78a3e7 OMA-LANDER: rebrand and tune physics, camera, terrain
Rename to OMA-LANDER in title bar and title screen. Retune lander
physics for a gentler, original-feel experience (lower gravity, slower
rotation, reduced fuel burn, speed cap, top-of-world clamp) and fix
thrust vx sign. Rework camera to frame lander and terrain together
with a smoother altitude-based zoom. Simplify terrain: pads at fixed
zones, cleaner generation loop, zoom-invariant multiplier labels.
Stronger abort burst (kills horizontal speed, halves downward vy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:39:12 +01:00

336 lines
9.8 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 function startGame()
World.state = "playing"
World.score = 0
World.fuel = 750
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 < 15 and hspeed < 30 and tilt < math.rad(15) then
-- Good landing
local pts = 50 * pad.mult
World.addScore(pts)
World.fuel = World.fuel + 50
World.landingResult = "GOOD LANDING! " .. pad.label .. " = " .. 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
-- Hard landing
local pts = 15 * pad.mult
World.addScore(pts)
World.landingResult = "HARD LANDING " .. pts .. " PTS"
World.landingPoints = pts
World.state = "landed"
Lander.land()
Sounds.play("land_hard")
else
-- Crash
World.addScore(5)
World.landingResult = "CRASH! 5 PTS"
World.landingPoints = 5
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 START", 0, sh * 0.52, sw, "center")
-- High scores
local allScores = HighScores.getScores()
if #allScores > 0 then
HighScores.drawTable(sw, sh, p, Fonts)
end
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(1)
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(1)
end
function love.update(dt)
World.ensureScale()
if World.state == "title" 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
local fx, fy = l.x, l.y + 8
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()
-- Game over if fuel depleted and landed/still
if World.fuel <= 0 and not l.thrusting then
-- Let them coast until they crash or land
end
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 == "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 == "crashed" and
(math.floor(World.stateTimer * 4) % 2 == 0) or true
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 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"
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