Dungeon venue: grid-first traversable dungeon with cast, loot and encounters

New VenueGenerator 'dungeon' archetype. The floor plan is generated first
on a 44x44 grid (rejection-sampled rooms, Kruskal MST + loop edges,
L-shaped corridors, BFS-verified connectivity); walls are merged runs
built exactly where open meets solid, so the shell has no holes, and
the walk route is a closed travelling tour through every room.

The camera either walks first-person (hero hidden to avoid clipping) or
plants on a per-room watch cell with line-of-sight to the route and
tracks the actor. Enemies (walker/structure/swarm ActorSpecs from the
track's own summary) lurk, stand sentry, dart and drift; loot caches
pause the analytic odometer, which can hold or advance but never retreat.

Includes dungeon-debug.html (2D map + timeline stills), 14 map tests
plus facade camera tests, and the portcullis-holds-for-the-whole-party
regression test.
This commit is contained in:
Dejvino
2026-10-04 22:13:25 +02:00
parent 3df4b3545b
commit c96e810d44
13 changed files with 3309 additions and 20 deletions
+58
View File
@@ -0,0 +1,58 @@
// Shared helpers for the dungeon venue submodules (layout / zones / props /
// hazards). Pure, no THREE, no wall-clock, no Math.random — seeded Rng only,
// all motion analytic f(t, seed) so seek === playback.
//
// A submodule owns ONE portion of the image and the 3D space its objects live
// in. The facade (../dungeon.js) owns the single walk clock and the path;
// every submodule reads (t, plan, pathAt) and never integrates.
import { Rng } from '../../engine/rng.js';
/** Independent seeded stream that never perturbs sibling streams. */
export function forkDungeon(seed, label) {
return new Rng(hashSeed(seed) ^ 0x9e3779b9).fork(String(label));
}
function hashSeed(seed) {
return (seed >>> 0) || 1;
}
/** Analytic wrap: position of a base offset walking at rate dz after time t. */
export function streamWrap(s0, t, dz, span) {
const s = s0 + t * dz;
return s0 + ((((s - s0) % span) + span) % span);
}
/** Smoothstep 0..1. */
export function smooth01(x) {
const t = Math.max(0, Math.min(1, x));
return t * t * (3 - 2 * t);
}
/**
* Crew-style visibility fade: how many of a population with `count` members
* show at a continuous level in [0, ~1.1]. Never toggles — pure f(level,index).
* Same pattern as ride.js crew fade (DESIGN §4: furniture never hard-toggles
* on interpolated state).
*/
export function fadeFor(level, index, count, soft = 0.12) {
const showIndex = index / Math.max(1, count);
return Math.max(0, Math.min(1, (level - showIndex) / soft));
}
/** Segment kinds the layout planner may emit. */
export const SEGMENT_KINDS = [
'hall',
'room',
'forest',
'wallwalk',
];
/** Zone names the zone planner may emit (one per section). */
export const ZONE_NAMES = ['hall', 'room', 'forest', 'wallwalk'];
/** Prop types in the prop library (primitives only, no glTF). */
export const PROP_TYPES = ['rubble', 'furniture', 'trees', 'rampart', 'torch', 'banner', 'stalagmite'];
/** Hazard types (visual only — the walk sidesteps them). */
export const HAZARD_TYPES = ['gate', 'gap', 'patrol'];