Rename to OMA-LANDER in title bar and title screen. Retune lander physics for a gentler, original-feel experience (lower gravity, slower rotation, reduced fuel burn, speed cap, top-of-world clamp) and fix thrust vx sign. Rework camera to frame lander and terrain together with a smoother altitude-based zoom. Simplify terrain: pads at fixed zones, cleaner generation loop, zoom-invariant multiplier labels. Stronger abort burst (kills horizontal speed, halves downward vy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
232 lines
7 KiB
Lua
232 lines
7 KiB
Lua
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
|