Skip to content

denisbondare/haldane-4

Repository files navigation

HALDANE-4

A procedural ASCII horror game, vibecoded with Cursor and Claude Code. Open-sourced as an example of how far you can push that workflow on a one-person side project.

Live at https://haldane4.denisbondare.com — best on desktop with headphones.

HALDANE-4 preview


The game

You are Dr. Harlan Webb, sealed inside the HALDANE-4 submersible, piloting into a subglacial cavity beneath the Ross Ice Shelf, Antarctica, 1972. A surface assistant talks you down by radio. Your job: reach the deepest point, collect a sample, and climb back out before life support runs out.

Something is waiting down there.

Everything you see is rendered as ASCII into a fake CRT — caves, lighting, the rover, the creatures, the HUD. The caves, music, SFX, ambience, and horror effects are all procedural. The radio dialogue is recorded and routed through a procedural filter chain that gets sicker the deeper you go.

How it plays

Slow, heavy movement on an inertial 3D world that you view as a 2D cross-section. Rotate the camera to see the cave from different angles — some passages only show up from certain rotations. The lamp is the only thing keeping the dark off you, but the lamp is also what the entity follows.

Input Action
WASD / arrows Thrust on the current view plane
Q / E or right-drag Rotate camera
Mouse Look
Space Toggle the lamp
Shift Hide mode — engine off, lamp off, silent

There's also a mobile build with a floating joystick + HIDE button. It works, but desktop is the intended experience.


Running locally

npm install
npm run dev

Open http://localhost:5173/ and click the screen once to start the audio.

npm run build   # outputs to dist/

Deployment

To push an update:

  1. Commit and push locally:
    git push
  2. SSH into the server and run:
    /var/www/haldane4/deploy.sh

The script pulls latest, reinstalls deps, rebuilds dist/, and nginx serves the new files immediately (no reload needed). Survives reboots — nginx auto-starts and serves from disk.


Codebase tour

Vite + TypeScript, no frameworks, Canvas 2D + Web Audio. The whole thing is ~50 source files under src/, organized by responsibility:

src/
├── main.ts          orchestrator — wires every system together, owns the frame loop
├── engine/          game loop, input, global state machine (menu → briefing → play → ascent → ...)
├── world/           voxel world, 3D simplex noise, streaming chunk generation in a Web Worker
├── render/          ASCII renderer, camera, lighting, CRT post-processing
├── game/            robot physics, umbilical cable, bubbles, creatures, sensors, radio director
├── audio/           music, ambient, SFX, radio playback, horror noise, signal degradation
├── horror/          entity AI, sanity, visual + text corruption, wall engravings
└── ui/              HUD, screens, prompts, typewriter, debug panels

A few parts are interesting to read on their own:

  • main.ts — the whole game in one file at the orchestration level. If you want to understand the architecture, start here.
  • world/generator.ts + world/gen-worker.ts — caves are 3D simplex noise carved into a voxel grid, streamed in cube-by-cube around the rover from a Web Worker so the main thread never stalls.
  • render/renderer.ts — the rendering trick. For every cell of the 80×45 ASCII grid, cast a ray into the 3D voxel world along the camera's look direction and walk it up to 24 steps deep; the first non-empty voxel decides the cell. Brightness comes from ray depth + a separate raycasted lighting pass (render/lighting.ts) that rakes shadows from the rover's lamp across the same grid. The character itself comes from a per-material brightness ramp in render/materials.ts — so the "shading" you see is the renderer picking glyphs . , : - = # as a function of incoming light. The whole thing is a 2D cross-section through a real 3D world; rotating the camera changes which plane gets sliced.
  • horror/engravings.ts — the cursed text on the walls. There's one long concatenated tape of horror phrases (YOU ARE ALONE . THIS IS UNBEARABLE . UNSKIN YOURSELF . ...). When the raycaster hits a CORRUPTED voxel, instead of painting the usual rock glyph, it asks the tape for the character at (screen_x, screen_y + world_y_offset). Adjacent screen cells pull adjacent letters, so the wall literally spells phrases as you slide past. Keying off screen position (not world voxel) is what keeps words reading left-to-right regardless of cave angle — the raycast hits different voxels per column and would scramble the letters otherwise. After the sample is collected, the same tape bleeds onto ordinary rock during the ascent, so walls you passed clean on the way down now whisper on the way up.
  • game/radio-director.ts — a finite state machine over story stages (pre_dive → descending → losing_signal → ... → real_rescue → final). Each stage declares which audio clips to play, which music layers turn on, and the predicate that lets you advance. Adding a story beat is basically adding a row to the table.
  • audio/engine.ts — Web Audio graph with per-bus user gains, a sidechain duck that pulls the music down whenever radio or creatures speak, and a signal degradation chain (highpass/lowpass/waveshaper) wired to entity proximity. The "broken radio" sound is real signal processing, not a sample.
  • horror/entity.ts — the thing in the cave. Tracks proximity, awareness, and threatLevel separately; rises in surges, collapses when you hide. The same struct drives every horror system (visual glitching, text corruption, music intensity, audio degradation).
  • game/creatures.ts + audio/creature-sfx.ts — the wall-clinging things that flee or stalk you. Each creature is a body voxel with extendable strands (which anchor to walls while the body drifts, then retract) and a scatter of eyes that blink on their own timers. They speak: a clicky stridulating "chirp" fires whenever a creature moves, a long low "howl" fires when one's been perched still long enough. Voices are spatialized in camera-local space with a stereo panner, run through a shared underwater lowpass, and routed into the ambient bus so they pick up the cave reverb for free — together they form a real diegetic creature language layered under the music.
  • horror/visual-corruption.ts + horror/text-corruption.ts — overlays that munge the rendered grid: frozen stripes, glyph substitution, blood-red entity injections in the HUD. Hooked to sanity and entity proximity.
  • render/crt.ts — pixel-level CRT pass (scanlines, bloom, chromatic abberation) applied to the canvas after the grid is flushed. Strength scales with 1 - sanity/100.

A couple of design tricks worth calling out:

  • Fog pool sanctuary. When the rover is submerged in the bottom pool, the entity update simply returns early, sanity is pinned to 100, and the audio chain swaps to a muffled bus. One if (inFog) return; in horror/entity.ts keeps the deepest, scariest spot feeling safe.
  • Spawn cave seeded once. The cave you spawn into is carved by every chunk that overlaps a sphere around the spawn point. To avoid seams between chunks, the noise seed for that shape is resolved exactly once at module load — see SPAWN_CAVE_SEEDS in world/generator.ts.
  • Debug panels (ui/design-panel.ts, ui/sound-design-panel.ts, ui/map-debug-panel.ts) let me tune sanity drain, entity behavior, audio mix, and inspect the voxel slice live. Toggle them in ui/design-tunables.ts.

Tech stack

  • Vite + TypeScript, no framework
  • Canvas 2D for the ASCII grid and CRT post-processing
  • Web Audio API for procedural music, SFX, ambient, signal degradation, and the radio filter chain
  • 3D simplex noise for cave generation, streamed via a Web Worker
  • Recorded radio dialogue (public/audio/*.mp3) sequenced by a depth/event-driven stage director

License

CC BY-NC 4.0 — free to fork, study, modify, and remix for non-commercial use, as long as you credit the original. Commercial use requires permission — reach out: @denisbondare.

About

A procedural ASCII horror game about the depths of the Southern Ocean

Topics

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors