oma-lander/game/lander.lua
28allday 790ca87bfb Lunar Lander: complete Love2D game with Omarchy integration
Faithful recreation of Atari Lunar Lander (1979) with vector wireframe aesthetic.
Auto-detects Omarchy system theme and font on launch.

Features:
- Dynamic zoom camera (zooms in as you approach the surface)
- Procedural jagged terrain with flat landing pads (2X, 3X, 5X multipliers)
- Apollo-style wireframe lander with thrust flame
- Gravity, thrust, rotation physics
- Landing evaluation: good (speed/angle/pad check), hard, crash
- Fuel management (750 starting, +50 for good landings)
- Crash debris particles, thrust exhaust particles
- Star field background
- HUD: altitude, horizontal/vertical speed, fuel bar, score, time
- Persistent high scores with 3-letter initial entry
- Procedural sound effects (thrust, landing chimes, crash, fuel warning)
- Full-screen scaling, system font detection

Controls: Arrows/WASD rotate+thrust, Space abort, Enter start

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:42:13 +01:00

211 lines
5.9 KiB
Lua

local World = require("game.world")
local Palette = require("rendering.palette")
local Lander = {}
local GRAVITY = 25
local THRUST_ACCEL = 60
local ROT_SPEED = 2.1 -- ~120 deg/sec in radians
local FUEL_RATE = 12 -- fuel per second at full thrust
-- Lander shape (local coords, 0 = pointing up, Y+ down)
local BODY = {
{-8, -8}, {-4, -12}, {4, -12}, {8, -8},
{10, 0}, {8, 6}, {-8, 6}, {-10, 0},
}
local LEGS = {
{{-8, 6}, {-14, 16}},
{{-6, 6}, {-14, 16}},
{{8, 6}, {14, 16}},
{{6, 6}, {14, 16}},
}
local FEET = {
{{-17, 16}, {-11, 16}},
{{11, 16}, {17, 16}},
}
local WINDOW = {
{{-3, -6}, {3, -6}},
{{3, -6}, {3, -2}},
{{3, -2}, {-3, -2}},
{{-3, -2}, {-3, -6}},
}
local ANTENNA = {
{{0, -12}, {0, -18}},
{{-3, -18}, {3, -18}},
}
local lander = {}
function Lander.init()
lander.x = World.WORLD_W / 2 + (math.random() - 0.5) * 1000
lander.y = 200
lander.vx = 20 + math.random() * 20
lander.vy = 5 + math.random() * 10
lander.angle = 0 -- 0 = pointing up
lander.alive = true
lander.landed = false
lander.thrusting = false
end
function Lander.get()
return lander
end
local function transformPoint(px, py, angle, cx, cy)
local cos_a = math.cos(angle)
local sin_a = math.sin(angle)
return cx + px * cos_a - py * sin_a,
cy + px * sin_a + py * cos_a
end
function Lander.update(dt)
if not lander.alive or lander.landed then return end
-- Gravity
lander.vy = lander.vy + GRAVITY * dt
-- Rotation
if love.keyboard.isDown("left", "a") then
lander.angle = lander.angle - ROT_SPEED * dt
end
if love.keyboard.isDown("right", "d") then
lander.angle = lander.angle + ROT_SPEED * dt
end
-- Thrust
lander.thrusting = love.keyboard.isDown("up", "w") and World.fuel > 0
if lander.thrusting then
-- Thrust in the direction the lander is pointing (up from lander's perspective)
lander.vx = lander.vx - math.sin(lander.angle) * THRUST_ACCEL * dt
lander.vy = lander.vy - math.cos(lander.angle) * THRUST_ACCEL * dt
World.fuel = math.max(0, World.fuel - FUEL_RATE * dt)
end
-- Move
lander.x = lander.x + lander.vx * dt
lander.y = lander.y + lander.vy * dt
-- Clamp X
lander.x = math.max(20, math.min(World.WORLD_W - 20, lander.x))
end
function Lander.abort()
if not lander.alive or lander.landed then return end
if World.fuel < 10 then return end
-- Auto-level: snap angle toward 0
lander.angle = lander.angle * 0.3
-- Full thrust burst
lander.vy = lander.vy - THRUST_ACCEL * 0.5
-- Costs a chunk of fuel
World.fuel = math.max(0, World.fuel - 50)
end
function Lander.getCollisionPoints()
-- Return transformed foot positions for terrain collision
local pts = {}
-- Foot endpoints
local footPts = {{-17, 16}, {-11, 16}, {11, 16}, {17, 16}}
for _, fp in ipairs(footPts) do
local wx, wy = transformPoint(fp[1], fp[2], lander.angle, lander.x, lander.y)
table.insert(pts, {x = wx, y = wy})
end
-- Body bottom
local bodyBottom = {{-8, 6}, {8, 6}}
for _, bp in ipairs(bodyBottom) do
local wx, wy = transformPoint(bp[1], bp[2], lander.angle, lander.x, lander.y)
table.insert(pts, {x = wx, y = wy})
end
return pts
end
function Lander.die()
lander.alive = false
end
function Lander.land()
lander.landed = true
lander.vx = 0
lander.vy = 0
end
function Lander.draw()
if not lander.alive then return end
local p = Palette.get()
local a = lander.angle
local cx, cy = lander.x, lander.y
-- Body outline
love.graphics.setColor(p.lander)
love.graphics.setLineWidth(2)
local bodyPts = {}
for _, v in ipairs(BODY) do
local wx, wy = transformPoint(v[1], v[2], a, cx, cy)
table.insert(bodyPts, wx)
table.insert(bodyPts, wy)
end
table.insert(bodyPts, bodyPts[1])
table.insert(bodyPts, bodyPts[2])
love.graphics.line(bodyPts)
-- Legs
love.graphics.setLineWidth(1.5)
for _, leg in ipairs(LEGS) do
local x1, y1 = transformPoint(leg[1][1], leg[1][2], a, cx, cy)
local x2, y2 = transformPoint(leg[2][1], leg[2][2], a, cx, cy)
love.graphics.line(x1, y1, x2, y2)
end
-- Feet
love.graphics.setLineWidth(2)
for _, foot in ipairs(FEET) do
local x1, y1 = transformPoint(foot[1][1], foot[1][2], a, cx, cy)
local x2, y2 = transformPoint(foot[2][1], foot[2][2], a, cx, cy)
love.graphics.line(x1, y1, x2, y2)
end
-- Window
love.graphics.setColor(p.lander[1], p.lander[2], p.lander[3], 0.5)
love.graphics.setLineWidth(1)
for _, seg in ipairs(WINDOW) do
local x1, y1 = transformPoint(seg[1][1], seg[1][2], a, cx, cy)
local x2, y2 = transformPoint(seg[2][1], seg[2][2], a, cx, cy)
love.graphics.line(x1, y1, x2, y2)
end
-- Antenna
love.graphics.setColor(p.lander)
for _, seg in ipairs(ANTENNA) do
local x1, y1 = transformPoint(seg[1][1], seg[1][2], a, cx, cy)
local x2, y2 = transformPoint(seg[2][1], seg[2][2], a, cx, cy)
love.graphics.line(x1, y1, x2, y2)
end
-- Thrust flame
if lander.thrusting then
love.graphics.setColor(p.thrust)
love.graphics.setLineWidth(2)
local flameLen = 10 + math.random() * 15
local flameSpread = 3 + math.random() * 3
local fx1, fy1 = transformPoint(-flameSpread, 6, a, cx, cy)
local fx2, fy2 = transformPoint(flameSpread, 6, a, cx, cy)
local ftx, fty = transformPoint((math.random()-0.5)*3, 6 + flameLen, a, cx, cy)
love.graphics.line(fx1, fy1, ftx, fty)
love.graphics.line(fx2, fy2, ftx, fty)
-- Inner flame
local flameLen2 = 5 + math.random() * 8
local ft2x, ft2y = transformPoint((math.random()-0.5)*2, 6 + flameLen2, a, cx, cy)
love.graphics.setColor(p.bright)
love.graphics.line(cx, cy + 3, ft2x, ft2y)
end
end
return Lander