Add "Update from scenario" to pull scenario edits into a running adventure
An adventure copies its scenario's plot text and story cards at creation so
later authoring never disturbs a story in progress. This is the explicit
opt-out, alongside the existing per-script "Sync from library".
GET /adventures/{id}/refresh returns a plan (per-field old/new diff, card
add/update/remove, world-state added/removed paths, and any ${...} answers
still needed); POST applies it under the turn lock so it can't race a
generating turn. The Plot panel shows the plan in a confirm modal first.
Overwrites the plot fields and scenario-derived cards. Deliberately left
alone: the opening `start` action (the story is built on it, and it is baked
into memories and the summary), the adventure's own title and summary,
player-authored story cards, and the live value of every stat the schema
still defines.
Two enablers were needed:
- adventures.placeholders (migration 32). ${...} answers were used once at
creation and discarded, so re-copying scenario text would have re-injected
a literal ${Hero}. Adventures predating the column re-prompt once via the
existing modal, then the answers are saved.
- story_cards.source_ref (migration 33), "card:<id>" / "npc:<key>", NULL for
player-authored. Adventure cards had no link back to their source, so a
rename read as delete-plus-add and player cards would have been clobbered.
Legacy cards name-match once, then adopt the ref.
World state goes through a new worldstate.reconcile(): keep values the schema
still defines, add missing ones at their initial, drop removed ones and their
cooldown bookkeeping. Not instantiate(), which would heal the player to full
and wipe their milestones.
The confirm modal is portalled to <body>: .side-panel's panel-in animation
has fill mode `both`, which makes it the containing block for position:fixed
descendants, so an overlay rendered in place was trapped in the 420px panel
and clipped by its overflow.
14 new tests in backend/tests/test_scenario_refresh.py; 85 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzBCyXH4hBEVHqEaertcq4
This commit is contained in:
co-authored by
Claude Opus 5
parent
a170f5aed5
commit
aed28d9295
@@ -149,6 +149,31 @@ class AdventureUpdate(BaseModel):
|
||||
memory_bank_enabled: bool | None = None
|
||||
|
||||
|
||||
class AdventureRefresh(BaseModel):
|
||||
"""Body for "Update from scenario". `placeholders` supplies answers the
|
||||
adventure has no stored value for (see AdventureCreate.placeholders); they
|
||||
are merged over the stored ones and saved."""
|
||||
|
||||
placeholders: dict[str, str] = {}
|
||||
|
||||
|
||||
class RefreshPlan(BaseModel):
|
||||
"""What a refresh would change — drives the confirm dialog."""
|
||||
|
||||
scenario_id: int
|
||||
scenario_title: str
|
||||
has_changes: bool
|
||||
# field name -> {"old": ..., "new": ...}, only for fields that differ.
|
||||
fields: dict[str, dict] = {}
|
||||
# {"added"|"updated"|"removed": [card name, ...]}
|
||||
cards: dict[str, list[str]] = {}
|
||||
# {"added"|"removed": [stat path, ...]} — live values are otherwise kept.
|
||||
world_state: dict[str, list[str]] = {}
|
||||
# ${Placeholder} names the scenario asks for that the adventure has no
|
||||
# stored answer to; the client must collect these and send them back.
|
||||
placeholders_needed: list[str] = []
|
||||
|
||||
|
||||
class ActionOut(ORMModel):
|
||||
id: int
|
||||
adventure_id: int
|
||||
|
||||
Reference in New Issue
Block a user