class_name EventCatalog ## Phase 15 — static registry that constructs all 25 EventDefs from the ## 25-prompt corpus (docs/design.md §"Prompt corpus") and registers them ## with the Storyteller autoload at world boot. ## ## Usage (in world.gd _ready, after systems are up): ## EventCatalog.register_all(Storyteller) ## ## Design decisions (see memory.md §"Decisions"): ## - English copy is inlined in each factory AND mirrored as Strings keys so ## 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. ## ## The 25-event count is the Phase 15 gate. Do not commit with != 25. ## Construct all 25 EventDefs and register them with `storyteller_node`. ## Called once from world._ready(), after World systems are mounted. static func register_all(storyteller_node: Node) -> void: var defs: Array = [ # ── Nudges (1-4) ────────────────────────────────────────────────── _event_01_first_beds(), _event_02_empty_larder(), _event_03_no_fire(), _event_04_walls(), # ── Seasonal (5-8) ──────────────────────────────────────────────── _event_05_spring_awakens(), _event_06_summers_heat(), _event_07_autumns_harvest(), _event_08_winters_edge(), # ── Wanderers (9-12) ────────────────────────────────────────────── _event_09_a_traveler(), _event_10_the_refugee_family(), _event_11_the_old_soldier(), _event_12_the_wandering_healer(), # ── Threats (13-16) ─────────────────────────────────────────────── _event_13_wolves_at_the_edge(), _event_14_lone_wolf(), _event_15_pack_hunt(), _event_16_bandit_scouts(), # ── Disease (17-19) ─────────────────────────────────────────────── _event_17_fever(), _event_18_a_bad_cut(), _event_19_the_sleeplessness(), # ── Resource (20-22) ────────────────────────────────────────────── _event_20_bountiful_harvest(), _event_21_lumberjacks_luck(), _event_22_veins_of_iron(), # ── Lore (23-24) ────────────────────────────────────────────────── _event_23_strange_stones(), _event_24_an_old_map(), # ── Milestone (25) ──────────────────────────────────────────────── _event_25_one_year_survived(), ] for def in defs: storyteller_node.register_event(def) Audit.log("storyteller", "%d events registered" % defs.size()) # ── Nudge factories ───────────────────────────────────────────────────────── static func _event_01_first_beds() -> EventDef: var d := EventDef.new() d.id = &"first_beds" d.title = "First Beds" d.body = "Your settlers slept on the cold ground again. They are starting to ache." d.category = EventDef.Category.NUDGE d.display = EventDef.Display.BANNER d.cooldown_days = 3 d.base_weight = 3.0 # high weight — active whenever trigger is true d.auto_pause = false # Trigger: day 2+, fewer beds than pawns. d.trigger_predicate = func() -> bool: return Clock.current_day() >= 2 and World.beds.size() < World.pawns.size() d.on_resolve = func(_c: int) -> void: pass # nudge — dismiss only, no mechanical effect return d static func _event_02_empty_larder() -> EventDef: var d := EventDef.new() d.id = &"empty_larder" d.title = "Empty Larder" d.body = "The larder is bare. Spring won't last forever." d.category = EventDef.Category.NUDGE d.display = EventDef.Display.BANNER d.cooldown_days = 3 d.base_weight = 2.0 d.auto_pause = false # Trigger: day 3+, no crops in the world (proxy for no farm zone). d.trigger_predicate = func() -> bool: return Clock.current_day() >= 3 and World.crops.is_empty() d.on_resolve = func(_c: int) -> void: pass return d static func _event_03_no_fire() -> EventDef: var d := EventDef.new() d.id = &"no_fire" d.title = "No Fire" d.body = "Without a hearth, the cold will bite by night." d.category = EventDef.Category.NUDGE d.display = EventDef.Display.BANNER d.cooldown_days = 3 d.base_weight = 2.0 d.auto_pause = false # Trigger: day 4+, no light sources (proxy for no hearth/torch). d.trigger_predicate = func() -> bool: return Clock.current_day() >= 4 and World.light_sources.is_empty() d.on_resolve = func(_c: int) -> void: pass return d static func _event_04_walls() -> EventDef: var d := EventDef.new() d.id = &"walls" d.title = "Walls?" d.body = "Sleeping under stars is romantic until the wolves arrive." d.category = EventDef.Category.NUDGE d.display = EventDef.Display.BANNER d.cooldown_days = 4 d.base_weight = 2.0 d.auto_pause = false # Trigger: day 5+, build queue has no wall entities and no completed walls. # Proxy: no doors means no enclosed building. d.trigger_predicate = func() -> bool: return Clock.current_day() >= 5 and World.doors.is_empty() d.on_resolve = func(_c: int) -> void: pass return d # ── Seasonal factories ────────────────────────────────────────────────────── static func _event_05_spring_awakens() -> EventDef: var d := EventDef.new() d.id = &"spring_awakens" d.title = "Spring Awakens" d.body = "The thaw runs in every stream. Crops will grow fast now." d.category = EventDef.Category.SEASONAL d.display = EventDef.Display.BANNER d.cooldown_days = 12 d.base_weight = 1.0 d.auto_pause = false # 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. d.on_resolve = func(_c: int) -> void: Audit.log("storyteller", "spring_awakens resolved — crop growth buff stubbed (Phase 17)") return d static func _event_06_summers_heat() -> EventDef: var d := EventDef.new() d.id = &"summers_heat" d.title = "Summer's Heat" d.body = "The sun beats down. Unsheltered work will tire faster." d.category = EventDef.Category.SEASONAL d.display = EventDef.Display.BANNER d.cooldown_days = 12 d.base_weight = 1.0 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. d.on_resolve = func(_c: int) -> void: Audit.log("storyteller", "summers_heat resolved — outdoor fatigue buff stubbed (Phase 17)") return d static func _event_07_autumns_harvest() -> EventDef: var d := EventDef.new() d.id = &"autumns_harvest" d.title = "Autumn's Harvest" d.body = "The fields are heavy with the last of the year's bounty." d.category = EventDef.Category.SEASONAL d.display = EventDef.Display.BANNER d.cooldown_days = 12 d.base_weight = 1.0 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. d.on_resolve = func(_c: int) -> void: Audit.log("storyteller", "autumns_harvest resolved — yield buff stubbed (Phase 17)") return d static func _event_08_winters_edge() -> EventDef: var d := EventDef.new() d.id = &"winters_edge" d.title = "Winter's Edge" d.body = "Frost has come. The road is closed; you are alone." d.category = EventDef.Category.SEASONAL d.display = EventDef.Display.BANNER d.cooldown_days = 12 d.base_weight = 1.0 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. d.on_resolve = func(_c: int) -> void: Audit.log("storyteller", "winters_edge resolved — wanderer suppress + threat weight stubbed (Phase 17)") return d # ── Wanderer factories ────────────────────────────────────────────────────── static func _event_09_a_traveler() -> EventDef: var d := EventDef.new() d.id = &"a_traveler" d.title = "A Traveler" d.body = "A weary traveler stumbles toward your gate. They look hungry. Will you welcome them?" d.category = EventDef.Category.WANDERER d.display = EventDef.Display.MODAL d.cooldown_days = 5 d.base_weight = 1.0 d.choices = ["Welcome", "Send away"] d.auto_pause = true # Trigger: ghost state (all pawns gone) OR ~8 days have passed. d.trigger_predicate = func() -> bool: return Storyteller.ghost_state or Clock.current_day() >= 8 # Effect: Welcome → +1 pawn (stub); Send away → no effect. d.on_resolve = func(c: int) -> void: if c == 0: _spawn_wanderer_pawn({}) return d static func _event_10_the_refugee_family() -> EventDef: var d := EventDef.new() d.id = &"the_refugee_family" d.title = "The Refugee Family" d.body = "A family fleeing bandits arrives. They have nothing, but they would work hard." d.category = EventDef.Category.WANDERER d.display = EventDef.Display.MODAL d.cooldown_days = 8 d.base_weight = 1.0 d.choices = ["Welcome", "Refuse"] d.auto_pause = true # Trigger: post-day-15. d.trigger_predicate = func() -> bool: return Clock.current_day() >= 15 # Effect: Welcome → +2 pawns low skills (stub); Refuse → −2 mood colony 1 day (stub). d.on_resolve = func(c: int) -> void: if c == 0: _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)") return d static func _event_11_the_old_soldier() -> EventDef: var d := EventDef.new() d.id = &"the_old_soldier" d.title = "The Old Soldier" d.body = "A retired soldier offers his blade for a place by your fire. Combat 8, but old and tired." d.category = EventDef.Category.WANDERER d.display = EventDef.Display.MODAL d.cooldown_days = 8 d.base_weight = 1.0 d.choices = ["Welcome", "Refuse"] d.auto_pause = true # Trigger: post-day-20. d.trigger_predicate = func() -> bool: return Clock.current_day() >= 20 d.on_resolve = func(c: int) -> void: if c == 0: _spawn_wanderer_pawn({"combat": 8}) return d static func _event_12_the_wandering_healer() -> EventDef: var d := EventDef.new() d.id = &"the_wandering_healer" d.title = "The Wandering Healer" d.body = "A traveling healer asks for shelter. She brings knowledge of medicine." d.category = EventDef.Category.WANDERER d.display = EventDef.Display.MODAL d.cooldown_days = 8 d.base_weight = 1.0 d.choices = ["Welcome", "Refuse"] d.auto_pause = true # Trigger: any pawn has a status (proxy for sick/injured — exact sick() status ships Phase 17). d.trigger_predicate = func() -> bool: for p in World.pawns: if not p.statuses.is_empty(): return true return false d.on_resolve = func(c: int) -> void: if c == 0: _spawn_wanderer_pawn({"medicine": 6}) return d # ── Threat factories ───────────────────────────────────────────────────────── static func _event_13_wolves_at_the_edge() -> EventDef: var d := EventDef.new() d.id = &"wolves_at_the_edge" d.title = "Wolves at the Edge" d.body = "Wolves howl in the distance. They will be here by nightfall." d.category = EventDef.Category.THREAT d.display = EventDef.Display.MODAL d.cooldown_days = 3 d.base_weight = 1.0 d.choices = ["Prepare"] d.auto_pause = true # 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). d.on_resolve = func(_c: int) -> void: _spawn_wolves(randi_range(1, 3)) return d static func _event_14_lone_wolf() -> EventDef: var d := EventDef.new() d.id = &"lone_wolf" d.title = "Lone Wolf" d.body = "A starving wolf circles your livestock." d.category = EventDef.Category.THREAT d.display = EventDef.Display.MODAL d.cooldown_days = 3 d.base_weight = 1.5 # slightly higher — low-threat random, more common d.choices = ["Prepare"] d.auto_pause = true # Trigger: anytime (low-threat random). d.trigger_predicate = func() -> bool: return true d.on_resolve = func(_c: int) -> void: _spawn_wolves(1) return d static func _event_15_pack_hunt() -> EventDef: var d := EventDef.new() d.id = &"pack_hunt" d.title = "Pack Hunt" d.body = "A hunting pack moves through the forest. They smell your colony." d.category = EventDef.Category.THREAT d.display = EventDef.Display.MODAL d.cooldown_days = 5 d.base_weight = 1.0 d.choices = ["Prepare"] d.auto_pause = true # Trigger: post-day-30. d.trigger_predicate = func() -> bool: return Clock.current_day() >= 30 d.on_resolve = func(_c: int) -> void: _spawn_wolves(randi_range(4, 6)) return d static func _event_16_bandit_scouts() -> EventDef: var d := EventDef.new() d.id = &"bandit_scouts" d.title = "Bandit Scouts" d.body = "Strange figures watched from the treeline at dusk. Bandits, perhaps." d.category = EventDef.Category.THREAT d.display = EventDef.Display.MODAL d.cooldown_days = 5 d.base_weight = 1.0 d.choices = ["Stay alert"] d.auto_pause = true # Trigger: post-day-25. d.trigger_predicate = func() -> bool: return Clock.current_day() >= 25 # Effect: flavor + threat-likelihood raise — stubbed (v2 raids). d.on_resolve = func(_c: int) -> void: Audit.log("storyteller", "bandit_scouts resolved — threat-likelihood raise stubbed (v2 raids)") return d # ── Disease factories ──────────────────────────────────────────────────────── static func _event_17_fever() -> EventDef: var d := EventDef.new() d.id = &"fever" d.title = "Fever" d.body = "%pawn% woke with a fever. The sickness may spread." d.category = EventDef.Category.DISEASE d.display = EventDef.Display.MODAL d.cooldown_days = 4 d.base_weight = 1.0 d.choices = ["Treat them"] d.auto_pause = true # 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. d.on_resolve = func(_c: int) -> void: Audit.log("storyteller", "fever resolved — StatusCatalog.sick() stubbed (Phase 17)") return d static func _event_18_a_bad_cut() -> EventDef: var d := EventDef.new() d.id = &"a_bad_cut" d.title = "A Bad Cut" d.body = "%pawn% gashed their hand chopping wood. The wound looks deep." d.category = EventDef.Category.DISEASE d.display = EventDef.Display.MODAL d.cooldown_days = 4 d.base_weight = 1.0 d.choices = ["Treat them"] d.auto_pause = true # Trigger: any tree chopped and low chance (proxy: trees exist and day >= 5). d.trigger_predicate = func() -> bool: return Clock.current_day() >= 5 and not World.trees.is_empty() # Effect: Bleeding status on a random pawn — real-wired (StatusCatalog.bleeding exists). d.on_resolve = func(_c: int) -> void: _apply_pawn_status(func() -> Status: return StatusCatalog.bleeding(1)) return d static func _event_19_the_sleeplessness() -> EventDef: var d := EventDef.new() d.id = &"the_sleeplessness" d.title = "The Sleeplessness" d.body = "%pawn% has barely slept. Something weighs on them." d.category = EventDef.Category.DISEASE d.display = EventDef.Display.BANNER d.cooldown_days = 4 d.base_weight = 1.0 d.auto_pause = false # Trigger: any pawn is tired (has the tired thought/status). d.trigger_predicate = func() -> bool: for p in World.pawns: if p.is_tired(): return true return false # Effect: mood penalty 2 days — apply tired thought colony-wide (stubbed intensity). d.on_resolve = func(_c: int) -> void: _apply_colony_mood_thought(func() -> Thought: return ThoughtCatalog.tired()) return d # ── Resource factories ─────────────────────────────────────────────────────── static func _event_20_bountiful_harvest() -> EventDef: var d := EventDef.new() d.id = &"bountiful_harvest" d.title = "Bountiful Harvest" d.body = "Your fields exceeded the season. The granary swells." d.category = EventDef.Category.RESOURCE d.display = EventDef.Display.BANNER d.cooldown_days = 3 d.base_weight = 1.0 d.auto_pause = false # 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. d.on_resolve = func(_c: int) -> void: _apply_buff_next_n_jobs(&"harvest", 10, 1.25) return d static func _event_21_lumberjacks_luck() -> EventDef: var d := EventDef.new() d.id = &"lumberjacks_luck" d.title = "Lumberjack's Luck" d.body = "%pawn% found a copse of unusually thick trees." d.category = EventDef.Category.RESOURCE d.display = EventDef.Display.BANNER d.cooldown_days = 3 d.base_weight = 1.0 d.auto_pause = false # Trigger: trees present (chop job context). d.trigger_predicate = func() -> bool: return not World.trees.is_empty() # Effect: next 3 trees +50% wood — stubbed. d.on_resolve = func(_c: int) -> void: _apply_buff_next_n_jobs(&"chop", 3, 1.5) return d static func _event_22_veins_of_iron() -> EventDef: var d := EventDef.new() d.id = &"veins_of_iron" d.title = "Veins of Iron" d.body = "A miner reports a rich vein, deeper than expected." d.category = EventDef.Category.RESOURCE d.display = EventDef.Display.BANNER d.cooldown_days = 3 d.base_weight = 1.0 d.auto_pause = false # Trigger: rocks present (mine job context). d.trigger_predicate = func() -> bool: return not World.rocks.is_empty() # Effect: next mining yield ×2 — stubbed. d.on_resolve = func(_c: int) -> void: _apply_buff_next_n_jobs(&"mine", 1, 2.0) return d # ── Lore factories ─────────────────────────────────────────────────────────── static func _event_23_strange_stones() -> EventDef: var d := EventDef.new() d.id = &"strange_stones" d.title = "Strange Stones" d.body = "Settlers report finding carved stones in the wood — older than any memory." d.category = EventDef.Category.LORE d.display = EventDef.Display.BANNER d.cooldown_days = 6 d.base_weight = 1.0 d.auto_pause = false # Trigger: post-day-15, random. d.trigger_predicate = func() -> bool: return Clock.current_day() >= 15 d.on_resolve = func(_c: int) -> void: pass # flavor only return d static func _event_24_an_old_map() -> EventDef: var d := EventDef.new() d.id = &"an_old_map" d.title = "An Old Map" d.body = "%pawn% found a tattered map. Roads to the north, half-faded." d.category = EventDef.Category.LORE d.display = EventDef.Display.BANNER d.cooldown_days = 6 d.base_weight = 1.0 d.auto_pause = false # Trigger: post-recruit (more than 1 pawn present). d.trigger_predicate = func() -> bool: return World.pawns.size() > 1 d.on_resolve = func(_c: int) -> void: pass # flavor; seeds v2 trade/exploration return d # ── Milestone factory ──────────────────────────────────────────────────────── static func _event_25_one_year_survived() -> EventDef: var d := EventDef.new() d.id = &"one_year_survived" d.title = "One Year Survived" d.body = "A full year. The first frost feels different now — yours is a real settlement." d.category = EventDef.Category.MILESTONE d.display = EventDef.Display.BANNER d.cooldown_days = 30 d.base_weight = 1.0 d.auto_pause = false # 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. 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. return d # ── Effect helpers ─────────────────────────────────────────────────────────── ## 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. static func _spawn_wanderer_pawn(skills: Dictionary = {}) -> void: Audit.log("storyteller", "_spawn_wanderer_pawn called with skills=%s — stubbed (Phase 17)" % 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. 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]) ## Emit EventBus.request_wolf_spawn so WolfSpawner (scene child) picks it up. ## WolfSpawner listens on that signal (Phase 10). This is the correct bus-based ## wiring — no get_node("/root/...") smell. static func _spawn_wolves(count: int) -> void: if EventBus.has_signal("request_wolf_spawn"): EventBus.emit_signal("request_wolf_spawn", count) else: Audit.log("storyteller", "_spawn_wolves: EventBus.request_wolf_spawn not declared — stubbed (Phase 17)") ## 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. static func _apply_pawn_status(status_factory: Callable, pawn = null) -> void: var target = pawn if target == null: if World.pawns.is_empty(): Audit.log("storyteller", "_apply_pawn_status: no pawns present") return target = World.pawns[randi() % World.pawns.size()] if target.has_method("add_status"): target.add_status(status_factory.call()) else: Audit.log("storyteller", "_apply_pawn_status: target missing add_status()") ## Apply a thought to every living pawn in the colony. ## `thought_factory` is a Callable that returns a fresh Thought each time. ## REAL — ThoughtCatalog thoughts and Pawn.add_thought() both exist. static func _apply_colony_mood_thought(thought_factory: Callable) -> void: for p in World.pawns: if p.has_method("add_thought"): p.add_thought(thought_factory.call())