Skip to content

ADR-035: Rename "Worlds" → "Campaigns" and "Campaigns" → "Stories"

Status: Accepted Date: 2026-04-30 Context: The product currently exposes two distinct multi-session experiences to players:

  • Campaigns — pre-authored static arcs (e.g. "Frontier Crisis"), 152 scenarios across five themes, ~$0 marginal cost per playthrough
  • Worlds — reactive AI-generated arcs (e.g. "The Inflection Point"), live-generated preambles and consequences, ~$0.50 marginal cost per playthrough

Two problems with this naming:

  1. "World" is ambiguous. It collides with how scenarios use "world" generically (the setting/context of any campaign), and it under-sells what the reactive mode actually is — a full narrative campaign that adapts to your decisions.
  2. The reactive mode is the product. Player traction and replay value sit overwhelmingly with reactive arcs. The static arcs function more as onboarding / quick play / fallback content. The headline term should belong to the headline experience.

At a glance

What it decides: Player-facing labels swap so the headline experience gets the headline word: reactive arcs are now "Campaigns" and the older static arcs are now "Stories." The arc_type discriminator values rename to match ('static' → 'story', 'reactive' → 'campaign').

  • UI + discriminator only table and type names (campaigns, campaign_arcs, Campaign) stay as generic umbrellas; routes, FKs, RLS, and migrations are untouched.
  • "Stories" beat "Arcs" chosen over the accurate-but-jargony "Arcs" because new players reading "Arc" get no signal about what it is; "Story" implies pre-authored intent.
  • Enum rename folded in to kill a foot-gun keeping arc_type = 'reactive' while it means "Campaign" in product terms forced mental translation on every query and review.
  • Static arcs kept, not removed removing the cheap free-to-serve tier was rejected — it is the natural onboarding ramp and preserves cost flexibility.
  • Watch: done now while there are ~5–6 testers and no external API consumers; URL paths unchanged so bookmarks still work.
flowchart LR
    subgraph Before
        W["Worlds (reactive arcs)"]
        C["Campaigns (static arcs)"]
    end
    subgraph After
        C2["Campaigns"]
        S["Stories"]
    end
    W --> C2
    C --> S

Label swap: "Worlds" becomes "Campaigns"; the old "Campaigns" becomes "Stories." Tables and types keep their generic names.

Decision

Swap the player-facing labels:

Was Becomes
Worlds (reactive arcs) Campaigns
Campaigns (static arcs) Stories

Also rename the arc_type discriminator values to match: 'static' → 'story', 'reactive' → 'campaign'. This eliminates the foot-gun of having a query like arc_type = 'reactive' mean "Campaign" in product terms — keeping the discriminator aligned with the surface the rest of the team talks in.

Database table names (campaigns, campaign_arcs, campaign_chapters) and TypeScript types (Campaign, CampaignArc) stay as-is — they are umbrella structures that hold both kinds of arcs. The campaigns table holds playthroughs (an instance of either kind of arc), which is generic enough to keep. Renaming it would require inventing a third term (e.g. playthroughs) and is a separate decision tracked outside this ADR.

Rationale

  • "Stories" maps to expectation. A pre-authored, on-rails experience reads as a "Story." A live-adapting one reads as a "Campaign" (D&D framing — the GM responds to the party).
  • Avoids "Arcs" jargon. Internally the team calls these "static arcs" and "reactive arcs." Surfacing "Arcs" in the UI is technically accurate but unfriendly to first-time players.
  • Avoids "Scenarios" collision. A scenario is the atomic decision unit (one round). Calling the container "Scenarios" or "Extended Scenarios" overloads a term that already has structural meaning.
  • No schema disruption. Because both arc types share tables and types, the rename touches only user-facing strings. Routes, FK constraints, RLS policies, and migrations are untouched. ~5–6 active testers and a planned invite expansion next week make this the right window.

