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

318 lines
9.2 KiB
Lua

local World = require("game.world")
local Palette = require("rendering.palette")
local Lander = {}
-- Physics tuned to match original Lunar Lander feel.
-- Gravity is set per difficulty via World.gravity.
local THRUST_ACCEL = 36 -- about 3x Pilot-difficulty gravity — can hover and climb
local ROT_SPEED = 1.4 -- ~80 deg/sec — deliberate, not twitchy
local FUEL_RATE = 6 -- fuel per second of thrust
local MAX_SPEED = 120 -- terminal velocity cap
-- Apollo LM-inspired silhouette.
-- Local coords: angle 0 = pointing up, +Y = down.
-- Wide, flat descent stage under a narrower, angular ascent stage;
-- A-frame legs splay out to saucer pads; engine bell protrudes below.
-- Descent stage: wide, flat, boxy base
local DESCENT = {
{-14, 2}, {14, 2}, {14, 8}, {-14, 8},
}
-- Panel lines on descent stage (horizontal divider + vertical bay separators)
local DESCENT_DETAIL = {
{{-14, 5}, {14, 5}},
{{-5, 2}, {-5, 8}},
{{5, 2}, {5, 8}},
}
-- Descent engine bell (hangs below the descent stage)
local ENGINE_BELL = {
{-3, 8}, {-5, 12}, {5, 12}, {3, 8},
}
-- Ascent stage: narrower, angular cabin
local ASCENT = {
{-8, 2}, {-8, -3}, {-7, -7}, {-4, -10}, {4, -10}, {7, -7}, {8, -3}, {8, 2},
}
-- Divider between crew cabin and equipment section
local ASCENT_DETAIL = {
{{-8, -3}, {8, -3}},
}
-- RCS thruster quads jutting out at the ascent-stage corners
local RCS_QUADS = {
{{-10, -5}, {-8, -5}, {-8, -3}, {-10, -3}},
{{10, -5}, {8, -5}, {8, -3}, {10, -3}},
}
-- Docking tunnel on top of ascent stage
local DOCKING_TUNNEL = {
{-3, -10}, {-3, -13}, {3, -13}, {3, -10},
}
-- Triangular forward cockpit windows
local WINDOWS = {
{{-5, -7}, {-2, -9}, {-2, -6}},
{{5, -7}, {2, -9}, {2, -6}},
}
-- Rendezvous radar / top antenna
local ANTENNA = {
{{0, -13}, {0, -17}},
{{-2, -17}, {2, -17}},
}
-- Four landing legs as two visible A-frames (main strut + diagonal brace)
-- Legs attach at the outer edges of the descent stage and splay out wide
local LEGS = {
{{-14, 2}, {-22, 16}}, -- left main strut (upper)
{{-14, 8}, {-22, 16}}, -- left secondary strut (lower, forms A-frame)
{{14, 2}, {22, 16}}, -- right main strut
{{14, 8}, {22, 16}}, -- right secondary strut
}
-- Saucer-shaped foot pads at the A-frame apex
local PAD_POSITIONS = {
{-22, 16}, {22, 16},
}
local PAD_RADIUS = 3
-- Surface-contact probes sticking down from the pads
local PROBES = {
{{-22, 16}, {-22, 20}},
{{22, 16}, {22, 20}},
}
local lander = {}
function Lander.init()
lander.x = World.WORLD_W / 2 + (math.random() - 0.5) * 800
lander.y = 200
lander.vx = 5 + math.random() * 8 -- gentle initial drift
lander.vy = 2 + math.random() * 3 -- slight downward
lander.angle = 0
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 (difficulty-driven, always pulls down)
lander.vy = lander.vy + World.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 (fires out the bottom of the lander, pushing opposite)
lander.thrusting = love.keyboard.isDown("up", "w") and World.fuel > 0
if lander.thrusting then
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
-- Cap speed so it doesn't get out of control
local speed = math.sqrt(lander.vx * lander.vx + lander.vy * lander.vy)
if speed > MAX_SPEED then
lander.vx = lander.vx / speed * MAX_SPEED
lander.vy = lander.vy / speed * MAX_SPEED
end
-- Move
lander.x = lander.x + lander.vx * dt
lander.y = lander.y + lander.vy * dt
-- Clamp X to world bounds and kill horizontal momentum against the wall
if lander.x < 20 then
lander.x = 20
if lander.vx < 0 then lander.vx = 0 end
elseif lander.x > World.WORLD_W - 20 then
lander.x = World.WORLD_W - 20
if lander.vx > 0 then lander.vx = 0 end
end
-- Don't let lander fly off the top
if lander.y < 0 then
lander.y = 0
lander.vy = math.max(0, lander.vy)
end
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 to upright
lander.angle = lander.angle * 0.1
-- Strong upward thrust burst — halve downward velocity and push up
if lander.vy > 0 then
lander.vy = lander.vy * 0.3
end
lander.vy = lander.vy - THRUST_ACCEL * 0.6
lander.vx = lander.vx * 0.3
World.fuel = math.max(0, World.fuel - 60)
end
function Lander.getCollisionPoints()
local pts = {}
local colPts = {
{-22, 16}, {22, 16}, -- foot pads
{-14, 8}, {0, 8}, {14, 8}, -- descent stage bottom (catches hard tilts)
}
for _, cp in ipairs(colPts) do
local wx, wy = transformPoint(cp[1], cp[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
local function drawClosedPolyline(verts, a, cx, cy)
local pts = {}
for _, v in ipairs(verts) do
local wx, wy = transformPoint(v[1], v[2], a, cx, cy)
table.insert(pts, wx)
table.insert(pts, wy)
end
table.insert(pts, pts[1])
table.insert(pts, pts[2])
love.graphics.line(pts)
end
local function drawSegment(seg, a, cx, cy)
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
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
love.graphics.setColor(p.lander)
-- Descent stage outline
love.graphics.setLineWidth(2)
drawClosedPolyline(DESCENT, a, cx, cy)
-- Descent stage panel detail (thinner)
love.graphics.setLineWidth(1)
for _, seg in ipairs(DESCENT_DETAIL) do
drawSegment(seg, a, cx, cy)
end
-- Descent engine bell
love.graphics.setLineWidth(1.5)
drawClosedPolyline(ENGINE_BELL, a, cx, cy)
-- Ascent stage outline
love.graphics.setLineWidth(2)
drawClosedPolyline(ASCENT, a, cx, cy)
-- Ascent stage cabin/equipment divider
love.graphics.setLineWidth(1)
for _, seg in ipairs(ASCENT_DETAIL) do
drawSegment(seg, a, cx, cy)
end
-- Docking tunnel on top
love.graphics.setLineWidth(1.5)
drawClosedPolyline(DOCKING_TUNNEL, a, cx, cy)
-- RCS thruster quads
for _, quad in ipairs(RCS_QUADS) do
drawClosedPolyline(quad, a, cx, cy)
end
-- Legs (A-frame struts)
love.graphics.setLineWidth(1.5)
for _, leg in ipairs(LEGS) do
drawSegment(leg, a, cx, cy)
end
-- Surface-contact probes
love.graphics.setLineWidth(1)
for _, probe in ipairs(PROBES) do
drawSegment(probe, a, cx, cy)
end
-- Foot pads (saucer circles)
love.graphics.setLineWidth(2)
for _, pad in ipairs(PAD_POSITIONS) do
local wx, wy = transformPoint(pad[1], pad[2], a, cx, cy)
love.graphics.circle("line", wx, wy, PAD_RADIUS)
end
-- Forward cockpit windows (dimmer)
love.graphics.setColor(p.lander[1], p.lander[2], p.lander[3], 0.6)
love.graphics.setLineWidth(1)
for _, win in ipairs(WINDOWS) do
drawClosedPolyline(win, a, cx, cy)
end
-- Rendezvous radar / antenna
love.graphics.setColor(p.lander)
love.graphics.setLineWidth(1)
for _, seg in ipairs(ANTENNA) do
drawSegment(seg, a, cx, cy)
end
-- Descent-engine thrust flame (exits the engine bell at y=12)
if lander.thrusting then
love.graphics.setColor(p.thrust)
love.graphics.setLineWidth(2)
local flameLen = 12 + math.random() * 18
local flameSpread = 2 + math.random() * 2
local fx1, fy1 = transformPoint(-flameSpread, 12, a, cx, cy)
local fx2, fy2 = transformPoint(flameSpread, 12, a, cx, cy)
local ftx, fty = transformPoint((math.random()-0.5)*3, 12 + flameLen, a, cx, cy)
love.graphics.line(fx1, fy1, ftx, fty)
love.graphics.line(fx2, fy2, ftx, fty)
-- Inner bright flame
love.graphics.setColor(p.bright)
local flameLen2 = 6 + math.random() * 10
local fi1x, fi1y = transformPoint(-1, 12, a, cx, cy)
local fi2x, fi2y = transformPoint(1, 12, a, cx, cy)
local ft2x, ft2y = transformPoint((math.random()-0.5)*1.5, 12 + flameLen2, a, cx, cy)
love.graphics.line(fi1x, fi1y, ft2x, ft2y)
love.graphics.line(fi2x, fi2y, ft2x, ft2y)
end
end
return Lander