@dreadwood-types/types 0.1.222 → 0.1.224

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * DAILY HOOKS (PLAN_1_TO_40.md E10/D4, 2026-10-14): a small reason to come back tomorrow without a chore list. Everything here
3
+ * turns over at UTC midnight, is the same for every player of the same level, and is a pure function of the day, so the server
4
+ * and the clients agree and a test can read it.
5
+ *
6
+ * - Bruce's daily contract: one of the contracts on the board at the player's level is "today's", and handing it in pays a
7
+ * SECOND Small Satchel, once per character per day (never a PvP target: a player who avoids PvP always has one).
8
+ * - The fair's market day: one of the fair's demand goods pays DOUBLE coins on any fair request that asks for it, all day.
9
+ * - Joss's cart (southFieldsQuests.ts) already turns over daily; the login line names its three orders.
10
+ * - The login line: one chat line at join naming today's contract, the market day, Joss's cart and when Ringstone next gathers.
11
+ * (The Fishing Derby, plan E5, joins the line when it is built.)
12
+ *
13
+ * Amounts are PLACEHOLDERS for the balance pass (a second satchel, double coins).
14
+ */
15
+ import { type BountyOffer, type BountyTargetKind } from './bounty.js';
16
+ import { FAIR_DEMAND_POOL } from './fairFolk.js';
17
+ export declare const DAILY_MS: number;
18
+ /** The day a moment falls in (UTC). */
19
+ export declare function dailyDay(nowMs: number): number;
20
+ /** Bounty targets that can be today's contract: every offer at the player's level except the Dreadwoods PvP ones. */
21
+ export declare function dailyContractCandidates(combatLevel: number): BountyOffer[];
22
+ /** Today's contract on Bruce's board for a player at this Combat level, or null when the board has nothing for them. */
23
+ export declare function dailyContractFor(combatLevel: number, day: number): BountyOffer | null;
24
+ /** The extra Small Satchels a handed-in daily contract adds to the usual one. */
25
+ export declare const DAILY_CONTRACT_EXTRA_SATCHELS = 1;
26
+ /** What the bounty_state message carries about today's contract. */
27
+ export type DailyContractView = {
28
+ target: BountyTargetKind;
29
+ count: number;
30
+ rewardLevel: number;
31
+ claimed: boolean;
32
+ };
33
+ export declare function dailyContractView(combatLevel: number, day: number, claimedDay: number): DailyContractView | null;
34
+ /** The saved day of a handed-in daily contract: a whole day number, else 0 (never). */
35
+ export declare function normalizeDailyDay(raw: unknown): number;
36
+ /** The fair's market day: the demand good that pays double coins today. */
37
+ export declare function marketDayGood(day: number): (typeof FAIR_DEMAND_POOL)[number];
38
+ /** Coins are multiplied by this on a fair request for the market day's good. */
39
+ export declare const MARKET_DAY_COIN_MULT = 2;
40
+ export declare function marketDayMultiplier(kind: string, day: number): number;
41
+ export type TodayInput = {
42
+ now: number;
43
+ combat: number;
44
+ /** The day the character last handed in a daily contract. */
45
+ claimedDay: number;
46
+ /** Joss's cart is open to them (The Harvest Supper is done). */
47
+ cartOpen: boolean;
48
+ };
49
+ /** The login line: what is on today, in one sentence (plain, like a person telling you the news). */
50
+ export declare function todayLine(input: TodayInput): string;
51
+ //# sourceMappingURL=dailyHooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dailyHooks.d.ts","sourceRoot":"","sources":["../src/dailyHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAmE,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvI,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAIjD,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAEzC,uCAAuC;AACvC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9C;AAUD,qHAAqH;AACrH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,EAAE,CAE1E;AAED,wHAAwH;AACxH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAMrF;AAED,iFAAiF;AACjF,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,oEAAoE;AACpE,MAAM,MAAM,iBAAiB,GAAG;IAAE,MAAM,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAIhH;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEtD;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAE5E;AAED,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAUD,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAkBnD"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * DAILY HOOKS (PLAN_1_TO_40.md E10/D4, 2026-10-14): a small reason to come back tomorrow without a chore list. Everything here
3
+ * turns over at UTC midnight, is the same for every player of the same level, and is a pure function of the day, so the server
4
+ * and the clients agree and a test can read it.
5
+ *
6
+ * - Bruce's daily contract: one of the contracts on the board at the player's level is "today's", and handing it in pays a
7
+ * SECOND Small Satchel, once per character per day (never a PvP target: a player who avoids PvP always has one).
8
+ * - The fair's market day: one of the fair's demand goods pays DOUBLE coins on any fair request that asks for it, all day.
9
+ * - Joss's cart (southFieldsQuests.ts) already turns over daily; the login line names its three orders.
10
+ * - The login line: one chat line at join naming today's contract, the market day, Joss's cart and when Ringstone next gathers.
11
+ * (The Fishing Derby, plan E5, joins the line when it is built.)
12
+ *
13
+ * Amounts are PLACEHOLDERS for the balance pass (a second satchel, double coins).
14
+ */
15
+ import { bountyTargetDisplayName, bountyTierForCombat, isPvpBountyTarget } from './bounty.js';
16
+ import { FAIR_DEMAND_POOL } from './fairFolk.js';
17
+ import { harvestCartDay, harvestCartOrdersForDay } from './southFieldsQuests.js';
18
+ import { bossForSlot, nextEventAt, slotAt, RING_BOSSES } from './worldBossRing.js';
19
+ export const DAILY_MS = 24 * 60 * 60_000;
20
+ /** The day a moment falls in (UTC). */
21
+ export function dailyDay(nowMs) {
22
+ return Math.floor(nowMs / DAILY_MS);
23
+ }
24
+ /** Small deterministic mix, so a day's picks are not simply "the next one in the list". */
25
+ function mix(day, salt) {
26
+ let seed = (Math.imul(day + 1, 2654435761) + Math.imul(salt + 7, 40503) + 97) >>> 0;
27
+ seed = (Math.imul(seed ^ (seed >>> 15), 2246822519) + 3266489917) >>> 0;
28
+ seed = (Math.imul(seed ^ (seed >>> 13), 3266489917) + 668265263) >>> 0;
29
+ return seed >>> 0;
30
+ }
31
+ /** Bounty targets that can be today's contract: every offer at the player's level except the Dreadwoods PvP ones. */
32
+ export function dailyContractCandidates(combatLevel) {
33
+ return bountyTierForCombat(combatLevel).filter((o) => !isPvpBountyTarget(o.target));
34
+ }
35
+ /** Today's contract on Bruce's board for a player at this Combat level, or null when the board has nothing for them. */
36
+ export function dailyContractFor(combatLevel, day) {
37
+ const candidates = dailyContractCandidates(combatLevel);
38
+ if (candidates.length === 0)
39
+ return null;
40
+ // The salt is the first offer's target length and count, so two tiers do not move in lock step.
41
+ const salt = candidates.reduce((n, o) => n + o.count + o.rewardLevel, 0);
42
+ return candidates[mix(day, salt) % candidates.length];
43
+ }
44
+ /** The extra Small Satchels a handed-in daily contract adds to the usual one. */
45
+ export const DAILY_CONTRACT_EXTRA_SATCHELS = 1;
46
+ export function dailyContractView(combatLevel, day, claimedDay) {
47
+ const offer = dailyContractFor(combatLevel, day);
48
+ if (!offer)
49
+ return null;
50
+ return { target: offer.target, count: offer.count, rewardLevel: offer.rewardLevel, claimed: claimedDay === day };
51
+ }
52
+ /** The saved day of a handed-in daily contract: a whole day number, else 0 (never). */
53
+ export function normalizeDailyDay(raw) {
54
+ return typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
55
+ }
56
+ /** The fair's market day: the demand good that pays double coins today. */
57
+ export function marketDayGood(day) {
58
+ return FAIR_DEMAND_POOL[mix(day, 11) % FAIR_DEMAND_POOL.length];
59
+ }
60
+ /** Coins are multiplied by this on a fair request for the market day's good. */
61
+ export const MARKET_DAY_COIN_MULT = 2;
62
+ export function marketDayMultiplier(kind, day) {
63
+ return marketDayGood(day).kind === kind ? MARKET_DAY_COIN_MULT : 1;
64
+ }
65
+ function span(ms) {
66
+ const mins = Math.max(1, Math.round(ms / 60_000));
67
+ if (mins < 60)
68
+ return `${mins} minute${mins === 1 ? '' : 's'}`;
69
+ const h = Math.floor(mins / 60);
70
+ const m = mins % 60;
71
+ return m === 0 ? `${h} hour${h === 1 ? '' : 's'}` : `${h} h ${m} min`;
72
+ }
73
+ /** The login line: what is on today, in one sentence (plain, like a person telling you the news). */
74
+ export function todayLine(input) {
75
+ const day = dailyDay(input.now);
76
+ const parts = [];
77
+ const contract = dailyContractFor(input.combat, day);
78
+ if (contract) {
79
+ parts.push(input.claimedDay === day
80
+ ? "Bruce's contract is done for today"
81
+ : `Bruce pays a second satchel for ${contract.count} ${bountyTargetDisplayName(contract.target)}`);
82
+ }
83
+ parts.push(`the fair pays double for ${marketDayGood(day).label} at the stalls`);
84
+ if (input.cartOpen) {
85
+ parts.push(`Joss's cart wants ${harvestCartOrdersForDay(harvestCartDay(input.now)).map((o) => o.label).join(', ')}`);
86
+ }
87
+ const msToNext = nextEventAt(input.now) - input.now;
88
+ const boss = RING_BOSSES[bossForSlot(slotAt(nextEventAt(input.now)))];
89
+ parts.push(`Ringstone gathers in ${span(msToNext)} (a ${boss.name})`);
90
+ const line = `Today: ${parts.join('; ')}.`;
91
+ return line.charAt(0).toUpperCase() + line.slice(1);
92
+ }
93
+ //# sourceMappingURL=dailyHooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dailyHooks.js","sourceRoot":"","sources":["../src/dailyHooks.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,13 @@
17
17
  * the signal brazier in the signal room; the wrong order fails and brings frost rats), then the crank by the chain gate,
