oma-lander/game/terrain.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

166 lines
4.7 KiB
Lua

local World = require("game.world")
local Palette = require("rendering.palette")
local Terrain = {}
local points = {}
local pads = {}
function Terrain.generate()
points = {}
pads = {}
local W = World.WORLD_W
local baseline = 1600
local step = 30
-- Place landing pads first
local padDefs = {
{width = 120, mult = 2, label = "2X"},
{width = 80, mult = 3, label = "3X"},
{width = 50, mult = 5, label = "5X"},
}
-- Distribute pads across the terrain
local padPositions = {}
local usedZones = {}
for _, def in ipairs(padDefs) do
local attempts = 0
local px
repeat
px = 400 + math.random() * (W - 800)
attempts = attempts + 1
local ok = true
for _, used in ipairs(usedZones) do
if math.abs(px - used) < 400 then ok = false; break end
end
if ok then break end
until attempts > 50
local py = baseline + (math.random() - 0.5) * 200
table.insert(padPositions, {x = px, y = py, width = def.width, mult = def.mult, label = def.label})
table.insert(usedZones, px)
table.insert(pads, {
x1 = px - def.width / 2,
x2 = px + def.width / 2,
y = py,
mult = def.mult,
label = def.label,
})
end
-- Sort pads by X for terrain generation
table.sort(pads, function(a, b) return a.x1 < b.x1 end)
-- Generate terrain points left to right
local x = 0
local y = baseline + (math.random() - 0.5) * 100
table.insert(points, {x = x, y = y})
while x < W do
-- Check if we're approaching a pad
local onPad = false
local currentPad = nil
for _, pad in ipairs(pads) do
if x >= pad.x1 - step and x <= pad.x2 + step then
onPad = true
currentPad = pad
break
end
end
if onPad and currentPad then
-- Transition to pad level
if x < currentPad.x1 then
-- Approach: slope down/up to pad
table.insert(points, {x = currentPad.x1 - 5, y = y})
table.insert(points, {x = currentPad.x1, y = currentPad.y})
x = currentPad.x1
end
-- Flat pad
table.insert(points, {x = currentPad.x2, y = currentPad.y})
x = currentPad.x2
y = currentPad.y
-- Resume jagged after pad
x = x + step * 0.5
y = y + (math.random() - 0.5) * 60
table.insert(points, {x = x, y = y})
else
x = x + step + math.random() * step
-- Jagged variation
y = y + (math.random() - 0.5) * 120
y = math.max(baseline - 250, math.min(baseline + 250, y))
table.insert(points, {x = x, y = y})
end
end
-- Ensure last point reaches edge
table.insert(points, {x = W, y = points[#points].y})
end
function Terrain.getPoints()
return points
end
function Terrain.getPads()
return pads
end
function Terrain.getHeightAt(wx)
-- Find terrain height at world X by interpolating between points
if #points < 2 then return 1600 end
if wx <= points[1].x then return points[1].y end
if wx >= points[#points].x then return points[#points].y end
for i = 1, #points - 1 do
if points[i].x <= wx and points[i+1].x > wx then
local t = (wx - points[i].x) / (points[i+1].x - points[i].x)
return points[i].y + t * (points[i+1].y - points[i].y)
end
end
return 1600
end
function Terrain.getPadAt(wx)
for _, pad in ipairs(pads) do
if wx >= pad.x1 and wx <= pad.x2 then
return pad
end
end
return nil
end
function Terrain.draw(visMinX, visMaxX)
local p = Palette.get()
-- Draw terrain surface
love.graphics.setColor(p.terrain)
love.graphics.setLineWidth(2)
local pts = {}
for _, pt in ipairs(points) do
if pt.x >= visMinX - 100 and pt.x <= visMaxX + 100 then
table.insert(pts, pt.x)
table.insert(pts, pt.y)
end
end
if #pts >= 4 then
love.graphics.line(pts)
end
-- Draw landing pads (brighter, with labels)
love.graphics.setColor(p.pad)
love.graphics.setLineWidth(3)
for _, pad in ipairs(pads) do
if pad.x2 >= visMinX and pad.x1 <= visMaxX then
love.graphics.line(pad.x1, pad.y, pad.x2, pad.y)
-- Label below pad
love.graphics.setColor(p.pad[1], p.pad[2], p.pad[3], 0.7)
local labelX = (pad.x1 + pad.x2) / 2
love.graphics.print(pad.label, labelX - 8, pad.y + 5)
end
end
end
return Terrain