Build log · Software & apps

Low Tide — a dead beacon, and something that hunts by sound

Software & appsdone1 versionsBlenderthree.js13,644 trisSynthesised audioTouch controls

reflection not written yet · every fact below is generated from this project’s own spec and files

What it is

A first-person stealth game whose world is generated in Blender and exported as a 0.62 MB GLB — 13,644 triangles across pier, boathouse, three breaker cabinets, lattice mast and a creature with named limb pivots. Your only light is a hand crank: winding it costs the noise that the blind hunter follows. All audio synthesised in the browser — no audio files. Verified by an autopilot that walks the game's own noise, hunter and breaker logic to a win and a death.

Version history

Straight from the project’s own card data — what each version added.

V1

Blender-built pier, boathouse, cabin, three breaker cabinets, lattice mast, hunter rig; crank torch, noise model, hunter hunt/search/charge, beacon reveal, win/death cards, synthesised audio, touch controls.

Files on the site

What broke, and what I’d change

Not written yet. This is the part only I can write — what actually went wrong, and what I’d do differently. It lives in builds/reflections/low-tide.md (see _TEMPLATE.md); as soon as that file has words in it, this section fills in on the next build.

Everything else on this page — the spec, the version history, the file counts — is generated from the project’s own build notes and files, so nothing here is guesswork.

Build spec

The project’s own build notes, rendered. Open the raw .md ↗

Show the spec

Low Tide — a harbour at night, a dead navigation lamp, and something that hunts by sound

Live: https://isaac1804.com/portfolio/demos/low-tide/play Source: ~/Desktop/low-tide — index.html, game.js, assets/pier.glb, assets/pier.json, blender/build_pier.py, and qa/ with the headless harnesses. Ask: "can you make me a horror game in blender"


WHAT IT IS

A first-person stealth game set on a harbour pier at night. The navigation lamp at the pier head is out, a boat is waiting outside the harbour mouth, and three breaker cabinets stand between you and the power: boathouse, harbour-master's cabin, pier head. Something in the water is blind and hunts by sound.

The whole world is generated by one Python script inside Blender and exported as a 0.62 MB GLB — 13,644 triangles across 16 objects and 17 materials. The pier, the boathouse, the cabin, the three breaker cabinets, the lattice mast, the crates, barrels, nets, the swamped dinghy and the creature are all geometry from that script, not a purchased asset pack.

Then game.js adds everything Blender cannot: first-person movement, a hand-crank torch with a real battery, a noise model, a hunter that investigates and charges, hold-to-throw breakers, and the ending. All audio is synthesised in the browser from oscillators and filtered noise — wind, water lapping the pilings, footfalls that change with the surface, the ratchet of the crank, the foghorn, the creature's breathing. There is not a single audio file in the project.

THE DESIGN PROBLEM: SEEING COSTS NOISE

The torch is not a switch. It is a hand crank. Hold to wind it and the battery climbs; the ratchet carries down the pier and the thing in the water turns toward the sound. So the two resources are the same resource, and every decision is "can I afford to look?" — which is a more interesting question than "do I have batteries left?"

That is also why the crank is a key you hold rather than a pickup you collect, and why the noise meter is on screen at all times: the game is about managing a number, not about jump scares.

FIVE BUGS THAT MATTERED MORE THAN THE FEATURES

1. THE EXPORTER SPLITS EVERY OBJECT PER MATERIAL, SO EVERY NAME LOOKUP FAILED SILENTLY. A Blender object with two materials exports as <Name>_1 and <Name>_2. The runtime looked up LampHead, Breaker_A, Hunter_Torso exactly, found nothing, and moved on without an error — so the hunter stood unparented at the pier gate for the whole build, the breaker cabinets never changed colour when thrown, and the lamp was never wired to the win condition. Nothing threw. The fix resolves parts by stem and collects every match.

2. THE CREATURE WAS TWO GREY TUBES. Torso and legs were within a few centimetres of the same width, with no neck and no gap at the shoulders, and both meshes shared one material instance — so the pale head material overwrote the body and the whole thing read as a pair of pipes. Fixed with a taper, a neck, an arm gap and flat shading so the silhouette breaks up in torchlight.

