rimlike/scenes/ai/status_catalog.gd
megaproxy d9638a4ea4 fix six critical bugs from audit sprint
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>
2026-05-16 18:06:55 +01:00

129 lines
5.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 0100 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 (13).
## 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