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>
This commit is contained in:
megaproxy 2026-05-16 18:06:55 +01:00
parent 00cf8f445d
commit d9638a4ea4
19 changed files with 711 additions and 101 deletions

View file

@ -11,14 +11,12 @@ class_name EventCatalog
## the i18n hook (`Strings.t(key)`) exists today without requiring it.
## EventDef carries the rendered English string; locale switching can swap
## it via the string table later without touching EventDef or this catalog.
## - Effect helpers below are stubbed with Audit.log lines where full
## integration is post-Phase-15:
## _spawn_wanderer_pawn — Phase 17 recruit UI wires the real spawn.
## _apply_buff_next_n_jobs — Phase 17 work-buff system.
## _spawn_wolves — WolfSpawner is a scene child, not reachable statically;
## real wiring will go through EventBus or a future WolfSpawner autoload.
## _apply_pawn_status (sick) — StatusCatalog.sick() ships Phase 17.
## All real-wired effects are noted per-event below.
## - Effect helpers (Phase 17 — all wired):
## _spawn_wanderer_pawn — emits EventBus.request_pawn_spawn; World scene handles.
## _apply_buff_next_n_jobs — registers a timed yield buff on Storyteller.
## _spawn_wolves — emits EventBus.request_wolf_spawn; WolfSpawner handles.
## _apply_pawn_status — applies a status to a random pawn.
## _apply_colony_mood_thought — applies a thought to all living pawns.
##
## The 25-event count is the Phase 15 gate. Do not commit with != 25.
@ -156,9 +154,10 @@ static func _event_05_spring_awakens() -> EventDef:
# Trigger: first tick of spring. Storyteller fires this once per season boundary.
d.trigger_predicate = func() -> bool:
return Clock.current_season() == Clock.SEASON_SPRING
# Effect: stubbed — Phase 17 will apply +10% crop growth for the season.
# Effect: +20% crop growth speed for 12 days (rest of spring season).
d.on_resolve = func(_c: int) -> void:
Audit.log("storyteller", "spring_awakens resolved — crop growth buff stubbed (Phase 17)")
Storyteller.add_buff(&"crop_growth", 1.20, 12)
Audit.log("storyteller", "spring_awakens: +20%% crop growth for 12 days")
return d
@ -174,9 +173,11 @@ static func _event_06_summers_heat() -> EventDef:
d.auto_pause = false
d.trigger_predicate = func() -> bool:
return Clock.current_season() == Clock.SEASON_SUMMER
# Effect: stubbed — Phase 17 will apply outdoor work 5% speed modifier.
# Effect: sleep-need decays 15% faster for 12 days (summer heat fatigue).
# Consumer: SleepProvider / Pawn._tick_energy checks Storyteller.get_buff_multiplier(&"sleep_decay").
d.on_resolve = func(_c: int) -> void:
Audit.log("storyteller", "summers_heat resolved — outdoor fatigue buff stubbed (Phase 17)")
Storyteller.add_buff(&"sleep_decay", 1.15, 12)
Audit.log("storyteller", "summers_heat: +15%% sleep decay for 12 days")
return d
@ -192,9 +193,11 @@ static func _event_07_autumns_harvest() -> EventDef:
d.auto_pause = false
d.trigger_predicate = func() -> bool:
return Clock.current_season() == Clock.SEASON_AUTUMN
# Effect: stubbed — Phase 17 will apply +15% harvest yield.
# Effect: +25% harvest yield for 12 days (autumn bonus).
# Consumer: Plant/HarvestToil checks Storyteller.get_buff_multiplier(&"harvest_yield").
d.on_resolve = func(_c: int) -> void:
Audit.log("storyteller", "autumns_harvest resolved — yield buff stubbed (Phase 17)")
Storyteller.add_buff(&"harvest_yield", 1.25, 12)
Audit.log("storyteller", "autumns_harvest: +25%% harvest yield for 12 days")
return d
@ -210,9 +213,12 @@ static func _event_08_winters_edge() -> EventDef:
d.auto_pause = false
d.trigger_predicate = func() -> bool:
return Clock.current_season() == Clock.SEASON_WINTER
# Effect: no wanderers 5 days + raise threat weight — both stubbed Phase 17.
# Effect: raise threat weight ×1.4 for 12 days (winter pressure); suppress
# wanderer weight ×0.2 for 5 days (roads closed).
d.on_resolve = func(_c: int) -> void:
Audit.log("storyteller", "winters_edge resolved — wanderer suppress + threat weight stubbed (Phase 17)")
Storyteller.add_buff(&"threat_weight", 1.40, 12)
Storyteller.add_buff(&"wanderer_weight", 0.20, 5)
Audit.log("storyteller", "winters_edge: threat_weight ×1.4 for 12d, wanderer_weight ×0.2 for 5d")
return d
@ -259,8 +265,7 @@ static func _event_10_the_refugee_family() -> EventDef:
_spawn_wanderer_pawn({})
_spawn_wanderer_pawn({})
else:
_apply_colony_mood_thought(func() -> Thought: return ThoughtCatalog.slept_on_floor()) # placeholder thought, Phase 17 adds colony_refused_refugees
Audit.log("storyteller", "refugee_family refused — colony mood penalty stubbed (Phase 17)")
_apply_colony_mood_thought(func() -> Thought: return ThoughtCatalog.refused_refugees())
return d
@ -323,7 +328,7 @@ static func _event_13_wolves_at_the_edge() -> EventDef:
# Trigger: anytime after day 3 (season-weighting handled by Storyteller tension).
d.trigger_predicate = func() -> bool:
return Clock.current_day() >= 3
# Effect: spawn 1-3 wolves (stubbed until WolfSpawner is accessible statically).
# Effect: spawn 1-3 wolves via EventBus.request_wolf_spawn.
d.on_resolve = func(_c: int) -> void:
_spawn_wolves(randi_range(1, 3))
return d
@ -381,9 +386,12 @@ static func _event_16_bandit_scouts() -> EventDef:
# Trigger: post-day-25.
d.trigger_predicate = func() -> bool:
return Clock.current_day() >= 25
# Effect: flavor + threat-likelihood raise — stubbed (v2 raids).
# Effect: additional threat weight ×1.3 for 3 days ("they'll be back").
# The base THREAT fire already bumps tension +15; this adds a short-window
# bias toward more threat events as a narrative follow-up.
d.on_resolve = func(_c: int) -> void:
Audit.log("storyteller", "bandit_scouts resolved — threat-likelihood raise stubbed (v2 raids)")
Storyteller.add_buff(&"threat_weight", 1.30, 3)
Audit.log("storyteller", "bandit_scouts: threat_weight ×1.3 for 3 days")
return d
@ -403,9 +411,9 @@ static func _event_17_fever() -> EventDef:
# Trigger: random after ~30 days.
d.trigger_predicate = func() -> bool:
return Clock.current_day() >= 30 and not World.pawns.is_empty()
# Effect: Sick status on random pawn — StatusCatalog.sick() ships Phase 17.
# Effect: Sick status (severity 2) on one random pawn.
d.on_resolve = func(_c: int) -> void:
Audit.log("storyteller", "fever resolved — StatusCatalog.sick() stubbed (Phase 17)")
_apply_pawn_status(func() -> Status: return StatusCatalog.sick(2))
return d
@ -445,7 +453,7 @@ static func _event_19_the_sleeplessness() -> EventDef:
if p.is_tired():
return true
return false
# Effect: mood penalty 2 days — apply tired thought colony-wide (stubbed intensity).
# Effect: apply tired thought colony-wide (reinforces existing penalty; feels like communal dread).
d.on_resolve = func(_c: int) -> void:
_apply_colony_mood_thought(func() -> Thought: return ThoughtCatalog.tired())
return d
@ -466,7 +474,7 @@ static func _event_20_bountiful_harvest() -> EventDef:
# Trigger: autumn (harvest season) and crops are present.
d.trigger_predicate = func() -> bool:
return Clock.current_season() == Clock.SEASON_AUTUMN and not World.crops.is_empty()
# Effect: +25% yield — stubbed, Phase 17 crop-yield multiplier.
# Effect: +25% harvest yield for ~5 days via Storyteller buff registry.
d.on_resolve = func(_c: int) -> void:
_apply_buff_next_n_jobs(&"harvest", 10, 1.25)
return d
@ -485,7 +493,7 @@ static func _event_21_lumberjacks_luck() -> EventDef:
# Trigger: trees present (chop job context).
d.trigger_predicate = func() -> bool:
return not World.trees.is_empty()
# Effect: next 3 trees +50% wood — stubbed.
# Effect: next ~1.5 days of chop jobs +50% wood via Storyteller buff registry.
d.on_resolve = func(_c: int) -> void:
_apply_buff_next_n_jobs(&"chop", 3, 1.5)
return d
@ -504,7 +512,7 @@ static func _event_22_veins_of_iron() -> EventDef:
# Trigger: rocks present (mine job context).
d.trigger_predicate = func() -> bool:
return not World.rocks.is_empty()
# Effect: next mining yield ×2 — stubbed.
# Effect: mining yield ×2 for ~1 day via Storyteller buff registry.
d.on_resolve = func(_c: int) -> void:
_apply_buff_next_n_jobs(&"mine", 1, 2.0)
return d
@ -563,11 +571,9 @@ static func _event_25_one_year_survived() -> EventDef:
# Trigger: end of first winter (day >= 4 seasons × 12 days = 48).
d.trigger_predicate = func() -> bool:
return Clock.current_day() >= 48 and Clock.current_season() == Clock.SEASON_WINTER
# Effect: +5 mood "We made it" colony 2 days — real-wired via colony thought.
# Effect: +6 mood "We made it through a year" colony-wide for 2 in-game days.
d.on_resolve = func(_c: int) -> void:
_apply_colony_mood_thought(func() -> Thought: return ThoughtCatalog.well_rested())
# well_rested() is the closest existing +5 mood thought; Phase 17 adds
# ThoughtCatalog.we_made_it() with correct label + longer duration.
_apply_colony_mood_thought(func() -> Thought: return ThoughtCatalog.we_made_it())
return d
@ -575,16 +581,27 @@ static func _event_25_one_year_survived() -> EventDef:
## All helpers are static. Stub bodies log via Audit where the underlying
## system is not yet available; real bodies are annotated per-helper.
## Instantiate a new pawn at a random map-edge tile with optional skill seed.
## STUB — Phase 17 recruit UI will wire the real spawn via PawnFactory or similar.
## Emit EventBus.request_pawn_spawn so the World scene instantiates a new pawn.
## Also applies the hopeful_newcomer thought colony-wide (including the new pawn
## if it is registered before the next thought tick — timing may vary).
## Phase 17 recruit UI wires additional name/trait input; this is the mechanical
## fallback that fires regardless of any future recruit dialog.
static func _spawn_wanderer_pawn(skills: Dictionary = {}) -> void:
Audit.log("storyteller", "_spawn_wanderer_pawn called with skills=%s — stubbed (Phase 17)" % str(skills))
EventBus.request_pawn_spawn.emit(skills)
_apply_colony_mood_thought(func() -> Thought: return ThoughtCatalog.hopeful_newcomer())
Audit.log("storyteller", "_spawn_wanderer_pawn: emitted request_pawn_spawn(skills=%s)" % str(skills))
## Flag the next `count` jobs of `kind` to use a yield multiplier.
## STUB — Phase 17 work-buff system will implement the ring buffer.
## Register a timed yield multiplier buff via Storyteller.add_buff().
## `kind` maps to the buff key that job runners query (e.g. &"harvest_yield",
## &"chop_yield", &"mine_yield"). `count` is used to estimate duration in days:
## each job is assumed to take ~0.5 in-game days at average pawn count, so
## duration = max(1, count / 2) days. Consumers call
## Storyteller.get_buff_multiplier(kind) when computing yield.
static func _apply_buff_next_n_jobs(kind: StringName, count: int, multiplier: float) -> void:
Audit.log("storyteller", "_apply_buff_next_n_jobs: kind=%s count=%d ×%.2f — stubbed (Phase 17)" % [kind, count, multiplier])
var duration: int = maxi(1, count / 2)
Storyteller.add_buff(kind, multiplier, duration)
Audit.log("storyteller", "_apply_buff_next_n_jobs: kind=%s ×%.2f for %d days" % [kind, multiplier, duration])
## Emit EventBus.request_wolf_spawn so WolfSpawner (scene child) picks it up.
@ -597,7 +614,7 @@ static func _spawn_wolves(count: int) -> void:
## Apply a status to a random pawn (or a specific pawn if non-null).
## The factory callable returns a fresh Status — mirrors StatusCatalog pattern.
## REAL for statuses that exist (bleeding); STUB for sick() until Phase 17.
## All statuses in StatusCatalog are now wired (bleeding, sick).
static func _apply_pawn_status(status_factory: Callable, pawn = null) -> void:
var target = pawn
if target == null: