The Last Light is a small pixel-art top-down survival shooter about holding a failing floodlit outpost against an endless horde. It uses a deliberately limited asset set and leans on dynamic lighting, projected shadows, procedural ambience, reactive effects, and escalating encounter design to create spectacle.
Play at thelastlight.alexheffernan.dev.
The game is open source, self-hostable, and deployed as a multi-architecture Docker image on a Raspberry Pi.
The current release is a complete score-attack survival game. Runs are intentionally short: learn the outpost, exploit its barrels and floodlights, use supplies and aerial flares, and survive long enough to place on the global leaderboard.
- Mouse-driven top-down shooting with responsive recoil, impacts, casings, blood, and explosions
- Dynamic darkness, directional floodlights, aerial crimson flares, occlusion, and projected shadows
- Four infected archetypes with distinct silhouettes, movement, health, and audio behavior
- A wave director that escalates pressure and introduces threats over time
- Generator-fabricated flares and delayed supply drops containing health, adrenaline, or flare charges
- Procedural monster vocals and combat audio with a rotating original soundtrack
- Persistent global deployment count, leaderboard, and repository-driven changelog
- Development-only collision visualization that is excluded from production builds
- Phaser 3 for gameplay and rendering
- TypeScript and Vite for the browser client
- Node.js for static hosting and the small persistence API
- JSON file persistence with atomic writes
- Docker, GHCR, and GitHub Actions for ARM64/AMD64 deployment
The browser owns the single-player simulation. The server only serves the built client and stores aggregate deployments and submitted score records; there is no multiplayer session or authoritative game simulation.
npm install
npm run devThe Vite client runs locally with graceful offline fallbacks for server-backed menu data. For the complete production stack:
npm run build
DATA_DIR=./data npm startOpen http://localhost:3000.
- WASD / arrow keys — move
- Mouse — aim
- Left mouse — fire
- F — fire an available aerial flare
- E — open a landed supply cache
- P / Escape — pause
- R / click after defeat — redeploy
During npm run dev only, press F2 to toggle collision visualization. The renderer and key binding are excluded from production builds.
npm run typecheck
npm run build
npm startThe client is emitted to dist/client, and the Node server is emitted to dist/server.
docker build --build-arg BUILD_TIMESTAMP="$(date +%s000)" -t the-last-light:latest .
THE_LAST_LIGHT_IMAGE=the-last-light:latest docker compose up -dThe Compose service binds to 127.0.0.1:3011 by default and persists data in the the-last-light-data volume. Override the host port with HOST_PORT.
Useful commands:
docker compose logs -f the-last-light
docker compose pull
docker compose up -d
docker compose down| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
Internal HTTP server port |
DATA_DIR |
/data |
Persistent score and deployment storage |
CHANGELOG_REPOSITORY |
AlexanderHeffernan/TheLastLight |
Repository queried for recent changes |
GITHUB_TOKEN |
unset | Optional token for higher GitHub API limits |
BUILD_TIMESTAMP |
startup time | Millisecond build time shown on the menu |
LAST_UPDATE |
unset | Explicit ISO update time override |
See .env.example for a local template.
npm run dev— run the Vite development clientnpm run typecheck— check browser and server TypeScriptnpm run build:changelog— generate the checked build fallback from GitHubnpm run build:client— build the Phaser clientnpm run build:server— compile the Node servernpm run build— run all checks and production buildsnpm start— serve the production build and API
src/scenes/— Phaser scene lifecycle and combat coordinationsrc/systems/— lighting, audio, supplies, flares, and wave directionsrc/ui/— landing screen, modal, and leaderboard behaviorsrc/assets/— runtime-ready game assets and typed load manifestserver/— static server, leaderboard persistence, and changelog APIpublic/menu/— landing-screen hero and title artworkart-source/— editable deterministic pixel-art pipeline sourcesscripts/— build-time metadata generation
- Gameplay is a fixed-resolution 960×540 Phaser scene scaled to the available desktop viewport.
- Darkness and light occlusion use cached render textures and bounded shadow-caster updates to preserve performance.
- A deployment is counted when a gameplay run starts, including redeployments.
- Leaderboard records rank by eliminations, then survival time, and retain one best run per anonymous browser profile and callsign pair. A long-lived first-party cookie identifies the profile; callsigns remain unique, and legacy records are claimed by the first matching profile to deploy. The public single-player client submits these records, so the leaderboard is intended for friendly competition rather than cheat-proof verification.
- Persistent data is written through a serialized queue to a temporary file and atomically renamed.
- GitHub Actions publishes both ARM64 and AMD64 images to GHCR on pushes to
main.
Contributions and issue reports are welcome. Before opening a pull request, run:
npm run typecheck
npm run build- Alexander Heffernan — creator, developer, game designer, and pixel-art pipeline direction
- Galen Green — Mobile Developer
- Original music generated with Suno
- Built with Phaser and open-source web tooling
The Last Light is not affiliated with or endorsed by Suno.
The source code and documentation are available under the MIT License © 2026 Alexander Heffernan. The music in src/assets/audio is excluded from the MIT License; see Asset Licensing. The repository as a whole is therefore not offered under a single license. Bundled dependency notices are included in Third-Party Notices.