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:
28allday 2026-04-18 23:05:45 +01:00
parent 74c15e555a
commit 6757106d90
7 changed files with 544 additions and 99 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
CLAUDE.md
.claude/

84
README.md Normal file
View 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

View file

@ -3,43 +3,87 @@ 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
-- 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 — 750 gives ~125 sec of thrust
local FUEL_RATE = 6 -- fuel per second 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},
-- 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},
}
local LEGS = {
{{-8, 6}, {-14, 16}},
{{-6, 6}, {-14, 16}},
{{8, 6}, {14, 16}},
{{6, 6}, {14, 16}},
-- 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}},
}
local FEET = {
{{-17, 16}, {-11, 16}},
{{11, 16}, {17, 16}},
-- Descent engine bell (hangs below the descent stage)
local ENGINE_BELL = {
{-3, 8}, {-5, 12}, {5, 12}, {3, 8},
}
local WINDOW = {
{{-3, -6}, {3, -6}},
{{3, -6}, {3, -2}},
{{3, -2}, {-3, -2}},
{{-3, -2}, {-3, -6}},
-- 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, -12}, {0, -18}},
{{-3, -18}, {3, -18}},
{{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 = {}
@ -69,8 +113,8 @@ 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
-- Gravity (difficulty-driven, always pulls down)
lander.vy = lander.vy + World.gravity * dt
-- Rotation
if love.keyboard.isDown("left", "a") then
@ -99,8 +143,14 @@ function Lander.update(dt)
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))
-- 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
@ -120,22 +170,18 @@ function Lander.abort()
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)
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
@ -151,6 +197,24 @@ function Lander.land()
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
@ -158,72 +222,94 @@ function Lander.draw()
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
-- 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
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)
drawSegment(leg, a, cx, cy)
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)
-- Surface-contact probes
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)
for _, probe in ipairs(PROBES) do
drawSegment(probe, a, cx, cy)
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.setLineWidth(1)
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)
drawSegment(seg, a, cx, cy)
end
-- Thrust flame (fully transformed)
-- 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 = 10 + math.random() * 15
local flameSpread = 3 + math.random() * 3
local flameLen = 12 + math.random() * 18
local flameSpread = 2 + math.random() * 2
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)
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 = 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)
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

View file

@ -12,6 +12,9 @@ local World = {
stateTimer = 0,
landingResult = "",
landingPoints = 0,
gravity = 12,
startFuel = 750,
difficultyName = "PILOT",
}
function World.resize(w, h)

33
icon.svg Normal file
View 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
View 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
View file

@ -14,10 +14,25 @@ local thrustSoundTimer = 0
local fuelWarnTimer = 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()
applyDifficulty(DIFFICULTIES[difficultyIndex])
World.state = "playing"
World.score = 0
World.fuel = 750
World.fuel = World.startFuel
World.time = 0
World.stateTimer = 0
@ -49,30 +64,47 @@ local function checkLanding()
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
if pad and vspeed < 8 and hspeed < 15 and tilt < math.rad(5) then
-- A perfect landing — tightest tolerances
local pts = 50 * pad.mult
World.addScore(pts)
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.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
-- Rough but survivable on-pad landing
local pts = 10 * pad.mult
World.addScore(pts)
World.landingResult = "HARD LANDING " .. pts .. " PTS"
World.landingResult = "ROUGH LANDING " .. pts .. " PTS"
World.landingPoints = pts
World.state = "landed"
Lander.land()
Sounds.play("land_hard")
else
-- Crash
elseif (not pad) and vspeed < 20 and hspeed < 40 and tilt < math.rad(20) then
-- Missed the pad but touched down intact
World.addScore(5)
World.landingResult = "CRASH! 5 PTS"
World.landingResult = "YOU MISSED THE LANDING AREA 5 PTS"
World.landingPoints = 5
World.state = "landed"
Lander.land()
Sounds.play("land_hard")
else
-- Destroyed
World.landingResult = "CRAFT DESTROYED"
World.landingPoints = 0
World.state = "crashed"
Particles.spawnCrash(l.x, l.y, l.vx, l.vy)
Lander.die()
@ -111,7 +143,7 @@ local function drawTitleScreen()
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")
love.graphics.printf("PRESS ENTER TO SELECT MISSION", 0, sh * 0.52, sw, "center")
-- High scores
local allScores = HighScores.getScores()
@ -120,6 +152,51 @@ local function drawTitleScreen()
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()
love.mouse.setVisible(false)
love.graphics.setBackgroundColor(0, 0, 0)
@ -142,7 +219,7 @@ end
function love.update(dt)
World.ensureScale()
if World.state == "title" then
if World.state == "title" or World.state == "difficulty_select" then
return
end
@ -160,10 +237,10 @@ function love.update(dt)
thrustSoundTimer = 0
Sounds.play("thrust")
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 fx = l.x - sa * 8
local fy = l.y + ca * 8
local fx = l.x - sa * 12
local fy = l.y + ca * 12
Particles.spawnThrust(fx, fy, l.angle)
else
thrustSoundTimer = 0.15
@ -213,6 +290,11 @@ function love.draw()
return
end
if World.state == "difficulty_select" then
drawDifficultyScreen()
return
end
if World.state == "high_score_entry" then
love.graphics.setColor(p.bg)
love.graphics.rectangle("fill", 0, 0, sw, sh)
@ -290,11 +372,24 @@ end
function love.keypressed(key)
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
return
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
local result = HighScores.keypressedEntry(key)
if result == "done" then