save/load round-trip: workbench bills, crop static-method, bed owner, wolf target now all survive reload via Bill.from_dict reconstruction, _spawn_crop using setup(), and a new _post_load_resolve_references pass. PlantProvider: sow path added; consumes 1 grain on a TILLED crop tile. CraftingProvider: ingredient2 supported via new KIND_DEPOSIT_AT_WB toil and Workbench.deposited_inputs buffer. Cremation pyre now actually consumes wood. HaulingProvider: per-item haul_retry_count + haul_rejected after 3 orphan passes; new EventBus.stockpile_layout_changed resets rejects on any player stockpile edit. Storyteller: 14 stubbed event effects implemented. New buff registry (add_buff/get_buff_multiplier/has_buff, day-prune, save/load) drives seasonal/resource events. New request_pawn_spawn signal + WANDERER table for arrivals. New SICK status + 3 mood thoughts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
129 lines
5.3 KiB
GDScript
129 lines
5.3 KiB
GDScript
class_name StatusCatalog
|
||
## Static factory registry for named statuses.
|
||
##
|
||
## Mirrors ThoughtCatalog: each factory returns a fresh Status with all fields
|
||
## set correctly. Callers must not mutate the returned object before passing
|
||
## it to Pawn.add_status() — add_status() handles severity stack-merging.
|
||
##
|
||
## Phase 9 ships: bleeding(), downed().
|
||
## Phase 12 ships: wet(), cold().
|
||
## Phase 17 ships: sick(). infected() is post-MVP.
|
||
##
|
||
## Usage pattern:
|
||
## pawn.add_status(StatusCatalog.bleeding(2))
|
||
##
|
||
## docs/design.md "Health & status effects"; docs/design.md "Downed & death".
|
||
|
||
## ── Wet status constants (Phase 12) ─────────────────────────────────────────
|
||
## Severity labels: 1 = Damp, 2 = Soaked.
|
||
const WET_DAMP_LEVEL: int = 1
|
||
const WET_SOAKED_LEVEL: int = 2
|
||
## Per-tick accumulator rates for the 0–100 Wet scale.
|
||
## Storm doubles WET_GAIN_PER_TICK.
|
||
const WET_GAIN_PER_TICK: float = 0.02
|
||
const WET_DECAY_PER_TICK: float = 0.05
|
||
## Accumulator thresholds that flip severity.
|
||
const WET_DAMP_THRESHOLD: float = 25.0 ## ≥ 25 → severity 1 (Damp)
|
||
const WET_SOAKED_THRESHOLD: float = 60.0 ## ≥ 60 → severity 2 (Soaked)
|
||
|
||
## ── Cold status constants (Phase 12) ────────────────────────────────────────
|
||
## Severity labels: 1 = Cold, 2 = Very Cold, 3 = Freezing.
|
||
## Cold activates in winter (Clock.SEASON_WINTER) or during a cold snap (any season).
|
||
## Cold snap doubles COLD_GAIN_PER_TICK.
|
||
const COLD_GAIN_PER_TICK: float = 0.015
|
||
const COLD_DECAY_PER_TICK: float = 0.04
|
||
const COLD_MILD_THRESHOLD: float = 25.0 ## ≥ 25 → severity 1
|
||
const COLD_SEVERE_THRESHOLD: float = 60.0 ## ≥ 60 → severity 2
|
||
const COLD_EXTREME_THRESHOLD: float = 85.0 ## ≥ 85 → severity 3
|
||
|
||
## Sim ticks before an untreated bleed-out causes death.
|
||
## design.md "Downed & death": 6 in-game hours.
|
||
## At 20 Hz × 60 s/min × 60 min/hr × 6 hr = 432 000 ticks at 1×.
|
||
## At Fast (5×) that compresses to ~86 400 real-Hz-ticks — but game time is
|
||
## what the player sees, so this constant is in game-time-equivalent ticks
|
||
## (the same 20-Hz tick stream; speed multiplier compresses the real clock,
|
||
## not the tick count that this timer counts against).
|
||
## Phase 20 may retune; keeping the name locked from day one per design.md.
|
||
const BLEED_OUT_TICKS: int = 432000 # 6 in-game hours at 20 Hz
|
||
|
||
## HP lost per sim tick per severity level.
|
||
## Severity 1: 0.05 / tick. Severity 3: 0.15 / tick.
|
||
## At severity 3, 100 HP → 0 in 100 / 0.15 = ~667 ticks ≈ 33 sim-seconds at 1×.
|
||
## At Fast (5×) real time: ~7 s. Tune Phase 20.
|
||
const BLEED_HP_PER_TICK: float = 0.05
|
||
|
||
|
||
## Returns a Bleeding status at the given severity (1–3).
|
||
## Severity is clamped to [1, 3]. Lifetime is PERSISTENT — cleared by doctor
|
||
## treatment, not by time expiry.
|
||
static func bleeding(severity: int = 1) -> Status:
|
||
var s := Status.new()
|
||
s.id = &"bleeding"
|
||
s.kind = Status.Kind.BLEEDING
|
||
s.label = "Bleeding"
|
||
s.severity = clampi(severity, 1, 3)
|
||
s.max_severity = 3
|
||
s.lifetime = Status.Lifetime.PERSISTENT
|
||
return s
|
||
|
||
|
||
## Returns a Downed status.
|
||
## Downed has severity 1 (single-instance — you're either down or you're not).
|
||
## Cleared externally by Pawn._check_revive() when HP rises to HP_REVIVE_THRESHOLD.
|
||
static func downed() -> Status:
|
||
var s := Status.new()
|
||
s.id = &"downed"
|
||
s.kind = Status.Kind.DOWNED
|
||
s.label = "Downed"
|
||
s.severity = 1
|
||
s.max_severity = 1
|
||
s.lifetime = Status.Lifetime.PERSISTENT
|
||
return s
|
||
|
||
|
||
## Returns a Wet status at the given severity (1 = Damp, 2 = Soaked).
|
||
## Severity is clamped to [1, WET_SOAKED_LEVEL]. Lifetime is PERSISTENT —
|
||
## cleared (or severity-adjusted) by Pawn._sync_wet_status() based on accumulator.
|
||
## The accumulator thresholds WET_DAMP_THRESHOLD / WET_SOAKED_THRESHOLD drive
|
||
## which severity the pawn is in; this factory just stamps the initial value.
|
||
static func wet(severity: int = WET_DAMP_LEVEL) -> Status:
|
||
var s := Status.new()
|
||
s.id = &"wet"
|
||
s.kind = Status.Kind.WET
|
||
s.label = "Wet"
|
||
s.severity = clampi(severity, WET_DAMP_LEVEL, WET_SOAKED_LEVEL)
|
||
s.max_severity = WET_SOAKED_LEVEL
|
||
s.lifetime = Status.Lifetime.PERSISTENT
|
||
return s
|
||
|
||
|
||
## Returns a Cold status at the given severity (1 = Cold, 2 = Very Cold, 3 = Freezing).
|
||
## Severity is clamped to [1, 3]. Lifetime is PERSISTENT —
|
||
## cleared (or severity-adjusted) by Pawn._sync_cold_status() based on accumulator.
|
||
static func cold(severity: int = 1) -> Status:
|
||
var s := Status.new()
|
||
s.id = &"cold"
|
||
s.kind = Status.Kind.COLD
|
||
s.label = "Cold"
|
||
s.severity = clampi(severity, 1, 3)
|
||
s.max_severity = 3
|
||
s.lifetime = Status.Lifetime.PERSISTENT
|
||
return s
|
||
|
||
|
||
## Returns a Sick status at the given severity (1 = Mild, 2 = Moderate, 3 = Severe).
|
||
## Severity is clamped to [1, 3]. Lifetime is EVENT — ticks_remaining drives
|
||
## self-clear after the illness duration. Doctor treatment clears it early.
|
||
## At severity 1: ~1 in-game day (4800 ticks at 20 Hz).
|
||
## At severity 2: ~2 in-game days. At severity 3: ~3 in-game days.
|
||
## Pawn._process_statuses() should apply a work-speed penalty while SICK is active.
|
||
static func sick(severity: int = 1) -> Status:
|
||
var s := Status.new()
|
||
s.id = &"sick"
|
||
s.kind = Status.Kind.SICK
|
||
s.label = "Sick"
|
||
s.severity = clampi(severity, 1, 3)
|
||
s.max_severity = 3
|
||
s.lifetime = Status.Lifetime.EVENT
|
||
s.ticks_remaining = 4800 * s.severity # scales with severity
|
||
return s
|