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