local World = require("game.world") local Palette = require("rendering.palette") local Lander = {} -- Physics tuned to match original Lunar Lander feel: -- Gentle lunar gravity, thrust comfortably overcomes it, -- deliberate rotation, fuel lasts long enough to learn local GRAVITY = 12 -- gentle lunar pull (original was ~1/6 earth) local THRUST_ACCEL = 36 -- about 3x gravity — can hover and climb local ROT_SPEED = 1.4 -- ~80 deg/sec — deliberate, not twitchy local FUEL_RATE = 6 -- fuel per second — 750 gives ~125 sec of thrust local MAX_SPEED = 120 -- terminal velocity cap -- Lander shape (local coords, angle 0 = pointing up, Y+ is down in world) 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) * 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 (always pulls down) 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 (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 lander.x = math.max(20, math.min(World.WORLD_W - 20, lander.x)) -- 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 -- Kill most horizontal speed lander.vx = lander.vx * 0.3 -- Heavy fuel cost World.fuel = math.max(0, World.fuel - 60) end function Lander.getCollisionPoints() local pts = {} 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 local bodyBottom = {{-8, 6}, {8, 6}, {0, 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 (fully transformed) 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, 8, a, cx, cy) local fx2, fy2 = transformPoint(flameSpread, 8, a, cx, cy) local ftx, fty = transformPoint((math.random()-0.5)*3, 8 + 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 = 5 + math.random() * 8 local fi1x, fi1y = transformPoint(-1.5, 8, a, cx, cy) local fi2x, fi2y = transformPoint(1.5, 8, a, cx, cy) local ft2x, ft2y = transformPoint((math.random()-0.5)*1.5, 8 + flameLen2, a, cx, cy) love.graphics.line(fi1x, fi1y, ft2x, ft2y) love.graphics.line(fi2x, fi2y, ft2x, ft2y) end end return Lander