318 lines
9.2 KiB
Lua
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
|