OMA-LANDER: LM silhouette, difficulty select, 5-tier landings, packaging
- Redraw the lander as an Apollo LM: wide octagonal descent stage with panel detail and engine bell, narrower angular ascent stage with docking tunnel, RCS quads, triangular cockpit windows and rendezvous antenna, A-frame legs splaying to saucer foot pads with surface probes. - Add mission select (Cadet / Pilot / Commander / Astronaut); gravity and starting fuel come from the chosen mission via World.gravity and World.startFuel. - Expand landing grading to five tiers with authentic-style messages: A PERFECT LANDING / GOOD LANDING / ROUGH LANDING / YOU MISSED THE LANDING AREA / CRAFT DESTROYED. Scoring and fuel bonuses tiered. - Zero horizontal velocity against the world-edge wall instead of leaving momentum dangling. - Package for release: README with user guide, install.sh matching the OMA-* series, icon.svg of the LM, .gitignore excluding CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
74c15e555a
commit
6757106d90
7 changed files with 544 additions and 99 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
CLAUDE.md
|
||||||
|
.claude/
|
||||||
84
README.md
Normal file
84
README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# OMA-LANDER
|
||||||
|
|
||||||
|
A faithful Lunar Lander arcade recreation for Omarchy Linux, inspired by the 1979 Atari vector cabinet. Built with Love2D — pure Lua, vector line graphics, procedural terrain, procedurally generated audio.
|
||||||
|
|
||||||
|
The lander itself is drawn as an Apollo Lunar Module silhouette: wide octagonal descent stage, angular ascent-stage cabin, splayed A-frame legs with saucer foot pads and surface probes, descent engine bell, docking tunnel, rendezvous radar, and triangular cockpit windows.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sL https://git.no-signal.uk/nosignal/oma-lander/raw/branch/master/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
oma-lander-uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Input | Action |
|
||||||
|
|-------|--------|
|
||||||
|
| **Up / W** | Thrust (main engine) |
|
||||||
|
| **Left / A** | Rotate counter-clockwise |
|
||||||
|
| **Right / D** | Rotate clockwise |
|
||||||
|
| **Space** | Abort burn (auto-level, kills horizontal speed, heavy fuel cost) |
|
||||||
|
| **Enter** | Confirm / advance menus |
|
||||||
|
| **Escape** | Back / quit |
|
||||||
|
|
||||||
|
## Gameplay
|
||||||
|
|
||||||
|
Select a mission difficulty, then pilot the LM down through procedurally-generated lunar terrain to a marked landing pad. The camera tracks you and zooms in as you descend — altitude, horizontal and vertical speed, fuel, score and time are on the HUD.
|
||||||
|
|
||||||
|
### Mission difficulties
|
||||||
|
|
||||||
|
| Mission | Gravity | Fuel | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **CADET** | 8 | 1000 | Training — low gravity, extra fuel |
|
||||||
|
| **PILOT** | 12 | 750 | Standard mission |
|
||||||
|
| **COMMANDER** | 16 | 600 | Heavier pull, less fuel |
|
||||||
|
| **ASTRONAUT** | 22 | 500 | Elite — extreme gravity |
|
||||||
|
|
||||||
|
### Landing pads
|
||||||
|
|
||||||
|
Three pads are placed on each generated map, each with a score multiplier. Narrower pads pay more:
|
||||||
|
|
||||||
|
| Label | Width | Multiplier |
|
||||||
|
|---|---|---|
|
||||||
|
| 2X | widest | × 2 |
|
||||||
|
| 3X | medium | × 3 |
|
||||||
|
| 5X | narrowest | × 5 |
|
||||||
|
|
||||||
|
### Landing results
|
||||||
|
|
||||||
|
Your touchdown is graded by vertical speed, horizontal speed and tilt at contact:
|
||||||
|
|
||||||
|
| Result | Required | Score | Fuel bonus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **A PERFECT LANDING** | on pad, v<8, h<15, tilt<5° | 50 × mult | +50 |
|
||||||
|
| **GOOD LANDING** | on pad, v<15, h<30, tilt<15° | 25 × mult | +25 |
|
||||||
|
| **ROUGH LANDING** | on pad, v<25, h<45, tilt<25° | 10 × mult | — |
|
||||||
|
| **YOU MISSED THE LANDING AREA** | off pad, survivable | 5 | — |
|
||||||
|
| **CRAFT DESTROYED** | else | 0 | — |
|
||||||
|
|
||||||
|
Run out of fuel and the mission ends. Top-10 high scores are saved locally; enter three initials on a new high score.
|
||||||
|
|
||||||
|
## Omarchy Integration
|
||||||
|
|
||||||
|
- **Theme colours** auto-detected from your active Omarchy Ghostty theme
|
||||||
|
- **System font** detected from your Waybar config
|
||||||
|
- **Full-screen** via SUPER+F (Hyprland compositor)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Love2D (`sudo pacman -S love`)
|
||||||
|
- Omarchy Linux (or any Arch-based distro)
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Inspired by the 1979 Atari Lunar Lander cabinet. No arcade assets, logos, or trademarked names are reproduced. The LM silhouette is drawn from public reference photographs of the Apollo Lunar Module.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
252
game/lander.lua
252
game/lander.lua
|
|
@ -3,43 +3,87 @@ local Palette = require("rendering.palette")
|
||||||
|
|
||||||
local Lander = {}
|
local Lander = {}
|
||||||
|
|
||||||
-- Physics tuned to match original Lunar Lander feel:
|
-- Physics tuned to match original Lunar Lander feel.
|
||||||
-- Gentle lunar gravity, thrust comfortably overcomes it,
|
-- Gravity is set per difficulty via World.gravity.
|
||||||
-- deliberate rotation, fuel lasts long enough to learn
|
local THRUST_ACCEL = 36 -- about 3x Pilot-difficulty gravity — can hover and climb
|
||||||
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 ROT_SPEED = 1.4 -- ~80 deg/sec — deliberate, not twitchy
|
||||||
local FUEL_RATE = 6 -- fuel per second — 750 gives ~125 sec of thrust
|
local FUEL_RATE = 6 -- fuel per second of thrust
|
||||||
local MAX_SPEED = 120 -- terminal velocity cap
|
local MAX_SPEED = 120 -- terminal velocity cap
|
||||||
|
|
||||||
-- Lander shape (local coords, angle 0 = pointing up, Y+ is down in world)
|
-- Apollo LM-inspired silhouette.
|
||||||
local BODY = {
|
-- Local coords: angle 0 = pointing up, +Y = down.
|
||||||
{-8, -8}, {-4, -12}, {4, -12}, {8, -8},
|
-- Wide, flat descent stage under a narrower, angular ascent stage;
|
||||||
{10, 0}, {8, 6}, {-8, 6}, {-10, 0},
|
-- 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},
|
||||||
}
|
}
|
||||||
|
|
||||||
local LEGS = {
|
-- Panel lines on descent stage (horizontal divider + vertical bay separators)
|
||||||
{{-8, 6}, {-14, 16}},
|
local DESCENT_DETAIL = {
|
||||||
{{-6, 6}, {-14, 16}},
|
{{-14, 5}, {14, 5}},
|
||||||
{{8, 6}, {14, 16}},
|
{{-5, 2}, {-5, 8}},
|
||||||
{{6, 6}, {14, 16}},
|
{{5, 2}, {5, 8}},
|
||||||
}
|
}
|
||||||
|
|
||||||
local FEET = {
|
-- Descent engine bell (hangs below the descent stage)
|
||||||
{{-17, 16}, {-11, 16}},
|
local ENGINE_BELL = {
|
||||||
{{11, 16}, {17, 16}},
|
{-3, 8}, {-5, 12}, {5, 12}, {3, 8},
|
||||||
}
|
}
|
||||||
|
|
||||||
local WINDOW = {
|
-- Ascent stage: narrower, angular cabin
|
||||||
{{-3, -6}, {3, -6}},
|
local ASCENT = {
|
||||||
{{3, -6}, {3, -2}},
|
{-8, 2}, {-8, -3}, {-7, -7}, {-4, -10}, {4, -10}, {7, -7}, {8, -3}, {8, 2},
|
||||||
{{3, -2}, {-3, -2}},
|
|
||||||
{{-3, -2}, {-3, -6}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- 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 = {
|
local ANTENNA = {
|
||||||
{{0, -12}, {0, -18}},
|
{{0, -13}, {0, -17}},
|
||||||
{{-3, -18}, {3, -18}},
|
{{-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 = {}
|
local lander = {}
|
||||||
|
|
@ -69,8 +113,8 @@ end
|
||||||
function Lander.update(dt)
|
function Lander.update(dt)
|
||||||
if not lander.alive or lander.landed then return end
|
if not lander.alive or lander.landed then return end
|
||||||
|
|
||||||
-- Gravity (always pulls down)
|
-- Gravity (difficulty-driven, always pulls down)
|
||||||
lander.vy = lander.vy + GRAVITY * dt
|
lander.vy = lander.vy + World.gravity * dt
|
||||||
|
|
||||||
-- Rotation
|
-- Rotation
|
||||||
if love.keyboard.isDown("left", "a") then
|
if love.keyboard.isDown("left", "a") then
|
||||||
|
|
@ -99,8 +143,14 @@ function Lander.update(dt)
|
||||||
lander.x = lander.x + lander.vx * dt
|
lander.x = lander.x + lander.vx * dt
|
||||||
lander.y = lander.y + lander.vy * dt
|
lander.y = lander.y + lander.vy * dt
|
||||||
|
|
||||||
-- Clamp X to world bounds
|
-- Clamp X to world bounds and kill horizontal momentum against the wall
|
||||||
lander.x = math.max(20, math.min(World.WORLD_W - 20, lander.x))
|
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
|
-- Don't let lander fly off the top
|
||||||
if lander.y < 0 then
|
if lander.y < 0 then
|
||||||
|
|
@ -120,22 +170,18 @@ function Lander.abort()
|
||||||
lander.vy = lander.vy * 0.3
|
lander.vy = lander.vy * 0.3
|
||||||
end
|
end
|
||||||
lander.vy = lander.vy - THRUST_ACCEL * 0.6
|
lander.vy = lander.vy - THRUST_ACCEL * 0.6
|
||||||
-- Kill most horizontal speed
|
|
||||||
lander.vx = lander.vx * 0.3
|
lander.vx = lander.vx * 0.3
|
||||||
-- Heavy fuel cost
|
|
||||||
World.fuel = math.max(0, World.fuel - 60)
|
World.fuel = math.max(0, World.fuel - 60)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Lander.getCollisionPoints()
|
function Lander.getCollisionPoints()
|
||||||
local pts = {}
|
local pts = {}
|
||||||
local footPts = {{-17, 16}, {-11, 16}, {11, 16}, {17, 16}}
|
local colPts = {
|
||||||
for _, fp in ipairs(footPts) do
|
{-22, 16}, {22, 16}, -- foot pads
|
||||||
local wx, wy = transformPoint(fp[1], fp[2], lander.angle, lander.x, lander.y)
|
{-14, 8}, {0, 8}, {14, 8}, -- descent stage bottom (catches hard tilts)
|
||||||
table.insert(pts, {x = wx, y = wy})
|
}
|
||||||
end
|
for _, cp in ipairs(colPts) do
|
||||||
local bodyBottom = {{-8, 6}, {8, 6}, {0, 6}}
|
local wx, wy = transformPoint(cp[1], cp[2], lander.angle, lander.x, lander.y)
|
||||||
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})
|
table.insert(pts, {x = wx, y = wy})
|
||||||
end
|
end
|
||||||
return pts
|
return pts
|
||||||
|
|
@ -151,6 +197,24 @@ function Lander.land()
|
||||||
lander.vy = 0
|
lander.vy = 0
|
||||||
end
|
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()
|
function Lander.draw()
|
||||||
if not lander.alive then return end
|
if not lander.alive then return end
|
||||||
|
|
||||||
|
|
@ -158,72 +222,94 @@ function Lander.draw()
|
||||||
local a = lander.angle
|
local a = lander.angle
|
||||||
local cx, cy = lander.x, lander.y
|
local cx, cy = lander.x, lander.y
|
||||||
|
|
||||||
-- Body outline
|
|
||||||
love.graphics.setColor(p.lander)
|
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
|
-- 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)
|
love.graphics.setLineWidth(1.5)
|
||||||
for _, leg in ipairs(LEGS) do
|
for _, leg in ipairs(LEGS) do
|
||||||
local x1, y1 = transformPoint(leg[1][1], leg[1][2], a, cx, cy)
|
drawSegment(leg, a, cx, cy)
|
||||||
local x2, y2 = transformPoint(leg[2][1], leg[2][2], a, cx, cy)
|
|
||||||
love.graphics.line(x1, y1, x2, y2)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Feet
|
-- Surface-contact probes
|
||||||
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)
|
love.graphics.setLineWidth(1)
|
||||||
for _, seg in ipairs(WINDOW) do
|
for _, probe in ipairs(PROBES) do
|
||||||
local x1, y1 = transformPoint(seg[1][1], seg[1][2], a, cx, cy)
|
drawSegment(probe, a, cx, cy)
|
||||||
local x2, y2 = transformPoint(seg[2][1], seg[2][2], a, cx, cy)
|
|
||||||
love.graphics.line(x1, y1, x2, y2)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Antenna
|
-- 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.setColor(p.lander)
|
||||||
|
love.graphics.setLineWidth(1)
|
||||||
for _, seg in ipairs(ANTENNA) do
|
for _, seg in ipairs(ANTENNA) do
|
||||||
local x1, y1 = transformPoint(seg[1][1], seg[1][2], a, cx, cy)
|
drawSegment(seg, a, cx, cy)
|
||||||
local x2, y2 = transformPoint(seg[2][1], seg[2][2], a, cx, cy)
|
|
||||||
love.graphics.line(x1, y1, x2, y2)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Thrust flame (fully transformed)
|
-- Descent-engine thrust flame (exits the engine bell at y=12)
|
||||||
if lander.thrusting then
|
if lander.thrusting then
|
||||||
love.graphics.setColor(p.thrust)
|
love.graphics.setColor(p.thrust)
|
||||||
love.graphics.setLineWidth(2)
|
love.graphics.setLineWidth(2)
|
||||||
local flameLen = 10 + math.random() * 15
|
local flameLen = 12 + math.random() * 18
|
||||||
local flameSpread = 3 + math.random() * 3
|
local flameSpread = 2 + math.random() * 2
|
||||||
|
|
||||||
local fx1, fy1 = transformPoint(-flameSpread, 8, a, cx, cy)
|
local fx1, fy1 = transformPoint(-flameSpread, 12, a, cx, cy)
|
||||||
local fx2, fy2 = transformPoint(flameSpread, 8, a, cx, cy)
|
local fx2, fy2 = transformPoint(flameSpread, 12, a, cx, cy)
|
||||||
local ftx, fty = transformPoint((math.random()-0.5)*3, 8 + flameLen, 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(fx1, fy1, ftx, fty)
|
||||||
love.graphics.line(fx2, fy2, ftx, fty)
|
love.graphics.line(fx2, fy2, ftx, fty)
|
||||||
|
|
||||||
-- Inner bright flame
|
-- Inner bright flame
|
||||||
love.graphics.setColor(p.bright)
|
love.graphics.setColor(p.bright)
|
||||||
local flameLen2 = 5 + math.random() * 8
|
local flameLen2 = 6 + math.random() * 10
|
||||||
local fi1x, fi1y = transformPoint(-1.5, 8, a, cx, cy)
|
local fi1x, fi1y = transformPoint(-1, 12, a, cx, cy)
|
||||||
local fi2x, fi2y = transformPoint(1.5, 8, a, cx, cy)
|
local fi2x, fi2y = transformPoint(1, 12, a, cx, cy)
|
||||||
local ft2x, ft2y = transformPoint((math.random()-0.5)*1.5, 8 + flameLen2, 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(fi1x, fi1y, ft2x, ft2y)
|
||||||
love.graphics.line(fi2x, fi2y, ft2x, ft2y)
|
love.graphics.line(fi2x, fi2y, ft2x, ft2y)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ local World = {
|
||||||
stateTimer = 0,
|
stateTimer = 0,
|
||||||
landingResult = "",
|
landingResult = "",
|
||||||
landingPoints = 0,
|
landingPoints = 0,
|
||||||
|
gravity = 12,
|
||||||
|
startFuel = 750,
|
||||||
|
difficultyName = "PILOT",
|
||||||
}
|
}
|
||||||
|
|
||||||
function World.resize(w, h)
|
function World.resize(w, h)
|
||||||
|
|
|
||||||
33
icon.svg
Normal file
33
icon.svg
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||||
|
<rect width="64" height="64" fill="#000"/>
|
||||||
|
<!-- Apollo LM silhouette -->
|
||||||
|
<g fill="none" stroke="#4ade80" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round">
|
||||||
|
<!-- Rendezvous antenna -->
|
||||||
|
<line x1="32" y1="10" x2="32" y2="14"/>
|
||||||
|
<line x1="29" y1="10" x2="35" y2="10"/>
|
||||||
|
<!-- Docking tunnel -->
|
||||||
|
<polyline points="30,17 30,14 34,14 34,17"/>
|
||||||
|
<!-- Ascent stage (pentagonal cabin) -->
|
||||||
|
<polyline points="26,26 26,21 28,17 36,17 38,21 38,26 26,26"/>
|
||||||
|
<!-- Cockpit windows -->
|
||||||
|
<polyline points="28,23 30,20 30,23 28,23"/>
|
||||||
|
<polyline points="36,23 34,20 34,23 36,23"/>
|
||||||
|
<!-- Descent stage (wide flat base) -->
|
||||||
|
<polyline points="20,26 44,26 44,36 20,36 20,26"/>
|
||||||
|
<!-- Panel detail -->
|
||||||
|
<line x1="20" y1="31" x2="44" y2="31"/>
|
||||||
|
<!-- Descent engine bell -->
|
||||||
|
<polyline points="29,36 30,42 34,42 35,36"/>
|
||||||
|
<!-- Landing legs (A-frames) -->
|
||||||
|
<line x1="20" y1="26" x2="10" y2="48"/>
|
||||||
|
<line x1="20" y1="36" x2="10" y2="48"/>
|
||||||
|
<line x1="44" y1="26" x2="54" y2="48"/>
|
||||||
|
<line x1="44" y1="36" x2="54" y2="48"/>
|
||||||
|
<!-- Foot pads -->
|
||||||
|
<circle cx="10" cy="48" r="2.5"/>
|
||||||
|
<circle cx="54" cy="48" r="2.5"/>
|
||||||
|
<!-- Surface probes -->
|
||||||
|
<line x1="10" y1="50.5" x2="10" y2="54"/>
|
||||||
|
<line x1="54" y1="50.5" x2="54" y2="54"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
142
install.sh
Executable file
142
install.sh
Executable file
|
|
@ -0,0 +1,142 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# OMA-LANDER Installer / Uninstaller
|
||||||
|
# Usage: ./install.sh — install the game
|
||||||
|
# ./install.sh uninstall — remove the game
|
||||||
|
|
||||||
|
GAME_NAME="oma-lander"
|
||||||
|
DISPLAY_NAME="OMA-LANDER"
|
||||||
|
COMMENT="Apollo LM lunar landing arcade with Omarchy theme integration"
|
||||||
|
REPO_URL="https://git.no-signal.uk/nosignal/oma-lander.git"
|
||||||
|
|
||||||
|
INSTALL_DIR="$HOME/.local/share/$GAME_NAME"
|
||||||
|
DESKTOP_FILE="$HOME/.local/share/applications/$GAME_NAME.desktop"
|
||||||
|
ICON_DIR="$HOME/.local/share/icons/hicolor"
|
||||||
|
UNINSTALL_BIN="$HOME/.local/bin/$GAME_NAME-uninstall"
|
||||||
|
|
||||||
|
# ── UNINSTALL ──
|
||||||
|
if [ "${1:-}" = "uninstall" ]; then
|
||||||
|
echo "=== Uninstalling $DISPLAY_NAME ==="
|
||||||
|
|
||||||
|
[ -f "$DESKTOP_FILE" ] && rm "$DESKTOP_FILE" && echo "Removed desktop entry"
|
||||||
|
|
||||||
|
for size in 16 32 48 64 128 256 512; do
|
||||||
|
icon_path="$ICON_DIR/${size}x${size}/apps/$GAME_NAME.png"
|
||||||
|
[ -f "$icon_path" ] && rm "$icon_path"
|
||||||
|
done
|
||||||
|
[ -f "$ICON_DIR/scalable/apps/$GAME_NAME.svg" ] && rm -f "$ICON_DIR/scalable/apps/$GAME_NAME.svg"
|
||||||
|
echo "Removed icons"
|
||||||
|
|
||||||
|
[ -d "$INSTALL_DIR" ] && rm -rf "$INSTALL_DIR" && echo "Removed game files"
|
||||||
|
[ -f "$UNINSTALL_BIN" ] && rm "$UNINSTALL_BIN" && echo "Removed uninstall command"
|
||||||
|
|
||||||
|
command -v gtk-update-icon-cache &>/dev/null && gtk-update-icon-cache -f -t "$ICON_DIR" 2>/dev/null || true
|
||||||
|
command -v omarchy-restart-walker &>/dev/null && omarchy-restart-walker 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "=== $DISPLAY_NAME uninstalled ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── INSTALL ──
|
||||||
|
echo "=== Installing $DISPLAY_NAME ==="
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
DEPS=()
|
||||||
|
command -v love &>/dev/null || DEPS+=(love)
|
||||||
|
command -v git &>/dev/null || DEPS+=(git)
|
||||||
|
command -v rsvg-convert &>/dev/null || DEPS+=(librsvg)
|
||||||
|
|
||||||
|
if [ ${#DEPS[@]} -gt 0 ]; then
|
||||||
|
echo "Installing dependencies: ${DEPS[*]}"
|
||||||
|
if command -v pacman &>/dev/null; then
|
||||||
|
sudo pacman -S --noconfirm "${DEPS[@]}"
|
||||||
|
else
|
||||||
|
echo "Error: missing ${DEPS[*]} and pacman not found."
|
||||||
|
echo "Install them manually and re-run this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clone or update the game
|
||||||
|
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||||
|
echo "Updating existing installation..."
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
git pull --ff-only
|
||||||
|
else
|
||||||
|
[ -d "$INSTALL_DIR" ] && rm -rf "$INSTALL_DIR"
|
||||||
|
echo "Cloning game repository..."
|
||||||
|
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install icon
|
||||||
|
echo "Installing icon..."
|
||||||
|
ICON_SVG="$INSTALL_DIR/icon.svg"
|
||||||
|
|
||||||
|
if command -v rsvg-convert &>/dev/null; then
|
||||||
|
for size in 16 32 48 64 128 256 512; do
|
||||||
|
mkdir -p "$ICON_DIR/${size}x${size}/apps"
|
||||||
|
rsvg-convert -w "$size" -h "$size" "$ICON_SVG" -o "$ICON_DIR/${size}x${size}/apps/$GAME_NAME.png"
|
||||||
|
done
|
||||||
|
elif command -v magick &>/dev/null; then
|
||||||
|
for size in 16 32 48 64 128 256 512; do
|
||||||
|
mkdir -p "$ICON_DIR/${size}x${size}/apps"
|
||||||
|
magick "$ICON_SVG" -resize "${size}x${size}" "$ICON_DIR/${size}x${size}/apps/$GAME_NAME.png"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "No SVG converter found, using SVG icon directly"
|
||||||
|
mkdir -p "$ICON_DIR/scalable/apps"
|
||||||
|
cp "$ICON_SVG" "$ICON_DIR/scalable/apps/$GAME_NAME.svg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create .desktop file
|
||||||
|
echo "Creating desktop entry..."
|
||||||
|
mkdir -p "$(dirname "$DESKTOP_FILE")"
|
||||||
|
cat > "$DESKTOP_FILE" << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=$DISPLAY_NAME
|
||||||
|
Comment=$COMMENT
|
||||||
|
Exec=uwsm app -- love $INSTALL_DIR
|
||||||
|
Icon=$GAME_NAME
|
||||||
|
Terminal=false
|
||||||
|
Categories=Game;ArcadeGame;
|
||||||
|
StartupNotify=true
|
||||||
|
TryExec=love
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Install uninstall command
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
cat > "$UNINSTALL_BIN" << 'UNINSTALL'
|
||||||
|
#!/bin/bash
|
||||||
|
# Uninstall OMA-LANDER
|
||||||
|
SCRIPT_URL="https://git.no-signal.uk/nosignal/oma-lander/raw/branch/master/install.sh"
|
||||||
|
curl -sL "$SCRIPT_URL" | bash -s uninstall 2>/dev/null || bash "$HOME/.local/share/oma-lander/install.sh" uninstall 2>/dev/null || {
|
||||||
|
rm -f "$HOME/.local/share/applications/oma-lander.desktop"
|
||||||
|
rm -rf "$HOME/.local/share/oma-lander"
|
||||||
|
for s in 16 32 48 64 128 256 512; do
|
||||||
|
rm -f "$HOME/.local/share/icons/hicolor/${s}x${s}/apps/oma-lander.png"
|
||||||
|
done
|
||||||
|
rm -f "$HOME/.local/share/icons/hicolor/scalable/apps/oma-lander.svg"
|
||||||
|
rm -f "$HOME/.local/bin/oma-lander-uninstall"
|
||||||
|
command -v omarchy-restart-walker &>/dev/null && omarchy-restart-walker 2>/dev/null
|
||||||
|
echo "OMA-LANDER uninstalled"
|
||||||
|
}
|
||||||
|
UNINSTALL
|
||||||
|
chmod +x "$UNINSTALL_BIN"
|
||||||
|
|
||||||
|
# Update icon cache
|
||||||
|
command -v gtk-update-icon-cache &>/dev/null && gtk-update-icon-cache -f -t "$ICON_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Restart walker
|
||||||
|
if command -v omarchy-restart-walker &>/dev/null; then
|
||||||
|
echo "Refreshing app launcher..."
|
||||||
|
omarchy-restart-walker 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== $DISPLAY_NAME installed ==="
|
||||||
|
echo ""
|
||||||
|
echo " Launch: search '$DISPLAY_NAME' in app launcher or run: love $INSTALL_DIR"
|
||||||
|
echo " Uninstall: $GAME_NAME-uninstall"
|
||||||
|
echo ""
|
||||||
127
main.lua
127
main.lua
|
|
@ -14,10 +14,25 @@ local thrustSoundTimer = 0
|
||||||
local fuelWarnTimer = 0
|
local fuelWarnTimer = 0
|
||||||
local STATE_PAUSE = 3.0
|
local STATE_PAUSE = 3.0
|
||||||
|
|
||||||
|
local DIFFICULTIES = {
|
||||||
|
{name = "CADET", gravity = 8, fuel = 1000, description = "TRAINING - LOW GRAVITY"},
|
||||||
|
{name = "PILOT", gravity = 12, fuel = 750, description = "STANDARD MISSION"},
|
||||||
|
{name = "COMMANDER", gravity = 16, fuel = 600, description = "EXPERIENCED - HEAVIER PULL"},
|
||||||
|
{name = "ASTRONAUT", gravity = 22, fuel = 500, description = "ELITE - EXTREME GRAVITY"},
|
||||||
|
}
|
||||||
|
local difficultyIndex = 2
|
||||||
|
|
||||||
|
local function applyDifficulty(d)
|
||||||
|
World.gravity = d.gravity
|
||||||
|
World.startFuel = d.fuel
|
||||||
|
World.difficultyName = d.name
|
||||||
|
end
|
||||||
|
|
||||||
local function startGame()
|
local function startGame()
|
||||||
|
applyDifficulty(DIFFICULTIES[difficultyIndex])
|
||||||
World.state = "playing"
|
World.state = "playing"
|
||||||
World.score = 0
|
World.score = 0
|
||||||
World.fuel = 750
|
World.fuel = World.startFuel
|
||||||
World.time = 0
|
World.time = 0
|
||||||
World.stateTimer = 0
|
World.stateTimer = 0
|
||||||
|
|
||||||
|
|
@ -49,30 +64,47 @@ local function checkLanding()
|
||||||
local hspeed = math.abs(l.vx)
|
local hspeed = math.abs(l.vx)
|
||||||
local tilt = math.abs(l.angle)
|
local tilt = math.abs(l.angle)
|
||||||
|
|
||||||
if pad and vspeed < 15 and hspeed < 30 and tilt < math.rad(15) then
|
if pad and vspeed < 8 and hspeed < 15 and tilt < math.rad(5) then
|
||||||
-- Good landing
|
-- A perfect landing — tightest tolerances
|
||||||
local pts = 50 * pad.mult
|
local pts = 50 * pad.mult
|
||||||
World.addScore(pts)
|
World.addScore(pts)
|
||||||
World.fuel = World.fuel + 50
|
World.fuel = World.fuel + 50
|
||||||
World.landingResult = "GOOD LANDING! " .. pad.label .. " = " .. pts .. " PTS"
|
World.landingResult = "CONGRATULATIONS — A PERFECT LANDING " .. pts .. " PTS"
|
||||||
|
World.landingPoints = pts
|
||||||
|
World.state = "landed"
|
||||||
|
Lander.land()
|
||||||
|
Sounds.play("land_good")
|
||||||
|
elseif pad and vspeed < 15 and hspeed < 30 and tilt < math.rad(15) then
|
||||||
|
-- Good landing
|
||||||
|
local pts = 25 * pad.mult
|
||||||
|
World.addScore(pts)
|
||||||
|
World.fuel = World.fuel + 25
|
||||||
|
World.landingResult = "GOOD LANDING " .. pts .. " PTS"
|
||||||
World.landingPoints = pts
|
World.landingPoints = pts
|
||||||
World.state = "landed"
|
World.state = "landed"
|
||||||
Lander.land()
|
Lander.land()
|
||||||
Sounds.play("land_good")
|
Sounds.play("land_good")
|
||||||
elseif pad and vspeed < 25 and hspeed < 45 and tilt < math.rad(25) then
|
elseif pad and vspeed < 25 and hspeed < 45 and tilt < math.rad(25) then
|
||||||
-- Hard landing
|
-- Rough but survivable on-pad landing
|
||||||
local pts = 15 * pad.mult
|
local pts = 10 * pad.mult
|
||||||
World.addScore(pts)
|
World.addScore(pts)
|
||||||
World.landingResult = "HARD LANDING " .. pts .. " PTS"
|
World.landingResult = "ROUGH LANDING " .. pts .. " PTS"
|
||||||
World.landingPoints = pts
|
World.landingPoints = pts
|
||||||
World.state = "landed"
|
World.state = "landed"
|
||||||
Lander.land()
|
Lander.land()
|
||||||
Sounds.play("land_hard")
|
Sounds.play("land_hard")
|
||||||
else
|
elseif (not pad) and vspeed < 20 and hspeed < 40 and tilt < math.rad(20) then
|
||||||
-- Crash
|
-- Missed the pad but touched down intact
|
||||||
World.addScore(5)
|
World.addScore(5)
|
||||||
World.landingResult = "CRASH! 5 PTS"
|
World.landingResult = "YOU MISSED THE LANDING AREA 5 PTS"
|
||||||
World.landingPoints = 5
|
World.landingPoints = 5
|
||||||
|
World.state = "landed"
|
||||||
|
Lander.land()
|
||||||
|
Sounds.play("land_hard")
|
||||||
|
else
|
||||||
|
-- Destroyed
|
||||||
|
World.landingResult = "CRAFT DESTROYED"
|
||||||
|
World.landingPoints = 0
|
||||||
World.state = "crashed"
|
World.state = "crashed"
|
||||||
Particles.spawnCrash(l.x, l.y, l.vx, l.vy)
|
Particles.spawnCrash(l.x, l.y, l.vx, l.vy)
|
||||||
Lander.die()
|
Lander.die()
|
||||||
|
|
@ -111,7 +143,7 @@ local function drawTitleScreen()
|
||||||
local pulse = 0.3 + math.sin(t * 3) * 0.3
|
local pulse = 0.3 + math.sin(t * 3) * 0.3
|
||||||
love.graphics.setFont(Fonts.medium)
|
love.graphics.setFont(Fonts.medium)
|
||||||
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse + 0.2)
|
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")
|
love.graphics.printf("PRESS ENTER TO SELECT MISSION", 0, sh * 0.52, sw, "center")
|
||||||
|
|
||||||
-- High scores
|
-- High scores
|
||||||
local allScores = HighScores.getScores()
|
local allScores = HighScores.getScores()
|
||||||
|
|
@ -120,6 +152,51 @@ local function drawTitleScreen()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function drawDifficultyScreen()
|
||||||
|
local p = Palette.get()
|
||||||
|
local sw, sh = World.screenW, World.screenH
|
||||||
|
local t = love.timer.getTime()
|
||||||
|
|
||||||
|
love.graphics.setColor(p.bg)
|
||||||
|
love.graphics.rectangle("fill", 0, 0, sw, sh)
|
||||||
|
Stars.draw(sw, sh)
|
||||||
|
|
||||||
|
love.graphics.setFont(Fonts.large)
|
||||||
|
love.graphics.setColor(p.bright)
|
||||||
|
love.graphics.printf("SELECT MISSION", 0, sh * 0.12, sw, "center")
|
||||||
|
|
||||||
|
love.graphics.setFont(Fonts.medium)
|
||||||
|
local lineH = Fonts.medium:getHeight() + 12
|
||||||
|
local menuTop = sh * 0.30
|
||||||
|
|
||||||
|
for i, d in ipairs(DIFFICULTIES) do
|
||||||
|
local y = menuTop + (i - 1) * lineH
|
||||||
|
local selected = (i == difficultyIndex)
|
||||||
|
if selected then
|
||||||
|
local pulse = 0.7 + math.sin(t * 4) * 0.3
|
||||||
|
love.graphics.setColor(p.bright[1], p.bright[2], p.bright[3], pulse)
|
||||||
|
love.graphics.printf("> " .. d.name .. " <", 0, y, sw, "center")
|
||||||
|
else
|
||||||
|
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.5)
|
||||||
|
love.graphics.printf(d.name, 0, y, sw, "center")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Description of selected mission
|
||||||
|
love.graphics.setFont(Fonts.small)
|
||||||
|
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.7)
|
||||||
|
local d = DIFFICULTIES[difficultyIndex]
|
||||||
|
local descY = menuTop + #DIFFICULTIES * lineH + 20
|
||||||
|
love.graphics.printf(d.description, 0, descY, sw, "center")
|
||||||
|
love.graphics.printf(string.format("GRAVITY %d FUEL %d", d.gravity, d.fuel),
|
||||||
|
0, descY + Fonts.small:getHeight() + 4, sw, "center")
|
||||||
|
|
||||||
|
-- Instructions
|
||||||
|
love.graphics.setColor(p.fg[1], p.fg[2], p.fg[3], 0.4)
|
||||||
|
love.graphics.printf("UP / DOWN: CHANGE MISSION ENTER: BEGIN ESC: BACK",
|
||||||
|
0, sh - Fonts.small:getHeight() - 12, sw, "center")
|
||||||
|
end
|
||||||
|
|
||||||
function love.load()
|
function love.load()
|
||||||
love.mouse.setVisible(false)
|
love.mouse.setVisible(false)
|
||||||
love.graphics.setBackgroundColor(0, 0, 0)
|
love.graphics.setBackgroundColor(0, 0, 0)
|
||||||
|
|
@ -142,7 +219,7 @@ end
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
World.ensureScale()
|
World.ensureScale()
|
||||||
|
|
||||||
if World.state == "title" then
|
if World.state == "title" or World.state == "difficulty_select" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -160,10 +237,10 @@ function love.update(dt)
|
||||||
thrustSoundTimer = 0
|
thrustSoundTimer = 0
|
||||||
Sounds.play("thrust")
|
Sounds.play("thrust")
|
||||||
end
|
end
|
||||||
-- Thrust particles — spawn at nozzle transformed by lander angle
|
-- Thrust particles — spawn at engine-bell exit transformed by lander angle
|
||||||
local sa, ca = math.sin(l.angle), math.cos(l.angle)
|
local sa, ca = math.sin(l.angle), math.cos(l.angle)
|
||||||
local fx = l.x - sa * 8
|
local fx = l.x - sa * 12
|
||||||
local fy = l.y + ca * 8
|
local fy = l.y + ca * 12
|
||||||
Particles.spawnThrust(fx, fy, l.angle)
|
Particles.spawnThrust(fx, fy, l.angle)
|
||||||
else
|
else
|
||||||
thrustSoundTimer = 0.15
|
thrustSoundTimer = 0.15
|
||||||
|
|
@ -213,6 +290,11 @@ function love.draw()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if World.state == "difficulty_select" then
|
||||||
|
drawDifficultyScreen()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if World.state == "high_score_entry" then
|
if World.state == "high_score_entry" then
|
||||||
love.graphics.setColor(p.bg)
|
love.graphics.setColor(p.bg)
|
||||||
love.graphics.rectangle("fill", 0, 0, sw, sh)
|
love.graphics.rectangle("fill", 0, 0, sw, sh)
|
||||||
|
|
@ -290,11 +372,24 @@ end
|
||||||
|
|
||||||
function love.keypressed(key)
|
function love.keypressed(key)
|
||||||
if World.state == "title" then
|
if World.state == "title" then
|
||||||
if key == "return" then startGame() end
|
if key == "return" then World.state = "difficulty_select" end
|
||||||
if key == "escape" then love.event.quit() end
|
if key == "escape" then love.event.quit() end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if World.state == "difficulty_select" then
|
||||||
|
if key == "up" or key == "w" then
|
||||||
|
difficultyIndex = ((difficultyIndex - 2) % #DIFFICULTIES) + 1
|
||||||
|
elseif key == "down" or key == "s" then
|
||||||
|
difficultyIndex = (difficultyIndex % #DIFFICULTIES) + 1
|
||||||
|
elseif key == "return" then
|
||||||
|
startGame()
|
||||||
|
elseif key == "escape" then
|
||||||
|
World.state = "title"
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if World.state == "high_score_entry" then
|
if World.state == "high_score_entry" then
|
||||||
local result = HighScores.keypressedEntry(key)
|
local result = HighScores.keypressedEntry(key)
|
||||||
if result == "done" then
|
if result == "done" then
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue