Three-agent fan-out reusing the contracts-first pattern: Opus pre-wrote
EventDef class + 5 EventBus signals + Storyteller autoload stub before
dispatch. Pattern proven across Phases 12/13/14/15.
EventDef + 25-event corpus (Agent A):
- scenes/storyteller/event_def.gd — data class with id/title/body/
category/display/cooldown_days/base_weight/choices/auto_pause/
focus_tile/trigger_predicate/on_resolve
- scenes/storyteller/event_catalog.gd — class_name EventCatalog with
register_all() dispatcher + 25 _event_NN() static factories covering
all 8 categories (nudge×4, seasonal×4, wanderer×4, threat×4, disease×3,
resource×3, lore×2, milestone×1)
- Strings catalog: 50 keys added (event.<id>.title + event.<id>.body)
+ ui.go_there / ui.dismiss for UI buttons
- on_resolve effects: real-wired for a_bad_cut (StatusCatalog.bleeding),
one_year_survived + refugee_family + sleeplessness (colony mood thoughts);
stubbed-with-log for wanderer spawns (Phase 17 recruit UI), resource
buffs (Phase 17 work-buff system), wolf spawn (EventBus signal pending),
fever (StatusCatalog.sick pending), seasonal effects
Storyteller real implementation (Agent B):
- autoload/storyteller.gd — replaced stub with full logic:
* Daily 6 AM roll via Clock.phase_changed(&dawn), one-per-day guard
* Per-event cooldown via _event_last_fired Dict; per-category via
_category_last_fired Dict + CATEGORY_COOLDOWN_DAYS (nudge=2,
seasonal=12, wanderer=5, threat=3, disease=4, resource=3, lore=6,
milestone=30) — both gates must pass
* Tension model: 0..100, −3/roll decay, +15 on THREAT fire (net +12)
Category multipliers: THREAT = lerp(2.0, 0.3, t/100),
RESOURCE = lerp(0.5, 1.5, t/100), others = 1.0
* State-trigger 3× weight boost when predicate currently true
* Auto-pause Sim before showing UI for auto_pause events
* Ghost state: _on_pawn_died flips on World.pawns empty,
_ghost_wanderer_target_day = today + randi_range(3, 5),
daily roll bypasses pool and force-fires WANDERER (prefers a_traveler)
* Full save/load round-trip incl. cooldown dicts (StringName↔String)
Banner + Modal UI (Agent C):
- scenes/ui/storyteller_banner.gd — class_name StorytellerBanner extends
CanvasLayer (layer 15), top-center under top-bar, 6-sec auto-dismiss
Timer, tap-to-dismiss-early, internal queue for back-to-back events
- scenes/ui/storyteller_modal.gd — class_name StorytellerModal extends
CanvasLayer (layer 20), center PanelContainer, full-screen 0.45 dim
ColorRect, 0/1/2 choice button layouts
- camera_rig.gd: pan_to_tile(tile) public helper using existing
_centre_on tween slot
- Both UI scenes runtime-instantiated in main.gd as CanvasLayer children
(no .tscn edit needed)
- %pawn% substitution at display time (World.pawns[0].pawn_name fallback)
Modal auto-hide-on-resolve fix (Opus mid-flight):
- Original Agent C modal only hid on internal button click. Added
EventBus.storyteller_event_resolved subscriber → _set_visible(false)
so external resolve_current calls (test scripts, ghost-state auto-fire)
also dismiss the dialog.
MCP runtime verified across two boots:
- Boot 1: day 0 roll → lone_wolf THREAT, modal 'A starving wolf circles
your livestock.' with Prepare/Dismiss + auto-pause (tick 1 frozen).
Resolve → tension 27→42, sim resumed.
- Boot 2: day 0 roll → an_old_map LORE, top-center banner, non-blocking.
Banner path + modal path both visually confirmed.
Deferred to Phase 17 polish:
- EventBus.request_wolf_spawn signal — wolf-spawn effects log-stub today
- Wanderer recruit UI (modal currently dismisses, pawn add deferred)
- Resource buff system (next-N-jobs multipliers)
- 3+ choice modals (current UI renders first 2)
- .tres event resources (currently code-as-data factories)
Delegation: 3× gdscript-refactor (Sonnet) agents in parallel;
modal-hide fix on Opus; integration + MCP verify on Opus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
6.7 KiB
GDScript
125 lines
6.7 KiB
GDScript
extends Node
|
||
## i18n string table — player-visible strings ONLY. Code keys, EN values.
|
||
##
|
||
## Locked from day one (per CLAUDE.md): no hardcoded display copy in scenes or
|
||
## scripts. If you have player-facing text, add a key here and call Strings.t(key).
|
||
##
|
||
## Locale switching is post-MVP; the indirection lands now so we don't have to
|
||
## retrofit the whole game later. When the table grows, move it to a .tres or
|
||
## external CSV import; the public API (`Strings.t(key)`) stays the same.
|
||
|
||
const TABLE: Dictionary = {
|
||
# Phase 0 placeholder — populate as features land.
|
||
&"app.title": "Rimlike",
|
||
&"smoke.hello": "Phase 0 — autoloads online.",
|
||
# Speed controls (top bar)
|
||
&"speed.pause": "‖",
|
||
&"speed.normal": "1×",
|
||
&"speed.fast": "5×",
|
||
&"speed.ultra": "12×",
|
||
# HUD
|
||
&"hud.tick": "Tick: {n}",
|
||
# Phase 11 — in-game clock display ("{d}" = day, "{t}" = "HH:MM")
|
||
&"clock.format": "Day {d}, {t}",
|
||
# Phase 12 — season indicator ("{s}" = season name, "{d}" = 1-indexed day, "{total}" = days per season)
|
||
&"season.spring": "Spring",
|
||
&"season.summer": "Summer",
|
||
&"season.autumn": "Autumn",
|
||
&"season.winter": "Winter",
|
||
&"season.format": "{s} {d}/12",
|
||
# Pawn state labels
|
||
&"pawn.state.idle": "idle",
|
||
&"pawn.state.walking": "walking",
|
||
# Item types (player-visible this phase)
|
||
&"item.wood": "Wood",
|
||
&"item.stone": "Stone",
|
||
&"item.iron_ore": "Iron ore",
|
||
# Item stack count badge ("{n}" is substituted at call site via .format())
|
||
&"item.stack_count": "×{n}",
|
||
# Phase 6 — new item types (carpenter bench + smelter outputs)
|
||
&"item.plank": "Plank",
|
||
&"item.stone_block": "Stone block",
|
||
# Phase 7 — food loop and cooking chain item types
|
||
&"item.flour": "Flour",
|
||
&"item.bread": "Bread",
|
||
&"item.meal": "Meal",
|
||
# Phase 7 — cooking workbench labels
|
||
&"workbench.hearth": "Hearth",
|
||
&"workbench.millstone": "Millstone",
|
||
# Phase 7 — pawn hunger states
|
||
&"pawn.state.eating": "eating",
|
||
&"pawn.state.hungry": "hungry",
|
||
# Phase 11 — mood thoughts (player-visible in pawn-detail, Phase 17)
|
||
&"thought.in_darkness": "In darkness",
|
||
# Phase 6 — quality tier labels
|
||
&"quality.shoddy": "Shoddy",
|
||
&"quality.normal": "Normal",
|
||
&"quality.excellent": "Excellent",
|
||
&"quality.masterwork": "Masterwork",
|
||
&"quality.legendary": "Legendary",
|
||
# Phase 15 — Storyteller UI buttons
|
||
&"ui.go_there": "Go there",
|
||
&"ui.dismiss": "Dismiss",
|
||
# Phase 15 — Storyteller event titles + bodies (25-event corpus).
|
||
# EventDef factories in EventCatalog carry the English string directly;
|
||
# these keys exist so Strings.t() is the indirection point for future locale
|
||
# switching. When a locale ships, swap EventDef.title/body from these keys
|
||
# instead of touching EventCatalog factories. (%pawn% is substituted by UI.)
|
||
&"event.first_beds.title": "First Beds",
|
||
&"event.first_beds.body": "Your settlers slept on the cold ground again. They are starting to ache.",
|
||
&"event.empty_larder.title": "Empty Larder",
|
||
&"event.empty_larder.body": "The larder is bare. Spring won't last forever.",
|
||
&"event.no_fire.title": "No Fire",
|
||
&"event.no_fire.body": "Without a hearth, the cold will bite by night.",
|
||
&"event.walls.title": "Walls?",
|
||
&"event.walls.body": "Sleeping under stars is romantic until the wolves arrive.",
|
||
&"event.spring_awakens.title": "Spring Awakens",
|
||
&"event.spring_awakens.body": "The thaw runs in every stream. Crops will grow fast now.",
|
||
&"event.summers_heat.title": "Summer's Heat",
|
||
&"event.summers_heat.body": "The sun beats down. Unsheltered work will tire faster.",
|
||
&"event.autumns_harvest.title": "Autumn's Harvest",
|
||
&"event.autumns_harvest.body": "The fields are heavy with the last of the year's bounty.",
|
||
&"event.winters_edge.title": "Winter's Edge",
|
||
&"event.winters_edge.body": "Frost has come. The road is closed; you are alone.",
|
||
&"event.a_traveler.title": "A Traveler",
|
||
&"event.a_traveler.body": "A weary traveler stumbles toward your gate. They look hungry. Will you welcome them?",
|
||
&"event.the_refugee_family.title": "The Refugee Family",
|
||
&"event.the_refugee_family.body": "A family fleeing bandits arrives. They have nothing, but they would work hard.",
|
||
&"event.the_old_soldier.title": "The Old Soldier",
|
||
&"event.the_old_soldier.body": "A retired soldier offers his blade for a place by your fire. Combat 8, but old and tired.",
|
||
&"event.the_wandering_healer.title": "The Wandering Healer",
|
||
&"event.the_wandering_healer.body": "A traveling healer asks for shelter. She brings knowledge of medicine.",
|
||
&"event.wolves_at_the_edge.title": "Wolves at the Edge",
|
||
&"event.wolves_at_the_edge.body": "Wolves howl in the distance. They will be here by nightfall.",
|
||
&"event.lone_wolf.title": "Lone Wolf",
|
||
&"event.lone_wolf.body": "A starving wolf circles your livestock.",
|
||
&"event.pack_hunt.title": "Pack Hunt",
|
||
&"event.pack_hunt.body": "A hunting pack moves through the forest. They smell your colony.",
|
||
&"event.bandit_scouts.title": "Bandit Scouts",
|
||
&"event.bandit_scouts.body": "Strange figures watched from the treeline at dusk. Bandits, perhaps.",
|
||
&"event.fever.title": "Fever",
|
||
&"event.fever.body": "%pawn% woke with a fever. The sickness may spread.",
|
||
&"event.a_bad_cut.title": "A Bad Cut",
|
||
&"event.a_bad_cut.body": "%pawn% gashed their hand chopping wood. The wound looks deep.",
|
||
&"event.the_sleeplessness.title": "The Sleeplessness",
|
||
&"event.the_sleeplessness.body": "%pawn% has barely slept. Something weighs on them.",
|
||
&"event.bountiful_harvest.title": "Bountiful Harvest",
|
||
&"event.bountiful_harvest.body": "Your fields exceeded the season. The granary swells.",
|
||
&"event.lumberjacks_luck.title": "Lumberjack's Luck",
|
||
&"event.lumberjacks_luck.body": "%pawn% found a copse of unusually thick trees.",
|
||
&"event.veins_of_iron.title": "Veins of Iron",
|
||
&"event.veins_of_iron.body": "A miner reports a rich vein, deeper than expected.",
|
||
&"event.strange_stones.title": "Strange Stones",
|
||
&"event.strange_stones.body": "Settlers report finding carved stones in the wood — older than any memory.",
|
||
&"event.an_old_map.title": "An Old Map",
|
||
&"event.an_old_map.body": "%pawn% found a tattered map. Roads to the north, half-faded.",
|
||
&"event.one_year_survived.title": "One Year Survived",
|
||
&"event.one_year_survived.body": "A full year. The first frost feels different now — yours is a real settlement.",
|
||
}
|
||
|
||
|
||
func t(key: StringName) -> String:
|
||
if TABLE.has(key):
|
||
return TABLE[key]
|
||
push_warning("Strings.t(): missing key %s" % key)
|
||
return String(key)
|