18
18
  * and the Watch Captain Remnant in the barracks, who sounds his horn for more deserters while he has you in sight.
19
19
  *
20
+ * - Shiverbell Reliquary (slice C5b, 2026-10-14, the hardest): a ruined chapel's crypt, three chapel symbols to light in the carved
21
+ * order (always Bell, Candle, Crown; a wrong one puts them out and sends frost rats), the Reliquary Warden guarding the Crown
22
+ * chapel, then the Reliquary Gate and the treasury.
23
+ *
24
+ * - Frozen Shore Smuggler Grotto (slice C6, 2026-10-14): the smugglers' sea cave in the frozen coast; wind the rope winch to lower
25
+ * the broken bridge over the ice channel, cross to the smuggler camp, break the vault's lock, open the cache.
26
+ *
20
27
  * Enemy levels, cache rewards and Explorer XP are formula-driven from the scale level (placeholders for the balance
21
28
  * pass in worldEventScaling.ts); the reward lines here keep the sheets' kinds of reward.
22
29
  */
@@ -25,6 +32,8 @@ import type { WorldEventDef } from './worldEvents.js';
25
32
  export declare const FROSTPINE_HOLLOW_ENTRANCE_MODEL = "evfrostpinehollow";
26
33
  export declare const ICE_CULVERT_ENTRANCE_MODEL = "evicedrainmouth";
