local World = require("game.world") local Lander = require("game.lander") local Terrain = require("game.terrain") local Camera = require("game.camera") local Particles = require("game.particles") local Stars = require("game.stars") local HUD = require("game.hud") local HighScores = require("game.highscores") local Palette = require("rendering.palette") local Fonts = require("rendering.fonts") local Sounds = require("audio.sounds") local thrustSoundTimer = 0 local fuelWarnTimer = 0 local STATE_PAUSE = 3.0 local function startGame() World.state = "playing" World.score = 0 World.fuel = 750 World.time = 0 World.stateTimer = 0 Terrain.generate() Lander.init() Camera.reset() Particles.clear() end local function nextAttempt() World.stateTimer = 0 Terrain.generate() Lander.init() Camera.reset() Particles.clear() World.state = "playing" end local function checkLanding() local l = Lander.get() local colPts = Lander.getCollisionPoints() for _, pt in ipairs(colPts) do local terrainY = Terrain.getHeightAt(pt.x) if pt.y >= terrainY then -- Contact! Evaluate landing local pad = Terrain.getPadAt(l.x) local vspeed = math.abs(l.vy) local hspeed = math.abs(l.vx) local tilt = math.abs(l.angle) if pad and vspeed < 15 and hspeed < 30 and tilt < math.rad(15) then -- Good landing local pts = 50 * pad.mult World.addScore(pts) World.fuel = World.fuel + 50 World.landingResult = "GOOD LANDING! " .. pad.label .. " = " .. pts .. " PTS" World.landingPoints = pts World.state = "landed" Lander.land() Sounds.play("land_good") elseif pad and vspeed < 25 and hspeed < 45 and tilt < math.rad(25) then -- Hard landing local pts = 15 * pad.mult World.addScore(pts) World.landingResult = "HARD LANDING " .. pts .. " PTS" World.landingPoints = pts World.state = "landed" Lander.land() Sounds.play("land_hard") else -- Crash World.addScore(5) World.landingResult = "CRASH! 5 PTS" World.landingPoints = 5 World.state = "crashed" Particles.spawnCrash(l.x, l.y, l.vx, l.vy) Lander.die() Sounds.play("crash") end World.stateTimer = 0 return end end end local function drawTitleScreen() local p = Palette.get() local sw, sh = World.screenW, World.screenH local t = love.timer.getTime() -- Background love.graphics.setColor(p.bg) love.graphics.rectangle("fill", 0, 0, sw, sh) Stars.draw(sw, sh) -- Title local titleY = sh * 0.15 love.graphics.setFont(Fonts.large) love.graphics.setColor(p.bright) love.graphics.printf("LUNAR LANDER", 0, titleY, sw, "center") -- Controls love.graphics.setFont(Fonts.small) love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.4) love.graphics.printf("ARROWS / WASD: ROTATE & THRUST SPACE: ABORT", 0, sh * 0.38, sw, "center") love.graphics.printf("LAND GENTLY ON MARKED PADS FOR POINTS", 0, sh * 0.42, sw, "center") -- Press Enter local pulse = 0.3 + math.sin(t * 3) * 0.3 love.graphics.setFont(Fonts.medium) love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse + 0.2) love.graphics.printf("PRESS ENTER TO START", 0, sh * 0.52, sw, "center") -- High scores local allScores = HighScores.getScores() if #allScores > 0 then HighScores.drawTable(sw, sh, p, Fonts) end end function love.load() love.mouse.setVisible(false) love.graphics.setBackgroundColor(0, 0, 0) love.graphics.setLineStyle("smooth") Palette.loadFromSystem() World.resize(love.graphics.getDimensions()) Fonts.init(1) HighScores.init() Sounds.init() Stars.init() World.highScore = HighScores.getHighest() World.state = "title" end function love.resize(w, h) World.resize(w, h) Fonts.init(1) end function love.update(dt) World.ensureScale() if World.state == "title" then return end if World.state == "playing" then World.time = World.time + dt Lander.update(dt) Camera.update(Lander.get(), Terrain, dt) Particles.update(dt) -- Thrust sound local l = Lander.get() if l.thrusting then thrustSoundTimer = thrustSoundTimer + dt if thrustSoundTimer > 0.2 then thrustSoundTimer = 0 Sounds.play("thrust") end -- Thrust particles local fx, fy = l.x, l.y + 8 Particles.spawnThrust(fx, fy, l.angle) else thrustSoundTimer = 0.15 end -- Fuel warning if World.fuel < 150 and World.fuel > 0 then fuelWarnTimer = fuelWarnTimer + dt if fuelWarnTimer > 0.5 then fuelWarnTimer = 0 Sounds.play("fuel_warn") end end -- Check terrain collision checkLanding() -- Game over if fuel depleted and landed/still if World.fuel <= 0 and not l.thrusting then -- Let them coast until they crash or land end elseif World.state == "landed" or World.state == "crashed" then World.stateTimer = World.stateTimer + dt Particles.update(dt) Camera.update(Lander.get(), Terrain, dt) if World.stateTimer >= STATE_PAUSE then if World.fuel <= 0 then World.state = "game_over" World.stateTimer = 0 else nextAttempt() end end elseif World.state == "game_over" then World.stateTimer = World.stateTimer + dt elseif World.state == "high_score_entry" then HighScores.updateEntry(dt) end end function love.draw() love.graphics.clear(0, 0, 0) local p = Palette.get() local sw, sh = World.screenW, World.screenH if World.state == "title" then drawTitleScreen() return end if World.state == "high_score_entry" then love.graphics.setColor(p.bg) love.graphics.rectangle("fill", 0, 0, sw, sh) Stars.draw(sw, sh) HighScores.drawEntry(sw, sh, p, Fonts) return end -- Draw game world love.graphics.setColor(p.bg) love.graphics.rectangle("fill", 0, 0, sw, sh) -- Stars (screen space, behind everything) Stars.draw(sw, sh) -- World space Camera.applyTransform() local minX, minY, maxX, maxY = Camera.getVisibleRect() Terrain.draw(minX, maxX) Lander.draw() Particles.draw() Camera.popTransform() -- HUD (screen space) local l = Lander.get() local altitude = Camera.getAltitude(l.y, Terrain, l.x) HUD.draw(altitude, l.vx, l.vy) -- Landing/crash result text if World.state == "landed" or World.state == "crashed" then love.graphics.setFont(Fonts.medium) local flash = World.state == "crashed" and (math.floor(World.stateTimer * 4) % 2 == 0) or true if flash then if World.state == "crashed" then love.graphics.setColor(p.warning) else love.graphics.setColor(p.bright) end love.graphics.printf(World.landingResult, 0, sh * 0.45, sw, "center") end end -- Game over if World.state == "game_over" then local t = love.timer.getTime() love.graphics.setColor(0, 0, 0, 0.5) love.graphics.rectangle("fill", 0, 0, sw, sh) if World.stateTimer > 0.5 then love.graphics.setFont(Fonts.large) local pulse = 0.7 + math.sin(t * 3) * 0.3 love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse) love.graphics.printf("OUT OF FUEL", 0, sh * 0.3, sw, "center") end if World.stateTimer > 1.5 then love.graphics.setFont(Fonts.medium) love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.7) love.graphics.printf("FINAL SCORE: " .. string.format("%d", World.score), 0, sh * 0.3 + Fonts.large:getHeight() + 8, sw, "center") end if World.stateTimer > 2.5 then local pulse = 0.3 + math.sin(t * 3) * 0.3 love.graphics.setFont(Fonts.small) love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse + 0.2) love.graphics.printf("PRESS ENTER", 0, sh * 0.3 + Fonts.large:getHeight() + Fonts.medium:getHeight() + 24, sw, "center") end end end function love.keypressed(key) if World.state == "title" then if key == "return" then startGame() end if key == "escape" then love.event.quit() end return end if World.state == "high_score_entry" then local result = HighScores.keypressedEntry(key) if result == "done" then World.highScore = HighScores.getHighest() World.state = "title" end return end if World.state == "playing" then if key == "space" then Lander.abort() Sounds.play("abort") end end if World.state == "game_over" then if key == "return" and World.stateTimer > 2.0 then if HighScores.isHighScore(World.score) then World.state = "high_score_entry" HighScores.startEntry(World.score) else World.state = "title" end return end if key == "escape" then World.state = "title" return end end if key == "escape" then love.event.quit() end end