3. BLENDER EXPORTS roughnessFactor, NOT METALNESS. The material reader treated the third number in the pbr triple as metalness. Everything in the world came out as either chrome or chalk, depending on which value happened to be where.

4. THE HARNESS WAS TESTING A FIFTH OF WHAT IT THOUGHT. The loop clamps dt at 0.05 s, so headless Chromium at ~5 fps advances one game second per five wall seconds. Every "hold W for 2 s" was holding for 0.4 s of game time — walks went a quarter as far as expected and the breaker-hold never completed. The keyboard holds now run until the game's own clock has advanced. The same bug was in the fps meter, which measured clamped time and therefore reported a healthy frame rate on a renderer that was crawling — so adaptive quality never dropped a resolution and shadows stayed on for machines that could not afford them.

5. THE ENDING CAMERA FACED AWAY FROM THE LAMP. A yaw of π is "look behind you". The win shot showed a ladder and empty sky for several builds while the beacon logic itself was correct.

THE BEACON, WHICH IS THE WHOLE ENDING

Fog in this scene is a shader trick, not air. A spotlight lights surfaces; it never lights the space between you and them, so no amount of spotlight intensity produces a visible beam. The beam is therefore a real, unlit, additive cone that sweeps, with the falloff done in a fragment shader:

  • A CanvasTexture flips Y, so the gradient that was meant to be "bright at the lamp, gone at 60 m" came out inverted and the beam appeared to hang in the sky with its bright end detached from the lantern.
  • The first halo was a canvas radial gradient, which quantises into visible concentric arcs against a near-black sky. It is now computed per pixel with a 1-bit dither.
  • A short, wide "haze" cone seen almost end-on projects as an ellipse with a hard silhouette, which reads as a smoke ring rather than light. It is gone; one narrow shaft reads better than two.
  • The reveal is a scripted camera move, and the ending card is gated on game time, not a setTimeout — at 5 fps a 3.2 s timeout lands before the beam has crossed the sky.

One more, found only by staring at a screenshot: the creature looked like a grey plastic mannequin. The material was not the problem — it is #08090a, nearly black. It was standing inside a pier bulb's 9.5 m point-light radius, and any surface inside one of those blows out to flat grey. The fix was to photograph it somewhere darker, not to touch the material. Worth writing down because the instinct was to keep "fixing" the shading.

VERIFIED BY PLAYING IT, NOT BY LOOKING AT IT

An autopilot drives the real game loop through Playwright and asserts on the game's own state, not on pixels:

  • walking makes noise, and the meter decays back to silence when you stop;
  • the torch drains the battery and the crank refills it, and cranking is measurably louder than walking;
  • a breaker throws through the real hold-to-use path and the throw is the loudest thing in the game;
  • the hunter hears footsteps from 14 m and comes to look, and loses the fix when you go quiet and move away;
  • being caught at arm's length kills you, the death card appears, and respawning keeps the breakers you already threw;
  • the lamp comes back on the third breaker and the win card reports the real elapsed time;
  • adaptive quality drops to low by itself on a slow renderer;
  • and on a 390×844 phone: no horizontal or vertical scroll, the canvas fills the screen, and every touch target is at least 44 px.

Two of those found real bugs. The win card had a hardcoded HARBOUR · 04:02 eyebrow and the death card a literal 03:__ placeholder — both now print the real clock. And the "noise decays" check was passing on a fast machine and failing on a slow one, because it waited in wall-clock milliseconds; it now waits in game seconds like everything else.

HONEST LIMITS

It is a stealth game with one enemy and one level. The hunter has no pathfinding — it moves toward the last loud thing and searches around it, which is enough for a pier that is 52 m long and 5 m wide, and would not be enough for anything larger. There is no save: a run is a run, deaths are counted, breakers stay thrown. The creature is geometry, not animation — it has named limb pivots so the joints rotate where they should, but the gait is procedural. And the beam is a fake: convincing in a still and in motion, but it lights nothing.