27
34
  export declare const FROSTWATCH_ENTRANCE_MODEL = "evfrostwatchpost";
35
+ export declare const SHIVERBELL_ENTRANCE_MODEL = "evshiverbellchapel";
36
+ export declare const SMUGGLER_GROTTO_ENTRANCE_MODEL = "evsmugglergrotto";
28
37
  /** The combat level a frozen dungeon lets you in at (the scaled band's floor; the standard gate says it, areaGates.ts). */
29
38
  export declare const FROZEN_DUNGEON_MIN_COMBAT = 10;
30
39
  /**
@@ -53,4 +62,22 @@ export declare const KINGS_ROAD_ICE_CULVERT: WorldEventDef;
53
62
  * the Signal Brazier. Past the Chain Gate lies the Final Cache Room and the Return Exit.
54
63
  */
55
64
  export declare const FROSTWATCH_CELLAR: WorldEventDef;
65
+ /**
66
+ * Shiverbell Reliquary (its sheet: 32 x 22, 15-25 minutes, the hardest, ends on a boss): a ruined roadside chapel with a
67
+ * cracked bell, and the crypt under its stair. The Entry Hall is in the south, the Central Reliquary north of it with the
68
+ * three chapels round it (Bell north, Candle west, Crown east); the Sealed Treasury is behind the Reliquary Gate in the
69
+ * east. The layout is the Sunken Wayshrine's (30 x 22), which has the same shape of puzzle. The order is always Bell, then
70
+ * Candle, then Crown, and the relief by the entry says so; a wrong symbol puts all three out and brings frost rats. The
71
+ * Reliquary Warden (the Watch Captain's model stands in) keeps the Crown chapel with a great deal of health.
72
+ */
73
+ export declare const SHIVERBELL_RELIQUARY: WorldEventDef;
74
+ /**
75
+ * The Frozen Shore Smuggler Grotto (its sheet: 28 x 20, 15-20 minutes): a hidden cave mouth in the frozen coast's crags,
76
+ * nets and barrels heaped in it and a smugglers' flag over. You wade in at the Entry Beach in the west; the Tide Pool
77
+ * Chamber and its fishing lie round the west side, the Ice Channel runs north to south down the middle (water), and the
78
+ * broken bridge over it is hoisted until the Rope Winch on the west bank lowers it. Across it the Smuggler Camp holds the
79
+ * Hidden Supply Vault behind a seized lock and the Final Cache; the way out is the ladder in the south-east. The layout is
80
+ * the Tidepool Crab Grotto's (28 x 20), iced.
81
+ */
82
+ export declare const SMUGGLER_GROTTO: WorldEventDef;
56
83
  //# sourceMappingURL=frozenDungeons.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frozenDungeons.d.ts","sourceRoot":"","sources":["../src/frozenDungeons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,mHAAmH;AACnH,eAAO,MAAM,+BAA+B,sBAAsB,CAAC;AACnE,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAC5D,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAE5D,2HAA2H;AAC3H,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAe5C;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAsItC,CAAC;AASF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,aA8IpC,CAAC;AAQF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAoI/B,CAAC"}
1
+ {"version":3,"file":"frozenDungeons.d.ts","sourceRoot":"","sources":["../src/frozenDungeons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,mHAAmH;AACnH,eAAO,MAAM,+BAA+B,sBAAsB,CAAC;AACnE,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAC5D,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAC5D,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,8BAA8B,qBAAqB,CAAC;AAEjE,2HAA2H;AAC3H,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAe5C;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAsItC,CAAC;AASF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,aA8IpC,CAAC;AAQF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAoI/B,CAAC;AASF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAoFlC,CAAC;AAUF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,aA4H7B,CAAC"}
@@ -17,14 +17,23 @@
17
17
  * the signal brazier in the signal room; the wrong order fails and brings frost rats), then the crank by the chain gate,
