A 3D voxel exploration game where you walk through three phases — Alpha, Beta, Gamma — and the world around you physically changes: blocks become solid in one phase, intangible in another. Built with Three.js + Vite.
- 📖 Design:
GAME_SPEC.md - 🛠️ Roadmap:
PROJECT_REMEDIATION_PLAN.md - 📓 Session handoff:
HANDOFF.md - 🚧 Known issues:
KNOWN_ISSUES.md
| Phase | What | Status |
|---|---|---|
| 0 | Architectural decision (single engine) | ✅ Done |
| 1.1–1.7 | Init, camera, world indexing, chunks, save/load | ✅ Done |
| 2.1–2.8 | Phase shift, per-phase collision/place/break, Phase Lens, Resonance, Phase Anchor, audio cues | ✅ Done |
| 3.1–3.6 | Biomes, Stabilizers, Echoes, Resonance Cores, Phase Glider, Tutorial Zone | ✅ Done |
| 4 | Settings menu, data-driven minimap, full-state save, 30s autosave, code-splitting | ✅ Done |
| 5 | 3-Act Goals, HUD objective, compass, FOV breathing, reduced-motion accessibility | ✅ Done |
| 6 | Focused test suite — unit + Playwright live debug API + smoke static-analysis | ✅ Done |
| 7 | README rewrite, KNOWN_ISSUES, GitHub Actions CI | ✅ Done |
| 8 | Tutorial skip, post-collapse invuln, audio restart, settings reset, compass distance, tutorial re-enter, footstep density | ✅ Done |
| 9 | Firefox pointer-lock + audio fix, edge case hardening, browser-matrix docs | ✅ Done |
| 10 | Gameplay mechanics pass (P0 + P1): energy rebalance, Phase Fuse, collapse=Echo loss, 36-Echo lore, Act 4 Convergence, per-biome signatures, phase erosion, energy danger states | ✅ Done (P0+P1); P2 deferred |
| 10 (cont.) | Echo Hunter panel (§10.10) + New Game+ (§10.14) + Resonance charge-up (§10.13) + Phase shift preview (§10.12) + Wrong-phase Echoes (§10.11) | ✅ Done (P2 fully) |
Full status with per-phase test counts and commit hashes: Progress section in PROJECT_REMEDIATION_PLAN.md.
git clone https://github.com/klampatech/phaseshift.git
cd phaseshift
npm install
npm run dev # http://localhost:5173Open the page, click the blocker overlay (this is the user gesture that unlocks the AudioContext for the audio cues), and start playing.
npm run build # produces dist/ (~570 KB / 149 KB gzipped, 36 KB main entry gz)
npm run preview # serves dist/ on http://localhost:4173Every push to main automatically deploys the latest dist/ to GitHub Pages via the deploy job in .github/workflows/ci.yml. The deploy runs after the build-and-test job passes (tests run first; a red CI blocks the deploy).
Live URL: https://klampatech.github.io/phaseshift/
One-time setup: in the repo's Settings → Pages, set the source to "GitHub Actions" (the workflow uses the official actions/deploy-pages@v4 action, which requires this source).
The Vite base is set to './' in vite.config.js so the same build serves correctly at both / and /phaseshift/ paths.
The 1.0 release + Phase 9 hardening pass was manually verified on the following browser / OS combos. The 8-scenario smoke test (loader → blocker → audio → WASD movement → place/break → T/Q/E → stabilizer → save/load → biome transition) passed on every entry.
| OS | Browser | Version | Date | Reviewer |
|---|---|---|---|---|
| Linux | Chrome | 138+ | 2026-08-10 | Codex (Phase 9) |
| Linux | Firefox | 130+ | 2026-08-10 | Codex (Phase 9) |
| Linux | Chromium (headless) | via Playwright | 2026-08-10 | Code-level + smoke static-analysis |
- Chromium path is exercised end-to-end by
tests/gameplay.spec.js(the 23+ Playwright tests). Every debug hook (__phaseShifter__.*) is asserted on real Chromium via thevite --port 3002web server. - Firefox path is gated on Firefox availability (
npx playwright install --with-deps firefox). The pointer-lock + audio acceptance is exercised bytests/firefox-pointer-lock.spec.js(the deferred-resume + first-input fallback). The audio context state is asserted to berunningafter the first input event. - Safari path is documented but not exercised in CI (no Linux runner for Safari). Safari 16+ is the supported version per the
KNOWN_ISSUES.mdPlatform section.
If you test on a new browser / OS combo, add a row to the table above and a corresponding entry in KNOWN_ISSUES.md if anything breaks.
Mouse: click the blocker to start, then move the mouse to look. Esc releases the pointer (and shows the pause menu).
| Key | Action |
|---|---|
| W / A / S / D | Move |
| Space | Jump |
| Shift + Space | Phase Shift (costs 15 energy; auto-respawns at nearest Stabilizer) |
| Q | Resonance — swap phase presence in a 3×3×3 around you |
| E (hold) | Phase Lens — highlights blocks that differ from the current phase (drains 0.5 energy / sec) |
| F (hold 3s) | Phase Fuse — permanently swap a block's phase presence (costs 30 energy; persists across save/load) |
| Shift + LMB | Place Phase Anchor (yellow outline, 10s lifetime, auto-snaps player on shift) |
| T | Cycle phase (Alpha → Beta → Gamma → Alpha) |
| R | Place Stabilizer (free, in-phase) |
| 1 / 2 / 3 | Jump directly to Alpha / Beta / Gamma |
| RMB | Cycle phase (legacy alias for T) |
| Ctrl | Crouch |
| M | Toggle minimap |
| I | Toggle inventory |
| F5 | Save game (manual; 30s autosave runs by default) |
| F9 | Load game |
| Esc | Pause menu |
The #phase-name indicator (top-left), #biome-info label (top-left under the phase name), #block-hint (top-center), #objective (top, the current Act), and #compass-arrow (top, points at the nearest unfinished marker) are the live HUD readouts.
The three phases — Alpha, Beta, Gamma — each see a different subset of blocks as solid:
- Alpha (cyan): default. Most terrain is solid.
- Beta (magenta): Obsidian + Void are solid; Stone is air. Useful for crossing gaps blocked by Stone.
- Gamma (gold): Stone + Obsidian are solid; Crystal Cavern crystals are air.
You start in Alpha with full energy (100). Each phase shift costs 15 energy. Energy flows in real time: 2.0/sec regen in Alpha, 0.5/sec drain in Beta, 1.0/sec drain in Gamma — a 15-energy shift recovers in 7.5s of Alpha rest. If you run out of energy in Beta or Gamma, you collapse — the world sucks you down, then teleports you to the nearest Stabilizer (if any) with 30 energy restored. Place Stabilizers freely (free, no energy cost) to build safe respawn points. Without one, you respawn at the original spawn with a "No Stabilizer nearby" warning.
Three Acts guide your exploration:
- Act 1 — Find your first Echo (collectible lore crystals in the Ruins).
- Act 2 — Reach the Phase Nexus (the central hub biome).
- Act 3 — Master all phases (collect all 3 Resonance Core amplifiers + place a Stabilizer).
The compass arrow points at the nearest unfinished marker (Echo → Stabilizer → Resonance Core). After all 3 Acts, you're free to explore.
index.html → main.js
├─ src/core/{world, phase, physics, constants}.js
├─ src/render/{renderer, checkpoint, scan, anchor, echo, resonanceCore, resonancePulse, tutorialOverlay}.js
├─ src/input/{controls, placeBlock}.js
├─ src/scan/lens.js (Phase 2.5 — Phase Lens)
├─ src/resonance/{resonate, core}.js (Phase 2.6 + 3.4)
├─ src/anchor/anchor.js (Phase 2.7)
├─ src/audio/{manager, footsteps}.js (Phase 2.8)
├─ src/world/{biome, stabilizer}.js (Phase 3.1 + 3.2)
├─ src/collapse/collapse.js (Phase 3.2)
├─ src/collect/echo.js (Phase 3.3)
├─ src/inventory/inventory.js (Phase 3.3 + 3.4)
├─ src/phase/{lock, glider}.js (Phase 3.5)
├─ src/tutorial/tutorial.js (Phase 3.6)
├─ src/progression/goals.js (Phase 5)
├─ src/ui/{hud, minimap}.js (Phase 4.1 + 4.3)
├─ src/settings/menu.js (Phase 4.2 + 4.5 + 5.5)
├─ src/save/system.js (Phase 1.6 + 2.4 + 2.7 + 4.4)
└─ src/progression/goals.js (Phase 5)
The orphan GameEngine modules under src/core/{game, player, phaseManager, phaseLockManager, particles}.js are kept as reference-only for incremental feature ports; they are not imported by the active path. Each carries a REFERENCE IMPLEMENTATION — DO NOT IMPORT banner at the top.
The pure modules (src/anchor/anchor.js, src/audio/footsteps.js, src/scan/lens.js, src/resonance/resonate.js, src/world/biome.js, src/world/stabilizer.js, src/collapse/collapse.js, src/collect/echo.js, src/tutorial/tutorial.js, src/progression/goals.js) follow the same pattern: no Three.js imports, no globals, no scene access. They're tested in isolation and consumed by main.js + the renderer.
The Vite config (vite.config.js) splits the bundle into 4 chunks:
three(480 KB / 121 KB gz) — Three.js libraryindex(122 KB / 36 KB gz) — main entry, all gameplay systemsgameplay(8 KB / 3 KB gz) — small dynamic import for the gameplay-only pathaudio(6 KB / 2 KB gz) — lazy-loaded audio module (the AudioContext requires a user gesture)
The initial main entry is 36 KB gzipped, well under the 200 KB target.
Pure-Node unit tests cover the pure modules + the World API. 38 files, 1903 checks total (all green — 9 pre-existing test regexes fixed; see commit "Test debt: fix 9 pre-existing headless test failures").
node tests/headless/test-phase12.cjs # 17 checks
node tests/headless/test-phase13.cjs # 7
node tests/headless/test-phase14.cjs # 22
node tests/headless/test-phase15.cjs # 12
node tests/headless/test-phase16.cjs # 21
node tests/headless/test-phase17.cjs # 26
node tests/headless/test-phase22.cjs # 35
node tests/headless/test-phase23.cjs # 51
node tests/headless/test-phase24.cjs # 46
node tests/headless/test-phase25.cjs # 70
node tests/headless/test-phase26.cjs # 71
node tests/headless/test-phase27.cjs # 107
node tests/headless/test-phase28.cjs # 87
node tests/headless/test-phase31.cjs # 95
node tests/headless/test-phase32.cjs # 101 (Stabilizers + Collapse state machine)
node tests/headless/test-phase33.cjs # 131 (Echoes + Inventory)
node tests/headless/test-phase34.cjs # 63 (Resonance Cores + Amplifiers)
node tests/headless/test-phase35.cjs # 95 (Phase Lock + Phase Glider)
node tests/headless/test-phase36.cjs # 59 (Tutorial Zone)
node tests/headless/test-phase4.cjs # 82 (Polish: HUD/Settings/Minimap/Save/Splitting)
node tests/headless/test-phase5.cjs # 58 (Goals + Compass + FOV breathing + a11y)
node tests/headless/test-phase6.cjs # 15 (Focused suite: boot + World.index round-trip + cyclePhase + BDD seed)A static-analysis regression lock (boots Chromium via Playwright, screenshots the page, asserts source contracts):
npm run build
node tests/headless/smoke.cjs # exits 0 on green, 1 on regressionThe smoke test runs ~400 static-analysis regex checks against the source code (covering every Phase's key exports + __phaseShifter__ debug hooks) + the live init_recovered_when_webgl_failed assertion. WebGL fails in the Codex sandbox, so on a real machine all 5 pre-existing WebGL-related checks pass; in the sandbox the §6 acceptance is "0 new failures" (we currently see 5 unrelated pre-existing failures).
npx playwright install # one-time — downloads Chromium
npm test # runs tests/gameplay.spec.js on http://localhost:3002The Playwright config (playwright.config.js) auto-starts vite --port 3002 for the test session. WebGL fails in headless Chromium without a GPU, so the Playwright tests assert at the API surface (the __phaseShifter__ debug hooks), not the visible pixels.
A GitHub Actions workflow (.github/workflows/ci.yml) runs npm run build + npm test on every PR. See the Actions tab for the latest run.
-
The git working tree is at
/home/kyle/Development/phaseshiftbut the git directory is at/tmp/phaseshift-git(a shared bare mirror). UseGIT_DIR=/tmp/phaseshift-git GIT_WORK_TREE=/home/kyle/Development/phaseshift git ...for git operations. -
nodeneedssudo -E -nfor DNS resolution in this sandbox (drop the prefix on a regular machine). -
WebGL fails in headless Chromium (no GPU) — the smoke test asserts
init_recovered_when_webgl_failed: truerather than failing outright. -
The push remote is
https://github.com/klampatech/phaseshift.gitwith the OAuth token read from~/.config/gh/hosts.yml. The canonical commit + push pattern is:export GIT_DIR=/tmp/phaseshift-git GIT_WORK_TREE=/home/kyle/Development/phaseshift cd /home/kyle/Development/phaseshift git add -A git commit -m "..." TOKEN=$(grep oauth_token ~/.config/gh/hosts.yml | tail -1 | awk '{print $2}') git remote set-url origin https://x-access-token:${TOKEN}@github.com/klampatech/phaseshift.git git push origin main git remote set-url origin https://github.com/klampatech/phaseshift.git
MIT — see LICENSE (TBD if absent).