Alternatives Considered

  • Arcs (for static). Accurate to the codebase ("static arcs"), short, and distinct from "scenario." Rejected as too jargony for new players. The team can still say "static arcs" internally; players see "Stories."
  • Quick Plays / Story Packs (for static). Friendlier but loses the structural meaning that these are multi-chapter narratives, not one-offs. "Quick Round" is already a separate solo mode.
  • Remove static arcs entirely. Discussed first. Rejected because static content is effectively free to serve and useful as onboarding / cheap-tier play. Keeping the lower-cost tier under a less prominent name preserves optionality.
  • Full DB / type / route rename. Initially scoped as a three-phase end-to-end rename. Investigation revealed the DB and code already use generic umbrella names that don't conflict with either product label. Renaming them would be churn for no semantic gain.

Discussion

Initial framing considered removing static arcs entirely on the theory that "people only want reactive." Pushback: reactive runs ~$0.50/campaign while static is free to serve, so eliminating the cheap tier removes the natural onboarding ramp and gives away cost flexibility. Reframed to a rename instead.

Naming debate landed on Stories over Arcs for player-facing copy. Arcs is the internal term and shows up in code (campaign_arcs, arc_type), but new players reading "Arc" get no signal about what it is. "Story" carries the implicit promise of pre-authored intent, which matches the static experience.

Scope debate landed on UI-only after walking the codebase. The architecture had already abstracted the product distinction behind arc_type, so the same CampaignBrowser component, the same /campaigns/[id] route, and the same Campaign TypeScript type already serve both modes. Renaming the data layer would cascade through 26 API routes, 4 page routes, RLS policies, and migrations — for no behavioral change. The umbrella names stay; the chrome changes.

The arc_type enum value rename ('static' → 'story', 'reactive' → 'campaign') was initially scoped as optional but folded into this ADR after weighing long-term cost. Keeping technical generation-strategy terms (static/reactive) distinct from product labels has theoretical appeal, but in practice it creates a real foot-gun: a query like WHERE arc_type = 'reactive' reads as "reactive arcs" but means "Campaigns" in product terms — and the discriminator drift forces every contributor to do mental translation on every query, code review, and bug report. The migration is cheap (one UPDATE, swap a CHECK constraint, regenerate types) and now is the cheapest moment to do it (5–6 active testers, no external API consumers). The technical-vs-marketing distinction can be reintroduced later by adding a separate generation_mode column if a hybrid mode ever emerges; today's arc_type values aren't load-bearing for that.

Consequences

  • Player-facing copy updated everywhere "Campaign" referred to static arcs and everywhere "World" referred to reactive arcs.
  • Solo tab labels change: "Quick Round | Campaign | World" → "Quick Round | Story | Campaign."
  • /campaigns/[id] page chrome now renders "Story" or "Campaign" headings based on the loaded arc's arc_type.
  • CampaignBrowser empty states and CTAs swap copy by arcType prop.
  • arc_type values change from 'static' | 'reactive' to 'story' | 'campaign' everywhere — DB rows, CHECK constraint, default value, TypeScript types, and ~20 call sites comparing the discriminator.
  • Table and type names (campaigns, campaign_arcs, Campaign, CampaignArc) stay as umbrella terms. A short terminology comment in types/database.ts documents the mapping for future readers.
  • ADR-032 (progression gating) terminology updated: the lock-at-3-games tier becomes "Story," the lock-at-8-games tier becomes "Campaign."
  • URL paths unchanged: existing bookmarks and deep links continue to work.

Key files: - src/app/solo/page.tsx — tab labels and state values - src/app/(app)/campaigns/[id]/page.tsx — page chrome conditional on isReactiveArc - src/components/campaign/CampaignBrowser.tsx — empty states, headings, "Featured" copy - src/components/onboarding/TabsIntroModal.tsx — onboarding copy - src/app/(app)/profile/page.tsx — profile labels - src/app/demo/campaign/page.tsx — demo narration - docs/decisions/032-progression-gating-campaign-world.md — terminology reference (no logic change)