18
18
  * and the Watch Captain Remnant in the barracks, who sounds his horn for more deserters while he has you in sight.
19
19
  *
20
+ * - Shiverbell Reliquary (slice C5b, 2026-10-14, the hardest): a ruined chapel's crypt, three chapel symbols to light in the carved
21
+ * order (always Bell, Candle, Crown; a wrong one puts them out and sends frost rats), the Reliquary Warden guarding the Crown
22
+ * chapel, then the Reliquary Gate and the treasury.
23
+ *
24
+ * - Frozen Shore Smuggler Grotto (slice C6, 2026-10-14): the smugglers' sea cave in the frozen coast; wind the rope winch to lower
25
+ * the broken bridge over the ice channel, cross to the smuggler camp, break the vault's lock, open the cache.
26
+ *
20
27
  * Enemy levels, cache rewards and Explorer XP are formula-driven from the scale level (placeholders for the balance
21
28
  * pass in worldEventScaling.ts); the reward lines here keep the sheets' kinds of reward.
22
29
  */
23
- import { EVENT_TOOL_WOODWORK, eventLayout, eventPlot, eventSite } from './worldEventLayout.js';
30
+ import { EVENT_TOOL_WOODWORK, eventLayout, eventPlot, eventPlotRect, eventSite } from './worldEventLayout.js';
24
31
  /** The entrance models (5 x 3 each, CAPITAL_BUILDING_MODELS): the gate table and the client know them by these. */
25
32
  export const FROSTPINE_HOLLOW_ENTRANCE_MODEL = 'evfrostpinehollow';
26
33
  export const ICE_CULVERT_ENTRANCE_MODEL = 'evicedrainmouth';
27
34
  export const FROSTWATCH_ENTRANCE_MODEL = 'evfrostwatchpost';
35
+ export const SHIVERBELL_ENTRANCE_MODEL = 'evshiverbellchapel';
36
+ export const SMUGGLER_GROTTO_ENTRANCE_MODEL = 'evsmugglergrotto';
28
37
  /** The combat level a frozen dungeon lets you in at (the scaled band's floor; the standard gate says it, areaGates.ts). */
29
38
  export const FROZEN_DUNGEON_MIN_COMBAT = 10;
30
39
  const WOODWORK_TOOL = { kind: EVENT_TOOL_WOODWORK, name: 'a hatchet or a saw' };
@@ -479,4 +488,240 @@ export const FROSTWATCH_CELLAR = {
479
488
  },
480
489
  ],
481
490
  };
