/** * THE HEARTWOOD HUNT (proposal A7 in the backend's AREA_PROPOSALS.md, 2026-10-14; early-game program track T74; levels 24-32). * Every three hours a great heartwood tree rises somewhere in the Rotwood Grove for 15 minutes. Cutting it needs Woodwork (24+), * and the Rotwood Ent's brood of moss wolves comes for the cutters, so Combat matters just as much: the people who cut AND the people * who guard share a podium, in the Grove, where other players can attack and nobody heals (the Dreadwoods' no-healing rule). * * Everything here is pure: the clock, the places, the scoring, the prizes and the saved state, so the server, a test and the login line * agree. Round n starts at `n * HUNT_PERIOD_MS + HUNT_OFFSET_MS`, so a restart, a deploy or a second server agree on the round and the * tree's place without saving anything. The offset keeps it clear of Ringstone's fights (the 3-hour marks plus 0, 60 and 90 minutes) * and of the Fishing Derby (even hours at :40). * * The scoring is built against collusion and farming (abuse-first): * - only OWN actions score: a successful cut of the tree, a kill of the round's own brood. A player kill, a death or a trade scores * nothing, so an ambush at the tree takes cuts from nobody and a friend cannot be fed points; * - each side is capped (16 cuts, 8 brood kills) so no one can run away with the board, and past the cut cap a cut still helps the * tree fall, which is the shared goal; * - one place per ACCOUNT (a shared account or an alt takes no second prize), a minimum score to be paid at all, a podium only when * three different accounts qualified AND the tree fell, and at most four paid rounds per account per UTC day; * - ties go to whoever reached the score first, then the character key, so the order is total; * - prizes are Woodwork XP, Explorer XP and logs: no coins, nothing that outclasses the gated 41+ gear. * * Amounts are PLACEHOLDERS for the balance pass, done last with James. */ import type { LanternWakeBox } from './lanternWake.js'; /** Time from one round's start to the next. */ export declare const HUNT_PERIOD_MS: number; /** Round n starts this long after the n-th period mark (Ringstone fights start at 0, 60 and 90 minutes; the Derby at :40 of even hours). */ export declare const HUNT_OFFSET_MS: number; /** How long the heartwood stands. */ export declare const HUNT_ROUND_MS: number; /** How long the result board stays up after the round. */ export declare const HUNT_RESULTS_MS: number; /** The warning before the round starts, and the warning before it ends. */ export declare const HUNT_SOON_MS: number; export declare const HUNT_LAST_MS: number; /** idle (nothing announced), soon, open, last (final minutes), results (the board is up). */ export type HuntStage = 'idle' | 'soon' | 'open' | 'last' | 'results'; export declare const HUNT_STAGES: readonly HuntStage[]; export declare function isHuntStage(s: unknown): s is HuntStage; export declare function huntStartOf(slot: number): number; export declare function huntEndOf(slot: number): number; export type HuntClock = { /** The round this stage is about: the open or just-finished one, else the next to start. */ slot: number; stage: HuntStage; /** Time to this round's start (0 once it has begun). */ msToStart: number; /** Time to this round's end (0 once it is over). */ msToEnd: number; }; export declare function huntClockAt(now: number): HuntClock; /** True while the tree stands and cuts and brood kills count. */ export declare function huntIsOpen(stage: HuntStage): boolean; /** The next round to start strictly after `now` (for the login line): its slot and start. */ export declare function huntNextRound(now: number): { slot: number; startsAt: number; }; /** The UTC day a time falls in (the daily paid-rounds cap resets at midnight UTC). */ export declare function huntDayOf(now: number): number; /** * Where the heartwood can rise (world tiles, inside the Grove's clearing, clear of every prop and at least nine tiles from the Ent (it wakes at six); the backend * smoke pins each one standable with a standable ring round it). Five places, the tree never in the same one twice running. */ export declare const HUNT_SPOTS: readonly { id: string; x: number; z: number; label: string; }[]; /** The place of a round. Consecutive rounds never share one (the step is 2 inside a block of five rounds and 3 across a block edge). */ export declare function huntSpotFor(slot: number): (typeof HUNT_SPOTS)[number]; /** The tree: an ash (the ladder's level-30 wood) drawn large; the server places it while a round is on and takes it away after. */ export declare const HUNT_TREE_ID = "hh-tree"; export declare const HUNT_TREE_STYLE = "ash"; export declare const HUNT_TREE_SCALE = 1.8; /** Woodwork needed to take a swing at the heartwood. */ export declare const HUNT_MIN_WOODWORK = 24; /** The tree's level in the felling roll (types woodwork.ts woodcutSuccessChance): between alder (25) and ash (30). */ export declare const HUNT_TREE_LEVEL = 28; /** Successful cuts, from everyone together, that fell the heartwood. PLACEHOLDER. */ export declare const HUNT_TREE_CUTS = 90; /** The log a cut yields (alder is the Woodwork ladder's level-25 wood: a placeholder for rotwood), and how many a player gets per round. */ export declare const HUNT_CUT_LOG = "alderwood"; export declare const HUNT_LOGS_PER_PLAYER = 12; /** Woodwork XP for a scored cut. */ export declare const HUNT_CUT_XP = 30; export declare const HUNT_CUT_POINTS = 2; export declare const HUNT_KILL_POINTS = 3; /** Past these a cut or a kill still helps (the tree still falls) but scores nothing more. */ export declare const HUNT_CUT_CAP = 16; export declare const HUNT_KILL_CAP = 8; /** The least score that counts as having taken part (3 cuts, or 2 kills). Below it: no prize, no mark. */ export declare const HUNT_MIN_SCORE = 6; /** Podium prizes need at least this many different accounts above the minimum, and the tree felled. */ export declare const HUNT_MIN_HUNTERS = 3; /** Paid rounds per account per UTC day (alts and shared accounts cannot farm every round of the day). */ export declare const HUNT_DAILY_PAID_ROUNDS = 4; export declare function huntScore(cuts: number, kills: number): number; export type HuntEntry = { /** characterId. */ key: string; /** The account, so one account's characters cannot all take prizes. Empty when unknown (then the key stands in). */ account: string; name: string; /** Successful cuts and brood kills this round (uncapped; the score caps them). */ cuts: number; kills: number; /** Logs already handed over this round (capped per player). */ logs: number; /** When the score last rose (ties go to whoever reached the score first). */ at: number; }; export declare function huntEntryScore(e: Pick): number; type Who = { key: string; account: string; name: string; }; /** One successful cut by this character. `gaveLog` is true when a log went to the bag. Returns a new entry (the old is untouched). */ export declare function huntWithCut(entry: HuntEntry | undefined, who: Who, gaveLog: boolean, at: number): HuntEntry; /** One brood kill by this character. */ export declare function huntWithKill(entry: HuntEntry | undefined, who: Who, at: number): HuntEntry; /** Does a log still go to this character's bag (before the cut that would give it)? */ export declare function huntLogDue(entry: HuntEntry | undefined): boolean; export declare function huntQualifies(e: HuntEntry): boolean; /** * The ranking: higher score first, then whoever reached it first, then the key (a total order a test can pin). One place per ACCOUNT: * of an account's characters only the best-placed one is listed. Entries below the minimum score are not ranked. */ export declare function huntRanking(entries: readonly HuntEntry[]): HuntEntry[]; /** The 1-based place of a character in the ranking (ranked entries only), or 0. */ export declare function huntPlaceOf(entries: readonly HuntEntry[], key: string): number; export type HuntRibbon = 'red' | 'blue' | 'bronze'; export declare const HUNT_RIBBON_LABEL: Readonly>; /** PLACEHOLDER prizes by place (felled tree only): Woodwork XP, Explorer XP and ash logs. */ export declare const HUNT_PODIUM_PRIZES: readonly { woodworkXp: number; explorerXp: number; ash: number; }[]; /** Everyone paid who qualified. A tree that stood to the end halves the XP and gives no logs. */ export declare const HUNT_PARTICIPATION: { readonly woodworkXp: 120; readonly explorerXp: 20; readonly alder: 2; }; /** Ledger marks (types explorerLedger.ts `hunt` page) a result can write. */ export declare const HUNT_MARK_ENTERED = "hunt:entered"; export declare const HUNT_MARK_FELLED = "hunt:felled"; export declare const HUNT_MARK_CUTTER = "hunt:cutter"; export declare const HUNT_MARK_GUARD = "hunt:guard"; export declare const HUNT_MARK_RIBBON: Readonly>; export type HuntItem = { kind: string; qty: number; }; /** What one character is owed for a round. */ export type HuntPrize = { woodworkXp: number; explorerXp: number; items: HuntItem[]; /** Ledger marks to write. */ marks: string[]; /** The chat line they are told. */ text: string; }; /** The paid-rounds record: per account, the day and how many rounds were paid that day. */ export type HuntPaid = Record; /** Paid rounds this account has had on this UTC day. */ export declare function huntPaidToday(paid: HuntPaid, account: string, day: number): number; /** The record after one more paid round for these accounts (old days dropped). Returns a new object. */ export declare function huntWithPaid(paid: HuntPaid, accounts: readonly string[], day: number): HuntPaid; export type HuntPrizeInput = { felled: boolean; paid: HuntPaid; day: number; }; /** * Everything a finished round pays, by character key. One prize per account (the ranking lists an account once). An account that has * had its four paid rounds today is skipped (the podium moves up to the next account). Pure, so a restart that finalises a round late * pays exactly what the clock-time finalise would have. */ export declare function huntPrizes(entries: readonly HuntEntry[], input: HuntPrizeInput): Map; /** The world chat line for a finished round. */ export declare function huntResultLine(entries: readonly HuntEntry[], felled: boolean): string; export declare const HUNT_NAME = "Heartwood Hunt"; /** The world chat line for a stage (idle and results have none: the results line is built from the standings). */ export declare function huntChatLine(stage: HuntStage, slot: number): string | null; /** The login line's piece about the hunt. */ export declare function huntTodayText(now: number): string; /** What /hunt tells a player. */ export declare function huntStatusLines(clock: HuntClock, entries: readonly HuntEntry[], myKey: string | null, cutsDone: number, now: number): string[]; /** * The Rotwood Ent's brood: moss wolves (the Grove's own kind), sent in waves while the tree stands. Levels follow the hunters' average * Combat, clamped; the count follows how many are at the tree. PLACEHOLDERS. */ export declare const HUNT_BROOD: { readonly kind: "mosswolf"; readonly idPrefix: "hh-"; readonly firstWave: 3; readonly waveEveryMs: 40000; readonly perExtraHunter: 1; readonly waveMax: 7; readonly aliveCap: 14; /** Wolves come out of the dead wood this far from the tree. */ readonly spawnMin: 6; readonly spawnMax: 9; readonly aggroRadius: 10; readonly leashRadius: 16; readonly minLevel: 22; readonly maxLevel: 32; /** A hunter is "at the tree" this close. */ readonly nearTree: 12; readonly aggroText: "A wolf of the Ent's brood bursts from the roots at you!"; }; /** How many wolves a wave brings: the first wave is fixed; later ones grow with the hunters at the tree. */ export declare function huntWaveSize(wave: number, hunters: number): number; /** The brood's level for a party's average Combat. */ export declare function huntBroodLevel(avgCombat: number): number; /** * The Ent's grasp, the mini-boss telegraph: the roots under the heartwood heave. A chat line goes out `HUNT_GRASP.leadMs` ahead * naming the tiles; whoever is still within `radius` of the tree when it lands takes the blow, whoever stepped out takes nothing. * (The real mini-boss, a drawn ground ring and a Sapling Ent, are logged as upgrades.) */ export declare const HUNT_GRASP: { readonly everyMs: 50000; readonly leadMs: 3000; readonly radius: 2; /** Raw damage before armour: PLACEHOLDER. */ readonly damage: 14; readonly warn: "The roots under the heartwood heave! Step away from the trunk!"; readonly hit: "The Ent's roots lash up and seize you!"; readonly dodged: "You step clear as the roots slam down."; }; /** Who stands within `radius` (Chebyshev) of the centre. */ export declare function huntInGrasp(centre: { x: number; z: number; }, players: readonly T[], radius?: number): T[]; export declare const FORESTER_NAME = "Forester Imre Thorne"; export declare const FORESTER_ID = "forester"; /** The forester's lean-to camp on the way into the Grove, just south of the Grove's path end (z 400): a PvP-off pocket (types safePockets.ts). */ export declare const HEARTWOOD_CAMP: LanternWakeBox; export declare const FORESTER_SPOT: { readonly x: 330; readonly z: number; readonly faceX: 330; readonly faceZ: number; }; /** The camp's placard. */ export declare const HEARTWOOD_CAMP_SIGN = "The Forester's Camp. No fighting inside the lanterns. A heartwood rises in the Rotwood Grove, north, every three hours (Woodwork 24 to cut it). The Grove is PvP ground."; export type HeartwoodCampProp = { id: string; kind: 'dreadprop' | 'campfire' | 'barrel' | 'lantern' | 'signpost'; style: string; x: number; z: number; scale?: number; }; /** Stand-in pieces from the Dreadwoods kit (logged as an upgrade): a tent, the fire, crates, a barrel, lanterns and a sign. */ export declare const HEARTWOOD_CAMP_PIECES: readonly HeartwoodCampProp[]; export declare const HEARTWOOD_CAMP_ENTER = "You step into the forester's camp. Nobody can attack you here, and the Grove's wolves do not come this far."; export declare const HEARTWOOD_CAMP_LEAVE = "You step out of the forester's camp. This is Dreadwoods ground again: other players can attack you."; export type HuntSave = { v: 1; /** The round the entries belong to (-1 = none). */ slot: number; /** The stage last announced for `announced.slot`. */ announced: { slot: number; stage: HuntStage; } | null; /** True once `slot`'s results have been worked out and paid or queued. */ finalized: boolean; /** True once the tree has fallen this round (the round ends there). */ felled: boolean; /** Successful cuts so far this round, from everyone. */ cutsDone: number; /** Brood waves sent this round. */ waves: number; entries: HuntEntry[]; /** Prizes for characters who were offline when the round ended, by characterId. */ pending: Record; /** Paid rounds per account per UTC day. */ paid: HuntPaid; }; export declare function emptyHuntSave(): HuntSave; /** At most this many prizes wait for one character, and entries are never more than this many. */ export declare const HUNT_PENDING_MAX = 5; export declare const HUNT_MAX_ENTRIES = 1000; /** A saved state, checked field by field (it comes from the database); anything wrong gives the empty state. */ export declare function parseHuntSave(raw: unknown): HuntSave; export {}; //# sourceMappingURL=heartwoodHunt.d.ts.map