/** * Random World Events: five-minute rewarding finds for levels 1-15 (James's sheets, 2026-10-03; backend * design-references random-world-events and random-event-kit, then one full sheet per event under its id). * * How one works (every sheet's loop): an entrance turns up somewhere in the starter area; you go in and have * WORLD_EVENT_DURATION_MS inside (a timed run keyed by the event id, timedRun.ts: logged in or not, you are put back * outside when it runs out); NPCs keep coming; you mine, fish or forage the dense nodes; a short puzzle opens the way * to the cache; going in pays Explorer XP and the cache pays more. * * The shape is the forest dens' (landmarks.ts FOREST_DEN_SITES): every candidate entrance site and every interior is * built by world generation; only WORLD_EVENT_ACTIVE_COUNT entrances show at once, each a different event at one of * its sites. The mouth stays open WORLD_EVENT_JOIN_WINDOW_MS after the first player goes in so friends can follow, * then caves in. When the interior is empty again it resets (puzzle, cache, nodes, NPCs) and another event opens. * * An event is data: floor rooms in its plot of the underground strip, pieces (the way out, the cache, the puzzle's * switches and the barriers they open, signs and dressing), puzzle steps, NPC streams, its loot and XP. The backend * carves and runs it from this (worldEventsMap.ts, worldEvents.ts); the client draws the pieces as Blender models. * Only events with `built` rotate; the others have their sites and plot and wait for their layout (James's order). */ import { type NpcKind } from './constants.js'; import type { WorldEventScaling } from './worldEventScaling.js'; export { EVENT_TOOL_WOODWORK } from './worldEventLayout.js'; /** James's build order (2026-10-03). Cart Ravine and Watch Post Cellar from the first overview are dropped. */ export type StarterEventId = 'copper-prospect' | 'buried-storehouse' | 'reedbed-sinkhole' | 'tidepool-grotto' | 'storm-felled-grove' | 'forgotten-smithy' | 'old-kings-culvert' | 'sunken-wayshrine' | 'goblin-sapper'; /** * The frozen random-spawn dungeons (frozenDungeons.ts, slice C4 of PLAN_1_TO_40.md, 2026-10-12): the same engine, a * rotation of their own in the Frozen north, levels 10-40 by scaling. The Frozen Shore Smuggler * Grotto and Shiverbell Reliquary join this list when they are built (NEXT_STEPS.md, "Frozen random-spawn dungeons"). */ export type FrozenDungeonId = 'frostpine-trapper-hollow' | 'kings-road-ice-culvert' | 'frostwatch-cellar' | 'frozen-shore-grotto' | 'shiverbell-reliquary'; /** The Deep Seam (deepSeam.ts, slice 1 of COVERAGE_1_40.md, 2026-10-14): a scaled random-spawn dungeon led by ore, in a rotation of its own. */ export type DeepSeamId = 'deep-seam'; export declare const DEEP_SEAM_IDS: readonly DeepSeamId[]; /** The Hollow Delve (hollowDelve.ts, slice 12 of COVERAGE_1_40.md, 2026-10-17): a scaled random-spawn dungeon for Combat 21-30 with an ordered-lever puzzle, in a rotation of its own. */ export type HollowDelveId = 'hollow-delve'; export declare const HOLLOW_DELVE_IDS: readonly HollowDelveId[]; export type WorldEventId = StarterEventId | FrozenDungeonId | DeepSeamId | HollowDelveId; export declare const STARTER_EVENT_IDS: readonly StarterEventId[]; /** The frozen dungeons in the suggested build order (NEXT_STEPS: Trapper Hollow, then the Culvert). */ export declare const FROZEN_DUNGEON_IDS: readonly FrozenDungeonId[]; export declare const WORLD_EVENT_IDS: readonly WorldEventId[]; /** * Which rotation an event is in: the nine starter events turn up round the starter area, the frozen dungeons in the * snow. Each region keeps its own count of mouths open at once (WORLD_EVENT_ACTIVE_BY_REGION), so a frozen dungeon * never takes a starter slot. */ export type WorldEventRegion = 'starter' | 'frozen' | 'deep' | 'hollow'; /** Time inside, wall clock (every sheet: "five minutes"). */ export declare const WORLD_EVENT_DURATION_MS: number; /** After the first player goes in, how long the mouth stays open for others to follow into the same run. */ export declare const WORLD_EVENT_JOIN_WINDOW_MS = 30000; /** Entrances showing across the starter area at once (never two of the same event). */ export declare const WORLD_EVENT_ACTIVE_COUNT = 3; /** * Frozen dungeon mouths showing in the snow at once. PLACEHOLDER for the balance pass: two, so with the two built * dungeons both are always somewhere; once all five are built two of five show and the rest wait their turn. */ export declare const FROZEN_DUNGEON_ACTIVE_COUNT = 2; export declare const WORLD_EVENT_ACTIVE_BY_REGION: Readonly>; /** How long after one resets before the next entrance turns up, somewhere else. */ export declare const WORLD_EVENT_REOPEN_MS: { readonly minMs: 45000; readonly maxMs: 120000; }; /** Kinds. Each is a Blender model prop (isModelPropKind): its style is `-`, its footprint the model's. */ export declare const EVENT_ENTRANCE_KIND = "evententrance"; /** A way back out (a ladder, a crawlway, stairs). */ export declare const EVENT_EXIT_KIND = "eventexit"; /** The cache: each character in the run may open it once. */ export declare const EVENT_CHEST_KIND = "eventchest"; /** A puzzle piece you work: a lever, a winch, a wheel, a latch, a brace, bellows, a symbol. */ export declare const EVENT_SWITCH_KIND = "eventswitch"; /** Something in the way until the puzzle opens it: a fallen beam, a gate, a hidden door. */ export declare const EVENT_BARRIER_KIND = "eventbarrier"; /** The event signs (the clock, the fight, the loot, the way out): a click reads them. */ export declare const EVENT_SIGN_KIND = "eventsign"; /** A quick gather node (grain sacks, fruit shelves, a reed bed): a click gives something and some XP, then it refills. */ export declare const EVENT_NODE_KIND = "eventnode"; export declare const WORLD_EVENT_KINDS: readonly string[]; export declare function isWorldEventKind(kind: string): boolean; export type WorldEventFacing = 'n' | 'e' | 's' | 'w'; /** A tile and the way a prop on it faces. */ export type WorldEventSpot = { x: number; z: number; facing: WorldEventFacing; }; /** One place an event's entrance can turn up: the footprint's centre tile (world space). */ export type WorldEventSite = WorldEventSpot & { id: string; }; /** Inclusive tile rectangle. */ export type TileRect = { minX: number; maxX: number; minZ: number; maxZ: number; }; export type WorldEventPieceRole = 'exit' | 'cache' | 'switch' | 'barrier' | 'sign' | 'decor' | 'node' | 'passage' | 'station' | 'snare'; /** A station piece's kind: the game's own smelting and smithing stations (blacksmithing.ts), not an event kind. */ export type WorldEventStation = 'furnace' | 'anvil'; export declare const WORLD_EVENT_ROLE_KIND: Readonly, string>>; /** * One piece of an interior. Its entity id is `ev--` (worldEventPieceEntityId); decor is a plain * `dreadmodel`, the rest are the event kinds above, so a click reaches the backend. */ export type WorldEventPiece = WorldEventSpot & { id: string; role: WorldEventPieceRole; /** The Blender model (CAPITAL_BUILDING_MODELS id) at the start of a run. */ model: string; /** Its model once its step is done (a pulled lever, a beam pushed aside, an emptied cache). */ doneModel?: string; /** What the client calls it, and the left-click verb ("Pull", "Push aside", "Climb out"). */ name: string; verb: string; /** What a click on a sign says, or a decor piece's description. */ text?: string; /** Not there until this piece's step is done (a latch under a crate): built, hidden at boot, shown when it opens. */ hiddenUntil?: string; /** A gather node (role 'node'): what a click gives, the skill it trains and how long until it fills again. */ node?: WorldEventNode; /** * Opened, this piece is a deck over water (the Sinkhole's stepping stones): its tile is water, which nobody stands * on, until the piece is done; then it can be walked, and the client lifts whoever stands there onto the stones * (EVENT_DECK_LIFT). */ deck?: boolean; /** * A passage (role 'passage'): where a click takes you inside the same interior (the Storm-Felled Grove's clearing * down into the Root Hollow under the root plate, and the roots back up). With a step, it opens first like a switch. */ to?: { x: number; z: number; }; /** * Work it takes to finish a switch (the grove's marked trunks): the tool and the skill level a click needs, and what * finishing it gives, with the skill's XP. Paid once, when its step is done. */ work?: WorldEventWork; /** * A station (role 'station'): the Forgotten Smithy's furnace and anvil are the game's real smelting and smithing * stations, placed as that kind (`furnace`, `anvil`) in the event's Blender model; a click opens the blacksmith * dialog as anywhere else, and the event never changes them (no done model, nothing to reset). */ station?: WorldEventStation; /** * A snare (role 'snare', Frostpine Trapper Hollow's sheet: "chop or clear ... snare lines"): armed it is a walkable * tile with a wire loop on it; a player who steps on it is caught (held, hurt, and the creatures nearby hear it); a * click disarms it first for its Foraging XP. Sprung or disarmed it takes its done model until the reset. */ snare?: WorldEventSnare; }; /** * A trapper's snare: what disarming it takes and gives. What a caught player suffers (a share of their own health, so * it bites the same at every level, and a hold) is SNARE_TUNING in worldEventScaling.ts, a placeholder for balance. */ export type WorldEventSnare = { /** Foraging level to disarm it. */ level: number; /** Foraging XP for a disarm. */ xp: number; /** What a careful disarm gives back (the wire, sometimes). */ give?: readonly { kind: string; min: number; max: number; }[]; }; /** A switch worked like a node: what it takes, what finishing it gives. */ export type WorldEventWork = { skill: 'foraging' | 'fishing' | 'woodwork' | 'mining'; level: number; xp: number; give: readonly { kind: string; min: number; max: number; }[]; tool?: { kind: string; name: string; }; }; /** A quick gather node's yield: each line rolls its own quantity; the done model shows it emptied until it refills. */ export type WorldEventNode = { give: readonly { kind: string; min: number; max: number; }[]; skill: 'foraging' | 'fishing' | 'woodwork' | 'mining'; xp: number; /** The skill level needed, if any. */ level?: number; respawnMs: number; /** * Pond scoops: each go rolls this many catches on the Wooden Fishing Net's table (pondFishing.ts) by the player's * Fishing level, each paying its own Fishing XP ("dense" catch points, the Sinkhole's sheet). */ pond?: number; /** A tool the bag (or the hand) must hold to work it, and how a refusal names it ("a Wooden Fishing Net"). */ tool?: { kind: string; name: string; }; }; /** * A puzzle step: a click on `piece` once every piece in `needs` is done. The piece takes its done model; `opens` * names other pieces that take theirs too (a winch lowering a walkway). A barrier's done model is walkable * (CAPITAL_BUILDING_MODELS `walkable`), so opening one opens the way. */ export type WorldEventStep = { piece: string; needs?: readonly string[]; /** Everyone inside hears this when it is done. */ text: string; /** A click before its needs are met: the sheet's "inspect" line, which says where to go. */ notYet: string; opens?: readonly string[]; /** * The wrong order (the Old King's Culvert: the downstream gate before the upstream wheel): a click before its needs * are met is an attempt that fails, not a look. `text` is said as a warning with the refusal sound instead of * `notYet`, and the noise sends `spawn` extra NPCs from the event's first stream (two rats off the ledge). */ fail?: { text: string; spawn?: number; }; }; /** * What an ordered puzzle says (the Hollow Delve's levers; omitted, the Wayshrine's chapel-symbol lines are used). `{order}` is this run's * order as the pieces' names, `{piece}` the piece just worked (lower case). */ export type WorldEventSymbolText = { /** The plaque: `{order}` is filled in. */ relief: string; /** A right one, not the last. */ lit: string; /** A wrong one: everything goes back and `wrongSpawn` more come. */ wrong: string; /** The last right one: the gate opens. */ complete: string; /** A click once the gate is open. */ open: string; /** The cache while the gate is shut. */ sealed: string; }; /** A level range (inclusive). */ export type LevelRange = readonly [number, number]; /** * NPCs that keep coming while anyone is inside: `firstWave` at once, then one more every `everyMs` up to `cap` (plus * `perExtraPlayer` for each player past the first), from the nests. Levels roll inside `levels`, leaning towards the * party's combat level. One spawn in `leader.oneIn` is the leader instead (at most `leader.max` alive). */ export type WorldEventStream = { kind: NpcKind; name: string; levels: LevelRange; nests: readonly { x: number; z: number; }[]; cap: number; perExtraPlayer: number; everyMs: number; firstWave: number; /** Encounter-specific movement, health and visible size; omitted for ordinary streams. */ stationary?: boolean; healthMultiplier?: number; scale?: number; leashRadius?: number; flank?: boolean; /** A newcomer below this combat level is left alone unless they strike first (the forest dens' rule). */ aggroCombat: number; aggroText: string; leader?: { kind: NpcKind; name: string; levels: LevelRange; oneIn: number; max: number; aggroText: string; }; /** * A scaled event (the def's `scaling`): this stream's level relative to the scale level (worldEventScaling.ts * scaledEnemyLevel), clamped to `levels`. Unscaled events ignore it and lean into `levels` as before. */ levelOffset?: number; /** How near a player must come before it goes for them (default: STREAM_AGGRO_RADIUS, or 1 when stationary). */ aggroRadius?: number; /** * A caller (the King's Road Ice Culvert's Lookout, "calls nearby rats"): while one of this stream is alive and has a * target, every `everyMs` it sends one more of stream `stream` from that stream's nests, up to `max` of them alive * above that stream's own cap. Everyone inside hears `text`. Kill the caller and the calling stops. */ calls?: { stream: number; everyMs: number; max: number; text: string; }; /** * A shooter that keeps its place (the Culvert Lookout's crossbow): every `everyMs` it looses at the nearest player it * can see within `range` (a clear line), a hit of up to `share` of its biggest blow, never leaving them below 1 * health. It is not a fight: the player keeps working (the sluices) under fire, and only a fight beside it (its * aggroRadius) is one. A caller with a volley calls while it has someone in its sights. */ volley?: { range: number; everyMs: number; share: number; }; }; /** * One line of a cache's loot. In a scaled event (the def's `scaling`) a line with `scales` has its quantity multiplied * by the reward multiplier for the opener's level, and a `kind` holding `{metal}` takes the metal of the opener's tier * (worldEventScaling.ts: `{metal}helmet` is a bronze, iron or steel helmet). */ export type WorldEventLoot = { kind: string; min: number; max: number; chance: number; scales?: boolean; /** A scaled event only: the line is on the table from this opener scale level up (the Deep Seam's coal from 21, gold from 28, silver from 32). */ fromLevel?: number; /** ...and only up to this scale level (the Deep Seam's iron, which coal, gold and silver replace past the middle). */ toLevel?: number; }; /** Quick ore seams (orerock): the metal is the seam's style, `:cluster` draws the sheet's ore cluster model. */ export type WorldEventSeam = { x: number; z: number; ore: 'copper' | 'tin' | 'iron' | 'coal' | 'gold' | 'silver'; }; /** * A cave-in on the run's clock (the Deep Seam): never rolled. Fall k comes at `firstMs + k * everyMs` into the run, with a chat warning `warnMs` * before; everyone inside the plot and not under a braced roof (`safe` tiles, `safeRooms`) takes `share` of their own maximum health (at least * `minHit`), never leaving them below 1. The step-clear counter: be under a refuge when it falls. */ export type WorldEventCaveIn = { firstMs: number; everyMs: number; warnMs: number; share: number; minHit: number; safe: readonly TileRect[]; safeRooms?: readonly string[]; warning: string; fall: string; hit: string; clear: string; }; export type WorldEventDef = { id: WorldEventId; /** Which rotation it is in (default 'starter'). */ region?: WorldEventRegion; /** How long a run lasts (default WORLD_EVENT_DURATION_MS): each frozen dungeon has its own (James, 2026-10-03). */ durationMs?: number; /** * Scaled to the player (James, 2026-10-03: "yes it can scale with level and difficulty", "xp can increase by scale * too"): enemy levels, cache rewards and Explorer XP from the player's combat level and the dungeon's difficulty * (worldEventScaling.ts). Entry XP is paid once per character per dungeon, ever (`discoveredZones`), not per mouth. */ scaling?: WorldEventScaling; /** The sheet's name. */ label: string; /** What the HUD timer and short notices call it. */ short: string; /** The sheet's levels: entry needs the lower combat level. */ levels: LevelRange; /** The sheet's skills line. */ skills: string; tagline: string; /** What the guide says it is. */ blurb: string; /** The entrance's Blender model (CAPITAL_BUILDING_MODELS id, 5 x 3); placed as EVENT_ENTRANCE_KIND `-`. */ entranceModel: string; /** What the left click on the mouth says ("Enter the mine mouth"). */ enterVerb: string; /** Where the entrance can turn up (all in the starter area; scripts/world-events-smoke.ts checks every one). */ sites: readonly WorldEventSite[]; /** The plot of rock the interior is carved in (walls included): inside(), the client's lighting and clipping. */ plot: TileRect; /** Built and in the rotation. */ built: boolean; /** Explorer XP: on the way in (every run), and the first time a character opens the cache in a run. */ xp: { entry: number; cache: number; }; /** The sheet's typical and rare cache lines, for the guide. */ rewards: string; /** What the cache gives each character once per run. `chance` 1 is always; the rest roll on their own. */ loot: readonly WorldEventLoot[]; /** Built events only: where you land, the floor, the pieces, the puzzle, the streams, the seams. */ arrival?: { x: number; z: number; }; rooms?: Readonly>; pieces?: readonly WorldEventPiece[]; steps?: readonly WorldEventStep[]; /** Symbols shuffled once when a shared run starts (unless `fixedOrder`: Shiverbell's is always Bell, Candle, Crown); the relief gives that run's order. */ symbolPuzzle?: { pieces: readonly string[]; relief: string; gate: string; wrongSpawn: number; fixedOrder?: boolean; text?: WorldEventSymbolText; }; cacheNeeds?: readonly string[]; streams?: readonly WorldEventStream[]; seams?: readonly WorldEventSeam[]; /** A roof that comes down on the run's clock (the Deep Seam). */ caveIn?: WorldEventCaveIn; /** Loose stone to gather (the `stone` pickup). */ stones?: readonly { x: number; z: number; }[]; /** * Rod fishing in a water room (the Old King's Culvert's "normal catches"): fixed spots the world adds to its lake * fishing spots at boot, fished like Henson Pond's rod peg (`rudd`: the Small Fishing Rod and a maggot, Fishing 3). */ rodSpots?: readonly { x: number; z: number; kind: 'rudd'; }[]; /** Rooms that are water, drawn one height step under the floor (the Sinkhole's pool). */ water?: readonly string[]; /** * Rooms open to the sky (the Storm-Felled Grove's clearing): lit as the outdoors (onWorldEventSurface), floored with * grass and ringed by forest instead of rock. The rest stay underground. */ surface?: readonly string[]; }; /** How far above a deck piece's water tile its walkers stand (world units: one height step and a little). */ export declare const EVENT_DECK_LIFT = 0.5; export declare const WORLD_EVENTS: Readonly>; export declare function worldEventDef(id: WorldEventId): WorldEventDef; export declare function isWorldEventId(value: unknown): value is WorldEventId; export declare function inTileRect(r: TileRect, x: number, z: number): boolean; /** The event whose interior plot this tile is in, if any (built or not). */ export declare function worldEventAtInterior(x: number, z: number): WorldEventDef | null; export declare function inWorldEventInterior(x: number, z: number): boolean; /** True in an interior's open-air room (the Storm-Felled Grove's clearing): lit as the outdoors, not as a cave. */ export declare function onWorldEventSurface(x: number, z: number): boolean; /** True on an interior's carved floor (a room), not its rock. */ export declare function onWorldEventFloor(def: WorldEventDef, x: number, z: number): boolean; /** The tile in front of an entrance: where you stand to go in, and where you come back out. */ export declare function worldEventSiteApproach(s: WorldEventSite): { x: number; z: number; }; /** The entrance entity's style. */ export declare function worldEventEntranceStyle(def: WorldEventDef, s: WorldEventSite): string; /** The stable entity ids: an entrance per site, and each piece of an interior. */ export declare function worldEventEntranceId(def: Pick, s: Pick): string; export declare function worldEventPieceEntityId(def: Pick, pieceId: string): string; /** The piece an entity is, if it is one of an event's (by its stable id). */ export declare function worldEventPieceForEntity(entityId: string): { def: WorldEventDef; piece: WorldEventPiece; } | null; /** The style of a piece's entity in a state. */ export declare function worldEventPieceStyle(piece: WorldEventPiece, done: boolean): string; /** * What an event model is called and what a left click on it does, for the client, which knows an entity by its * kind and style (`-`): an entrance's event and verb, a piece's name and verb. A barrier once opened * is just the way through. */ export declare function worldEventModelInfo(model: string, entityId?: string): { name: string; verb: string; open?: boolean; deckLift?: number; } | null; /** The step a click on this piece works, if it is a puzzle piece. */ export declare function worldEventStepFor(def: WorldEventDef, pieceId: string): WorldEventStep | undefined; /** The orerock style for an event seam: the metal, drawn as the sheet's cluster. */ export declare function worldEventSeamStyle(seam: WorldEventSeam): string; /** * One character's share from an event cache. `roll` is called once per line (Math.random in the game, a seeded * stream in tests). Lines that miss their chance give nothing. */ export declare function rollWorldEventLoot(def: WorldEventDef, roll: () => number): { kind: string; qty: number; }[]; /** * A stream NPC's level: inside the range, leaning towards the party's combat level (so a level-2 newcomer meets * level 1-3 rats and a level-9 character the top of the range), with a little spread. */ export declare function worldEventSpawnLevel(range: LevelRange, partyCombat: number, roll: () => number): number; /** How long a run in this event lasts (each frozen dungeon has its own; the starter events five minutes). */ export declare function worldEventDurationMs(def: Pick): number; /** Which rotation the event is in. */ export declare function worldEventRegion(def: Pick): WorldEventRegion; //# sourceMappingURL=worldEvents.d.ts.map