A playable, high-fidelity demo of Elden Ring Nightreign's run loop, built in the browser with Three.js and Vite. Everything is procedural — every mesh, material, shader, particle and UI element is generated in code. No models, textures, images, fonts or network requests.
Fan project, built as a rendering/gameplay exercise. Not affiliated with FromSoftware or Bandai Namco.
npm install
npx vite --port 5173 # http://localhost:5173 — click the canvas for pointer lockControls: WASD move · Shift sprint · Space roll (i-frames) · V jump · LMB light · RMB heavy · Q lock-on (touch: tap the right pad, or double-tap an enemy) · E interact/rest · C flask · 1 weapon skill · 2 ultimate · X swap weapon · I inventory · M map · Esc pause. Ironeye's bow and the casters' staffs fire projectiles: light = quick shot, heavy = charged shot / comet (costs FP); sprinting is free outside combat. Weapons found in chests and dens go into a six-slot inventory (held at once when they out-damage what you carry, stowed otherwise); every weapon type carries its own skill — Lion's Claw, Lunging Strike, Unsheathe, Spinning Slash, War Cry, Shadow Step, Glintstone Arc, Barrage.
The game ships as a native Android app via Capacitor (android/ is the checked-in project). On any touch device — the Android build or a phone browser — a touch control overlay activates automatically: a dynamic virtual stick on the left half, a camera pad on the right half (tap it to toggle lock-on), and labelled buttons for attack/heavy/roll/skill/ultimate, a latching sprint toggle, a contextual interact button, plus map and pause. Button placement adapts per orientation, and on phone-sized screens the HUD and menus scale to fit. The Android shell runs portrait, immersive fullscreen, keep-screen-on; the hardware back button closes the map → pauses → backgrounds the app (never kills a run).
Build the APK (needs JDK 21 + the Android SDK, compileSdk 36 / minSdk 24 — or just open android/ in Android Studio):
npm install
npm run android:apk # vite build + cap sync + gradlew assembleDebug
# → android/app/build/outputs/apk/debug/app-debug.apknpm run android:sync rebuilds the web bundle into the Android project without compiling. node tools/touch-smoke.mjs runs 34 end-to-end checks over the touch input path, in portrait and landscape (dev server on :5173).
Performance: ~270 fps uncapped at 1920×1080 on an M-series GPU (measured with tools/fps-probe.mjs in headed Chromium), against a 60 fps target.
- A three-day expedition — explore → ring shrink → explore → ring shrink → day boss, three times, ending in a Nightlord on night 3, then a results screen.
- The night ring — a translucent "rain of night" wall that closes in on a schedule and damages you outside it.
- Limveld — a seeded 1200×1200 m heightmap with a lake, churches, a fort, ruins, a catacomb, enemy camps, and Sites of Grace.
- Souls combat — stamina, roll i-frames, poise/stagger, hit-stop, lock-on, light/heavy combos, weapon trails, spark impacts; soldiers, knights and wolf packs with telegraphed attacks.
- Sites of Grace — rest, spend runes to level, set your respawn.
- Roundtable Hold hub — Nightfarer select and relic vessels persisted in localStorage.
- Zero console errors;
node tools/smoke.mjsruns 27 end-to-end checks over the whole loop.
Each piece of the demo (vista, character, combat, ring, HUD, grace, architecture, bosses, …) is improved by a builder agent and judged by a separate critic agent with fresh context. The critic sees our capture and a real screenshot from a shipped game side by side with the labels stripped, picks the better frame, and names the single biggest gap. A piece is done only when the critic picks ours blind.
The bar: Ashen (A44 Games) for look and feel, Elden Ring Nightreign for run structure.
| Tool | What it does |
|---|---|
tools/screenshot.mjs |
Headless capture of any pose via window.__game.screenshotPose(); retries across Vite reloads, rejects blank frames |
tools/blindpair.mjs |
Normalises our capture and the reference to identical size/format, shuffles them into A.jpg/B.jpg, hides the key |
tools/gauntlet-round.workflow.js |
One round: builder → capture → blind pair → critic, fanned out per piece |
tools/record-verdict.mjs |
Applies verdicts to progress/state.json |
tools/progress.mjs |
Regenerates the live progress page from that state |
tools/fps-probe.mjs |
Real-GPU fps in headed Chromium |
tools/smoke.mjs |
27 end-to-end checks over the full run loop |
tools/motion.mjs |
Motion review: captures one action (swing, combo, roll, run, enemy attack) from fixed angles with the sim stepped deterministically → labelled contact sheets, onion-skin overlays with the blade-tip path, GIFs, a per-frame trace and pass/fail checks (blade through ground, peak tip speed vs hitbox window, readability, clipping) |
ARCHITECTURE.md is the module contract every builder works against; GAUNTLET.md is the critic/builder protocol; CHANGELOG.md records what each round changed.
Four pieces won their first blind comparison. A confirmation pass then re-judged them with neutral, craft-only wording against fairer reference frames — and all four losses came back. The earlier wins were artifacts of the setup, not the work:
- two bar images were mis-filed (an Ashen ruin interior standing in for a "vista"; a Nightreign frame with no HUD used to judge the HUD),
- one goal said "night" while the reference was shot in daylight, which told the critic which frame to prefer,
- two wins were "leaning", not "sure".
So the honest board is: fps is the only confirmed win; every visual piece is still behind the bar. Four independent critics named the same fundamentals — no contact shadows or AO, flat untextured ground, and light sources that don't actually light characters or terrain. That's a rendering-foundations problem, not a per-piece one, and it's the work in flight when this snapshot was taken.
Recording that honestly is the point of the method: a gauntlet loop only produces quality if the comparison is real.
src/core/ Game loop, input, events, seeded RNG, debug/pose API
src/render/ Palette, atmosphere, post, lights, contact shadows, particles
src/world/ Terrain, Limveld map gen, structures, props, POIs
src/entity/ Humanoid rig + procedural animation, player, camera, enemies, bosses
src/combat/ Hit volumes, damage, FX, weapon trails, arena dressing
src/run/ Expedition state machine, night ring, graces, loot
src/nightfarers/ The eight preset characters
src/hub/ meta/ Roundtable Hold, relics (localStorage)
src/ui/ HUD, menus, map
progress/ Live progress page + per-round verdicts
window.__game exposes screenshotPose, teleport, setTime, spawn, setQuality, startExpedition and fps for tooling.