rimlike/scenes/ai/status_catalog.gd
megaproxy 1b6ad2bcc6 Phase 9+10: Status/Doctor/Medical bed + Wolves/WolfSpawner
The 'drama pair' shipped together via 3-agent fan-out.

Phase 9 — Status effects + Medicine:
- Status data class (PERSISTENT/EVENT, severity stacks max=3) + StatusCatalog
  (Bleeding ticks HP loss; Downed = incapacitated)
- Pawn HP (100 max, 30 downed threshold, 50 revive threshold), take_damage,
  heal, add_status/remove_status_by_id, is_downed/is_incapacitated, downed
  visual (body rotated 90° + desaturated)
- DoctorProvider (priority 9, highest) — scans World.pawns for nearest downed
  pawn, finds medical bed (or any bed fallback), emits 4-toil job:
  walk_to_patient → rescue → walk_to_bed → treat
- Bed.is_medical with red-cross marker draw on pillow; round-trips save
- KIND_RESCUE + KIND_TREAT toils + JobRunner _tick_rescue/_tick_treat
  (snap-to-bed on first treat tick, +0.5 hp/tick, bleed cure at 100-tick
  intervals; done at HP≥50 + no bleeding, 600-tick timeout)
- EventBus: pawn_took_damage, pawn_status_added, pawn_status_removed

Phase 10 — Combat + Wolves (wolf-first slice):
- Wolf entity (Node2D, 4-state APPROACH/ENGAGE/FLEE/DEAD, procedural
  canine sprite with red glowing eyes, 40 HP)
- Two-roll combat: 70% hit + 50% chance to apply Bleeding(1) on hit
- WolfSpawner — triggers at Clock.darkness_factor()≥0.8 with 1-in-game-day
  cooldown, packs of 1–2 at random map-edge cluster
- World.wolves registry + register_wolf/unregister_wolf

Integration: world.tscn load_steps 15→17 with DoctorProvider + WolfSpawner
nodes. world.gd registers doctor at top of provider list (priority 9 >
sleep 8 > eat 7 > construction 6 > chop≈plant 5 > mine≈craft 4 > haul 3
> rest 0). Middle bed at (47,24) marked is_medical=true.

MCP runtime verified: Bram took 75 dmg + Bleeding(2) → Downed (hp 25) →
Edda + Cora both volunteered doctor job → walked to patient → carried to
medical bed → treated → Bram healed to 94.2 hp, statuses cleared, back to
work. Wolf raid at day 3 22:00 fired; 4 wolves alive across raid cycles
by day 4 01:51. Screenshots confirm red-cross medical bed and wolf
silhouettes at night.

Phase 10 deliberately partial: wolf-side combat ships, pawn-side
weapons/armor/cover/friendly-fire deferred — full chain
(wolf→bites→pawn→bleeds→doctor) awaits player weapons.
Bleed-out timer at demo value (1200) vs design value (432000 = 6 in-game
hours) — documented in status_catalog.gd for first time-balance pass.

Delegation: Agent A (status + pawn HP), Agent B (doctor + treatment),
Agent C (wolf + spawner) — all Sonnet gdscript-refactor; integration on
Opus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:11:36 +01:00

59 lines
2.2 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 will add: wet(), cold().
## Phase 17 will add: sick(), infected().
##
## Usage pattern:
## pawn.add_status(StatusCatalog.bleeding(2))
##
## docs/design.md "Health & status effects"; docs/design.md "Downed & death".
## 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