491
+ // ---------------------------------------------------------------- Shiverbell Reliquary
492
+ const SB_PLOT = eventPlot(0, 6);
493
+ const SB = eventLayout(SB_PLOT);
494
+ const SB_MINUTES = 20;
495
+ /**
496
+ * Shiverbell Reliquary (its sheet: 32 x 22, 15-25 minutes, the hardest, ends on a boss): a ruined roadside chapel with a
497
+ * cracked bell, and the crypt under its stair. The Entry Hall is in the south, the Central Reliquary north of it with the
498
+ * three chapels round it (Bell north, Candle west, Crown east); the Sealed Treasury is behind the Reliquary Gate in the
499
+ * east. The layout is the Sunken Wayshrine's (30 x 22), which has the same shape of puzzle. The order is always Bell, then
500
+ * Candle, then Crown, and the relief by the entry says so; a wrong symbol puts all three out and brings frost rats. The
501
+ * Reliquary Warden (the Watch Captain's model stands in) keeps the Crown chapel with a great deal of health.
502
+ */
503
+ export const SHIVERBELL_RELIQUARY = {
504
+ id: 'shiverbell-reliquary',
505
+ region: 'frozen',
506
+ durationMs: SB_MINUTES * 60_000,
507
+ // The hardest of the five (NEXT_STEPS: Hollow 0, Culvert 1, Frostwatch 1, Grotto 2, Shiverbell 3): a combat-34 player meets 40s.
508
+ scaling: { difficulty: 3 },
509
+ label: 'Shiverbell Reliquary',
510
+ short: 'Shiverbell Reliquary',
511
+ levels: [10, 40],
512
+ skills: 'Combat, Explorer',
513
+ tagline: 'A ruined roadside chapel with a cracked bell, and a crypt nobody has emptied.',
514
+ blurb: 'Climb down the crypt stair of a ruined chapel on the snowfields: frost rats in packs, shrine deserters, and at the end the Reliquary Warden, a crowned knight with a spiked mace who keeps the Crown chapel. Read the carved relief, then light the Bell, the Candle and the Crown, always in that order (a wrong symbol puts all three out and brings more rats), and the Reliquary Gate rises on the Sealed Treasury. Everything in it meets you at your own level, 10 to 40; at the top the Warden is a real fight.',
515
+ entranceModel: SHIVERBELL_ENTRANCE_MODEL,
516
+ enterVerb: 'Descend the crypt stair',
517
+ // Level snow with the camera's view clear (backend finder, T8 2026-10-14: 5 x 3 footprint, ten clear rows south, 28+ tiles from every other frozen mouth).
518
+ sites: [eventSite('bell-north', 133, 81), eventSite('bell-hill', 134, 214), eventSite('bell-south', 97, 260)],
519
+ plot: SB_PLOT,
520
+ built: true,
521
+ xp: { entry: 60, cache: 300 },
522
+ rewards: 'Coins, rations, bars and arrows (more the higher your level); rarely a sword, helmet or shield of your tier (bronze, iron or steel).',
523
+ loot: [
524
+ { kind: 'coins', min: 60, max: 180, chance: 1, scales: true },
525
+ { kind: 'ration', min: 1, max: 3, chance: 0.8, scales: true },
526
+ { kind: '{metal}bar', min: 1, max: 3, chance: 0.65, scales: true },
527
+ { kind: '{metal}arrow', min: 10, max: 30, chance: 0.6, scales: true },
528
+ { kind: '{metal}sword', min: 1, max: 1, chance: 0.05 },
529
+ { kind: '{metal}helmet', min: 1, max: 1, chance: 0.05 },
530
+ { kind: '{metal}shield', min: 1, max: 1, chance: 0.05 },
531
+ ],
532
+ arrival: SB.at(15, 18),
533
+ rooms: Object.fromEntries(Object.entries({
534
+ entry: [12, 16, 18, 19], centre: [11, 10, 19, 14], approach: [14, 15, 16, 16],
535
+ bell: [12, 1, 18, 6], northhall: [14, 7, 16, 9], candle: [1, 7, 7, 12], westhall: [8, 10, 10, 11],
536
+ crown: [22, 7, 27, 12], easthall: [20, 10, 21, 11], westside: [8, 12, 9, 17], southwest: [10, 16, 11, 17],
537
+ eastside: [20, 12, 21, 19], southeast: [18, 18, 19, 19], gatehall: [22, 16, 25, 16],
538
+ treasury: [26, 14, 29, 18], stairs: [20, 20, 23, 21],
539
+ }).map(([id, r]) => [id, SB.rect(r[0], r[1], r[2], r[3])])),
540
+ pieces: [
541
+ { id: 'return', role: 'exit', model: 'evshrinestairs', ...SB.spot(15, 19, 'n'), name: 'Crypt stair', verb: 'Climb out' },
542
+ { id: 'exit', role: 'exit', model: 'evshrinestairs', ...SB.spot(23, 21, 'n'), name: 'Way up', verb: 'Climb out' },
543
+ { id: 'altar', role: 'decor', model: 'evshrinealtar', ...SB.spot(15, 12, 's'), name: 'Central reliquary', verb: 'Examine',
544
+ text: 'A stone reliquary on a tiered plinth, split down one side by the frost. Whatever it held was taken long ago; the bell above still rings now and then.' },
545
+ { id: 'relief', role: 'sign', model: 'evsymbolrelief', ...SB.spot(13, 16, 's'), name: 'Carved relief', verb: 'Read' },
546
+ ...['bell', 'candle', 'crown'].map((id, k) => ({
547
+ id, role: 'switch', model: `evsymbol${['sun', 'river', 'crown'][k]}`, doneModel: `evsymbol${['sun', 'river', 'crown'][k]}lit`,
548
+ ...SB.spot([15, 3, 25][k], [2, 9, 9][k], 's'),
549
+ name: `${id[0].toUpperCase()}${id.slice(1)} symbol`, verb: 'Light',
550
+ })),
551
+ { id: 'gate', role: 'barrier', model: 'evreliquarygate', doneModel: 'evreliquarygateopen', ...SB.spot(24, 16, 's'), name: 'Reliquary Gate', verb: 'Inspect' },
552
+ { id: 'cache', role: 'cache', model: 'evreliquarycache', doneModel: 'evreliquarycacheopen', ...SB.spot(28, 16, 'w'), name: 'Treasury Chest', verb: 'Open' },
553
+ { id: 'sign-timer', role: 'sign', model: 'evsigntimer', ...SB.spot(17, 17, 's'), name: 'The clock', verb: 'Read', text: timerText(SB_MINUTES) },
554
+ { id: 'sign-combat', role: 'sign', model: 'evsigncombat', ...SB.spot(13, 18, 's'), name: 'Crypt warning', verb: 'Read',
555
+ text: 'Frost rats in packs, shrine deserters, and the Reliquary Warden, who keeps the Crown chapel with a spiked mace. They meet whoever comes at their own strength.' },
556
+ ],
557
+ symbolPuzzle: { pieces: ['bell', 'candle', 'crown'], relief: 'relief', gate: 'gate', wrongSpawn: 2, fixedOrder: true },
558
+ steps: [{ piece: 'gate', needs: ['bell', 'candle', 'crown'], text: '', notYet: 'The Reliquary Gate is sealed. Read the carved relief and light the chapel symbols in its order.' }],
559
+ streams: [
560
+ {
561
+ kind: 'frostrat', name: 'Frost Rat', levels: [10, 35], levelOffset: -8,
562
+ // The Frostwatch data fix from the start (W-313): half health, a lower offset, one every 30 s up to two, so a rat pack
563
+ // never keeps a tester in combat (in combat nothing can be clicked, and the symbols are clicks). PLACEHOLDERS.
564
+ nests: [SB.at(16, 5), SB.at(5, 11), SB.at(9, 14)],
565
+ healthMultiplier: 0.5, cap: 2, perExtraPlayer: 1, everyMs: 30_000, firstWave: 1, aggroCombat: 10,
566
+ aggroText: 'A frost rat bursts out of the chapel shadows, red eyes in a white face!',
567
+ },
568
+ {
569
+ kind: 'snowdeserter', name: 'Shrine Deserter', levels: [10, 38], levelOffset: -5,
570
+ // The sheet: "melee or javelin". Melee only for now, out of the Candle chapel and the east side, never within five
571
+ // tiles (the aggro radius) of the landing.
572
+ nests: [SB.at(4, 9), SB.at(20, 17), SB.at(16, 3)],
573
+ healthMultiplier: 0.75, cap: 1, perExtraPlayer: 1, everyMs: 45_000, firstWave: 1, aggroCombat: 10,
574
+ aggroText: 'A shrine deserter staggers out of the dark with a rusted blade: "The bell tolls for us. Not for you."',
575
+ },
576
+ {
577
+ kind: 'watchcaptain', name: 'Reliquary Warden', levels: [11, 40], levelOffset: 0,
578
+ // The sheet: a crowned knight with a spiked mace and a frost aura, one a run, the boss. He keeps the Crown chapel (the last
579
+ // symbol) and is a real fight at the top of the band. The watch captain's model stands in until the Warden has a Blender
580
+ // build (NEXT_STEPS).
581
+ nests: [SB.at(24, 10)],
582
+ cap: 1, perExtraPlayer: 0, everyMs: SB_MINUTES * 60_000, firstWave: 1, aggroCombat: 10,
583
+ stationary: true, aggroRadius: 4, leashRadius: 0, healthMultiplier: 2.5, scale: 1.3,
584
+ aggroText: 'The Reliquary Warden turns in his frozen crown, spiked mace rising, and the cold comes off him in a wave: "None pass the Crown."',
585
+ },
586
+ ],
587
+ };
588
+ // ---------------------------------------------------------------- Frozen Shore Smuggler Grotto
589
+ // Row 7 would run into the Dwarf Hold's props at z 1107+ (x 952-980), so the Grotto's plot is the free rock between Shiverbell's and the Hold's.
590
+ const SG_PLOT = eventPlotRect(0, 1058, 48);
591
+ const SG = eventLayout(SG_PLOT);
592
+ const SG_MINUTES = 18;
593
+ const SG_WALK = [15, 16, 17, 18, 19];
594
+ /**
595
+ * The Frozen Shore Smuggler Grotto (its sheet: 28 x 20, 15-20 minutes): a hidden cave mouth in the frozen coast's crags,
596
+ * nets and barrels heaped in it and a smugglers' flag over. You wade in at the Entry Beach in the west; the Tide Pool
597
+ * Chamber and its fishing lie round the west side, the Ice Channel runs north to south down the middle (water), and the
598
+ * broken bridge over it is hoisted until the Rope Winch on the west bank lowers it. Across it the Smuggler Camp holds the
599
+ * Hidden Supply Vault behind a seized lock and the Final Cache; the way out is the ladder in the south-east. The layout is
600
+ * the Tidepool Crab Grotto's (28 x 20), iced.
601
+ */
602
+ export const SMUGGLER_GROTTO = {
603
+ id: 'frozen-shore-grotto',
604
+ region: 'frozen',
605
+ durationMs: SG_MINUTES * 60_000,
606
+ scaling: { difficulty: 2 },
607
+ label: 'Frozen Shore Smuggler Grotto',
608
+ short: 'Smuggler Grotto',
609
+ levels: [10, 40],
610
+ skills: 'Combat, Fishing, Foraging, Explorer',
611
+ tagline: "A smugglers' sea cave in the frozen coast, and a bridge they pulled up behind them.",
612
+ blurb: "Wade into a hidden cave mouth in the crags of the frozen coast: shore crabs in the tide pools, fish and shells to gather, and across the ice channel a smuggler camp with a lookout who whistles up more of them for as long as he has you in sight. Wind the rope winch to lower the broken bridge, cross, break the vault's seized lock and open the cache. Everything in it meets you at your own level, 10 to 40.",
613
+ entranceModel: SMUGGLER_GROTTO_ENTRANCE_MODEL,
614
+ enterVerb: 'Wade into the cave mouth',
615
+ // The frozen coast (discovery zone frozen-shore, x 6-7 beside the crags; north of z 120 the shore is cut off) found the same way as Shiverbell's.
616
+ sites: [eventSite('shore-middle', 6, 172), eventSite('shore-crag', 6, 232), eventSite('shore-south', 7, 323), eventSite('shore-far', 6, 368)],
617
+ plot: SG_PLOT,
618
+ built: true,
619
+ xp: { entry: 50, cache: 150 },
620
+ rewards: 'Coins, fish, crab meat, arrows and rations (more the higher your level); rarely a dagger, helmet or boots of your tier (bronze, iron or steel).',
621
+ loot: [
622
+ { kind: 'coins', min: 60, max: 200, chance: 1, scales: true },
623
+ { kind: 'rawtrout', min: 1, max: 3, chance: 0.7, scales: true },
624
+ { kind: 'crabmeat', min: 1, max: 2, chance: 0.6, scales: true },
625
+ { kind: '{metal}arrow', min: 8, max: 24, chance: 0.5, scales: true },
626
+ { kind: 'ration', min: 1, max: 2, chance: 0.6, scales: true },
627
+ { kind: '{metal}dagger', min: 1, max: 1, chance: 0.08 },
628
+ { kind: '{metal}helmet', min: 1, max: 1, chance: 0.03 },
629
+ { kind: '{metal}boots', min: 1, max: 1, chance: 0.03 },
630
+ ],
631
+ arrival: SG.at(2, 9),
632
+ rooms: {
633
+ beach: SG.rect(0, 6, 4, 12),
634
+ poolA: SG.rect(2, 1, 8, 5),
635
+ poolB: SG.rect(9, 1, 14, 5),
636
+ west: SG.rect(5, 6, 14, 12),
637
+ poolC: SG.rect(2, 13, 14, 18),
638
+ channel: SG.rect(15, 0, 19, 19),
639
+ shelf: SG.rect(20, 1, 25, 5),
640
+ far: SG.rect(20, 6, 26, 13),
641
+ exitway: SG.rect(21, 14, 26, 18),
642
+ },
643
+ water: ['channel'],
644
+ pieces: [
645
+ { id: 'tunnel', role: 'exit', model: 'evseatunnel', ...SG.spot(0, 9, 'e'), name: 'Cave mouth', verb: 'Wade back out' },
646
+ { id: 'ladder', role: 'exit', model: 'evladder', ...SG.spot(26, 17, 'w'), name: 'Return Exit', verb: 'Climb out' },
647
+ { id: 'winch', role: 'switch', model: 'evhandwinch', doneModel: 'evhandwinchwound', ...SG.spot(14, 9, 'e'), name: 'Rope winch', verb: 'Wind' },
648
+ ...SG_WALK.map((x, k) => ({
649
+ id: `walk${k + 1}`, role: 'barrier', model: k === 0 ? 'evwalkwayleafup' : 'evwalkwayposts', doneModel: 'evwalkwaydeck', ...SG.spot(x, 10, 'e'),
650
+ name: 'Broken bridge', verb: 'Inspect', deck: true,
651
+ })),
652
+ { id: 'lock', role: 'switch', model: 'evseizedlock', doneModel: 'evlockstruck', ...SG.spot(24, 12, 'n'), name: 'Vault lock', verb: 'Break' },
653
+ { id: 'cache', role: 'cache', model: 'evfrozencache', doneModel: 'evfrozencacheopen', ...SG.spot(25, 9, 'w'), name: "Smugglers' Cache", verb: 'Open' },
654
+ ...[[4, 1, 's'], [23, 1, 's']].map(([x, z, f], k) => ({
655
+ id: `shells${k + 1}`, role: 'node', model: 'evshellshelf', doneModel: 'evshellshelfbare', ...SG.spot(x, z, f),
656
+ name: 'Shell shelf', verb: 'Gather from', node: { give: [{ kind: 'seashell', min: 1, max: 2 }], skill: 'foraging', xp: 6, respawnMs: 45_000 },
657
+ })),
658
+ ...[[2, 11, 's'], [7, 17, 's']].map(([x, z, f], k) => ({
659
+ id: `worms${k + 1}`, role: 'node', model: 'evlugworms', doneModel: 'evlugwormsdug', ...SG.spot(x, z, f),
660
+ name: 'Bait sand', verb: 'Dig in', node: { give: [{ kind: 'lugworm', min: 1, max: 3 }], skill: 'foraging', xp: 6, respawnMs: 45_000 },
661
+ })),
662
+ // The fishing inside (the sheet: "fishing and foraging spots inside"): the cage ledge hauls crab and the chamber's ice hole a trout.
663
+ { id: 'cages', role: 'node', model: 'evcrabcages', doneModel: 'evcrabcagesempty', ...SG.spot(12, 4, 's'), name: 'Crab cages', verb: 'Haul',
664
+ node: { give: [{ kind: 'crabmeat', min: 0, max: 2 }], skill: 'fishing', xp: 12, respawnMs: 60_000 } },
665
+ { id: 'icehole', role: 'node', model: 'evripplespot', doneModel: 'evripplespotcalm', ...SG.spot(6, 15, 's'), name: 'Ice fishing hole', verb: 'Fish',
666
+ node: { give: [{ kind: 'rawtrout', min: 0, max: 2 }], skill: 'fishing', xp: 14, respawnMs: 60_000 } },
667
+ ...[[3, 3], [11, 2], [4, 15], [12, 14], [22, 4], [24, 8]].map(([x, z], k) => ({
668
+ id: `pool${k + 1}`, role: 'decor', model: 'evtidepool', ...SG.spot(x, z, 's'), name: 'Tide pools', verb: 'Examine',
669
+ })),
670
+ { id: 'sign-timer', role: 'sign', model: 'evsigntimer', ...SG.spot(3, 7, 's'), name: 'The clock', verb: 'Read', text: timerText(SG_MINUTES) },
671
+ { id: 'sign-combat', role: 'sign', model: 'evsigncombat', ...SG.spot(1, 10, 's'), name: 'Smuggler warning', verb: 'Read',
672
+ text: 'Shore crabs in the pools, snow smugglers at the camp and a lookout on the far ledge who whistles for more. Take the crabs first, then the bridge. They meet whoever comes at their own strength.' },
673
+ { id: 'sign-exit', role: 'sign', model: 'evsignexit', ...SG.spot(24, 16, 's'), name: 'Way out', verb: 'Read', text: 'The way out. Take it any time you have had enough.' },
674
+ ],
675
+ steps: [
676
+ {
677
+ piece: 'winch',
678
+ opens: SG_WALK.map((_, k) => `walk${k + 1}`),
679
+ text: "The rope winch groans round and round. Across the ice channel the broken bridge comes down with a boom: the smugglers' camp is in reach.",
680
+ notYet: 'The winch is frozen stiff.',
681
+ },
682
+ ...SG_WALK.map((_, k) => ({
683
+ piece: `walk${k + 1}`,
684
+ needs: ['winch'],
685
+ text: '',
686
+ notYet: 'The bridge over the ice channel is hauled up out of reach, its planks standing on end. The rope winch on this bank should lower it.',
687
+ })),
688
+ {
689
+ piece: 'lock',
690
+ needs: ['winch'],
691
+ text: "You bring a rock down on the vault's seized lock again and again, and the frozen iron shears: the Smugglers' Cache can be opened.",
692
+ notYet: 'A seized iron lock on the vault door, frozen shut. Cross the bridge first.',
693
+ },
694
+ ],
695
+ cacheNeeds: ['lock'],
696
+ streams: [
697
+ {
698
+ kind: 'shorecrab', name: 'Shore Crab', levels: [10, 35], levelOffset: -8,
699
+ // The sheet: "many, low threat". The Frostwatch data fix from the start (W-313): half health, a lower offset, one every
700
+ // 30 s up to three, so crabs never keep a tester in combat while the winch and the lock wait to be clicked.
701
+ nests: [SG.at(5, 3), SG.at(11, 3), SG.at(6, 15), SG.at(22, 3)],
702
+ healthMultiplier: 0.5, cap: 3, perExtraPlayer: 1, everyMs: 30_000, firstWave: 1, aggroCombat: 10,
703
+ aggroText: 'A shore crab sidles out of the pool, claws raised!',
704
+ },
705
+ {
706
+ kind: 'smuggler', name: 'Snow Smuggler', levels: [10, 38], levelOffset: -5,
707
+ // The sheet: "dangerous in groups". Pairs at most, at the camp across the bridge and never within five tiles of the
708
+ // bridge's landing; a lower offset and 0.75x health from the start (W-313).
709
+ nests: [SG.at(23, 7), SG.at(23, 12)],
710
+ healthMultiplier: 0.75, cap: 2, perExtraPlayer: 1, everyMs: 45_000, firstWave: 1, aggroCombat: 10,
711
+ aggroText: 'A snow smuggler jumps down from the crates with a short blade: "This cave is ours."',
712
+ },
713
+ {
714
+ kind: 'culvertlookout', name: 'Grotto Lookout', levels: [10, 40], levelOffset: 0,
715
+ // The sheet: "archer on ledges, calls reinforcements". The Culvert Lookout's mechanics: on the camp's near edge over
716
+ // the channel, a volley (not a fight) at whoever he can see crossing, and a whistle for another smuggler while he does;
717
+ // come up onto the ledge and he fights hand to hand.
718
+ nests: [SG.at(21, 8)],
719
+ cap: 1, perExtraPlayer: 0, everyMs: SG_MINUTES * 60_000, firstWave: 1, aggroCombat: 10,
720
+ stationary: true, aggroRadius: 1, leashRadius: 0,
721
+ volley: { range: 7, everyMs: 4_000, share: 0.6 },
722
+ calls: { stream: 1, everyMs: 30_000, max: 1, text: 'The Grotto Lookout swings his lantern and whistles: a snow smuggler comes running from the camp!' },
723
+ aggroText: 'On the far ledge the Grotto Lookout raises his lantern and his bow: "Not one step further, shorewalker."',
724
+ },
725
+ ],
726
+ };
482
727
  //# sourceMappingURL=frozenDungeons.js.map