Can a wolf see you in Lullwood, our free browser horror game? Yes, if you are inside its sight radius and no cover box sits on the straight line between you. We decide that in three.js without the Raycaster: a distance check, then a walk along the line through a spatial hash, with one slab test per box. Here is the whole thing, read from the source at commit b03da13.
Check the number before the geometry
canSee in lib/game/cover.ts (L741-771) runs two tests in order. If the distance is at or beyond the predator's effective sight radius, it returns false and never touches the map. Only a player inside the radius gets the line-of-sight test.
The radius is detect * (1 - stillness * 0.82) * detectMul (L734-737). A wolf's base detect is 42 units. The stillness term is state.hidden ? min(1, hideTime / STILL_RAMP) : 0 (L735), so it is zero unless the player is in the hide stance. The engine only sets hidden in enterHide (engine/forest-engine.js L4405-4406), reached when the player presses H with a bramble within 2.2 units of its edge, and any movement key breaks it (L8374). A player who just stops in the open keeps the full 42; the source comment says standing still in the open next to a predator still gets you caught (L711-712). A player hidden within 2.2 of a bramble's edge but outside its footprint ramps the cut in over 1.2 seconds, and 42 becomes about 7.6 before the caller's multipliers (difficulty, mist veil, fog tide, time of day and others). A hidden player inside the footprint skips this arithmetic entirely, because an earlier branch (L766-768) returns first. Cover and the hide stance are separate dials: one removes the line, the other shrinks the radius.
Walk the line in half-cell steps
hasLOS (L568-617) steps along the segment every half cell. Our spatial-hash cell is 8 units, so every 4 units. Each sample point maps to a cell, repeats are skipped, and only the boxes filed under that cell are tested. Each box has a centre, half-width, half-depth, kind and rotation. No height, so sight is flat on the ground plane.
For each box, rotate the segment into the box's own frame (L588-592) and run a slab test: clip the segment against the x range, then the z range, and it hits if a stretch survives both. First hit returns false.
The rotation is the part that bit us. A comment in the file (L224-228) says the same rotated-box sign bug shipped twice, in two functions sharing one rotation convention, because neither could be called from a test. We lifted the geometry out of the engine file so it could be.
Where a raycaster would have gone
We have no use of Raycaster anywhere in our engine, library or app code (git grep -i raycaster). The cover is drawn as instanced meshes, but sight reads plain boxes in a Map, so it never touches a scene mesh. We have not benchmarked the two, so this is not a speed claim.
One exemption and one limit
A segment that starts or ends inside a box reads as blocked from every direction. Logs and brambles are walkable, so a wolf or a player can stand in one. hasLOS skips a walkable box when either endpoint is inside it (L606-610), which also means the hiding player's own bramble needs an explicit rule to stay protective (L757-768).
The limit: a box is filed under the one cell its centre is in (buildCoverGrid, engine/forest-engine.js L997-1002), and hasLOS reads only the cells its samples land in. Collision (coverBlockedR, cover.ts L431) reads the surrounding 3 by 3 block. So a long box centred just across a cell line can overhang into a cell the line samples but never reads. We ran the real function on a synthetic case: a box 4.8 long centred at x = 9.5, and a line from (6, -6) to (7.9, 6) that crosses its overhang at about x = 7.27. It returned true, so the wolf sees through the log. Centre the same box at x = 6.5 and it returns false. That is a probe, not a rate, and we do not know how often a real forest produces it.
The map does not wrap yet
Every position comparison goes through wrapDelta and wrapCoord, so the same code works on a torus. The engine passes an infinite span while wrapEnabled is false in engine/tuning.js (L84), so in the game today the map does not wrap and these are plain subtractions. We wrote the seam math first so that turning wrapping on would not mean hunting for every raw subtraction.
The full version, with every line number, is the Lullwood devlog post this one is cross-posted from. For what it means in play, see how to survive wolves in Lullwood, or play Lullwood free in your browser.
Disclosure: we are the studio behind Lullwood. This post was drafted by an AI agent, checked by a second agent against our written brand rules, and published by the studio.
Top comments (0)