Current Next Steps¶
Purpose¶
This page exists to keep the immediate follow-up work visible.
Unlike the broader roadmap and master backlog, this is a short list of the most important design and production steps that should happen next.
Current Tracked Next Steps¶
0C. Validate The New Simulator Output Loop Under Live Load¶
The simulator output surface is now present in the Admin app: batches are tracked as first-class records, admin-launched jobs are executed by the API worker, and the dashboard can poll live batch progress, render summary charts, list runs, drill into a selected run, and show analyzer-derived suggestions.
The immediate follow-up is operational hardening and signal quality rather than missing UI surface area.
The immediate concrete outputs for this step are:
- validate the dashboard against a multi-run live batch while the local API is not holding the simulation assembly open for test/build operations
- validate the new GameTelemetry-backed simulator path against the Kusto emulator and the target cluster, including the canonical GameTelemetry column shape and Details payloads
- decide whether existing simulation configuration and reporting should stay on one Admin page or be split now that simulator results are sourced from canonical telemetry
- confirm the new API worker behavior is acceptable for the target deployment shape and decide whether production should keep the in-process worker or move the same contract into a dedicated worker service
- deepen the suggestions section from analyzer-derived heuristics into richer AI-assisted tuning advice if an approved summarization path is introduced
- expand the drill-in surface if needed with per-combat node type, relic state, or deck-growth details once designers identify the most useful missing signals
0B. Finish Quick Play Runtime And Headless Simulator Parity¶
Quick Play now exposes all ten requested node families in the active three-realm WPF flow, with Protection routed through combat and Anchor and Treasure resolved through the shared recovery surface.
The remaining platform gap is parity and observability rather than raw screen presence: the simulator can run locally, but it still does not execute the same quick-battle style authored engine path that the UX uses, and it still lacks analyzer-grade outputs for tuning decisions.
The immediate concrete outputs for this step are:
- replace the placeholder simulator harness with an automated runner that exercises the same core quick-battle engine path as the UX
- randomize faction and class combinations per run while honoring authored starter decks, class mechanics, faction passives, and biome enemy selection
- add stronger automated play policies than the current random-only baseline so simulated decisions resemble a competent human more closely
- turn simulator telemetry into an analyzer pass that can surface actionable balance advice instead of returning empty summaries
- decide whether Treasure and Anchor should stay as instantaneous shared-screen outcomes or expand into richer authored interaction flows
- expand the new asset-generation pipeline beyond cards so authored prompts can continue to fill runtime-facing gaps without one-off scripts
0. Repair The Canonical Content Pipeline From Startup Seed To Runtime Snapshot¶
The latest runtime audit confirmed that canonical classes and factions seed correctly, but effect-registry rows and usable combat content were not guaranteed to appear in the local API-backed database unless a separate console seeder path had already been run.
The immediate path is to treat startup seeding, published snapshot generation, and runtime content verification as one platform concern instead of assuming authored docs or sidecar seed tools reflect live game data.
The immediate concrete outputs for this step are:
- ensure startup seeding keeps canonical effect-registry rows and the baseline production cards and enemies present in the DB, not just classes and factions
- rebuild or reconcile the published content snapshot whenever canonical startup seed content changes so runtime systems do not drift behind the source tables
- add an automated verification pass that compares runtime
IEffectimplementations, DB effect-registry rows, and published snapshot contents to catch missing content before manual QA
0A. Convert Wave 1 Card Imports From Authored Shells Into Runtime-Playable Content¶
The Wave 1 recommendation CSV is now imported into the database as 75 deterministic Card rows, but they are intentionally stored as Dev and inactive because they still lack runtime-backed effect payloads.
The immediate path is to treat the authored CSV as content source material, then map each imported card onto the actual Card runtime contract instead of assuming prose descriptions are enough for combat.
The immediate concrete outputs for this step are:
- define the effect, target, and keyword mapping rules needed to convert the 75 imported Wave 1 cards from description-only shells into playable card definitions
- decide the intended rarity distribution for the Wave 1 imports so reward pools do not default to placeholder rarity heuristics
- activate imported cards only after their effect payloads are runtime-valid and verified in API-backed combat
- ~~review API card pagination defaults so operator checks do not undercount the card pool by silently stopping at 50 rows~~ ✅ DONE —
Takemade nullable (int?) inCardFilter,EnemyFilter,RelicFilter, andAuditFilter; repository only appliesSkip/TakewhenTakehas a value; React admin list pages now receive all records without truncation; QuestTemplates status filter case mismatch fixed; React Query caching (5-min stale time) already prevents duplicate API calls for shared lookups (factions, classes)
1. Harden Save And Resume Around Pinned Content Snapshots¶
Save and restore now need to be treated as version-aware platform work, not just state serialization.
The immediate path is to finish the persistence model so active runs are pinned to an immutable published content snapshot or version set covering cards, quests, and the rest of the runtime definition payload.
This means resume must restore against the run's pinned snapshot instead of silently drifting to current live data, and save records must carry explicit save-schema metadata so later migrations have a stable contract.
The immediate concrete outputs for this step are:
- add save-schema migration and compatibility rules for older persisted run versions now that hero identity, hero profile, hero campaign state, and active runs are separated
- define when run progress is promoted back into hero campaign state and progression state on completion or abandonment
- add session management policy for refresh-token revocation, logout, and external provider linking
- expand save coverage beyond sanctuary-only autosave once the pinned snapshot contract is stable
2. ~~Replace Tutorial Seed Backfill With Admin-Owned Authoring~~ ✅ DONE¶
Completed. Tutorials have been consolidated into the quest system. The FactionTutorial entity and all *TutorialData.cs seed files have been removed. Tutorials are now QuestTemplate records with QuestType.Tutorial and optional JSONB sections:
RunPresets— fixed seed, starter deck/relics, starting HP/goldAuthoredContent— fixed encounters, story beats, intro/outro narrativeCompletionReward— goal type/params, passive unlock on completion
MudBlazor has been added to the Admin app and the Quest Template Editor page now has structured sub-editors for all three sections. The migration ConsolidateTutorialsIntoQuests drops the faction_tutorials table and adds the new columns to quest_templates.
2A½. Standardized GameDataGrid In React Admin ✅ DONE¶
Completed. All game-data list pages in the React Admin app now use a shared GameDataGrid component built on @mui/x-data-grid. Every column supports built-in filtering, sorting, and full-text quick search out of the box — pages just define column definitions and pass a dataset.
Key details:
- GameDataGrid wraps MUI DataGrid with automatic lineage grouping (picks latest-version representative per
lineageId), auto Status + Versions columns, quick-search toolbar, and click-to-edit row navigation - 11 list pages migrated: Cards, Enemies, Classes, Factions, Relics, QuestTemplates, RandomEvents, MapParams, EconomyParams, FeatureSwitches, Effects
- Effects uses DataGrid directly (inline CRUD dialog pattern rather than route-based editing)
- FeatureSwitches uses the
externalDataprop since it has a non-standard API endpoint - Audit keeps its custom implementation (server-side filtered, user-controlled Take)
- Old
ContentEntityListcomponent deleted — no pages import it anymore - Net result: −919 lines removed, +630 lines added; typical list page dropped from ~130 lines to ~50
2A. Bring Admin To Feature Parity With Runtime Content Contracts¶
The Admin site now has a real operator login flow, role-aware navigation, richer Quest editors with MudBlazor structured sub-editors, typed rift payload authoring support, and UI surfaces for publish, rollback, and audit.
Critical DI scope fix (completed): The Admin app's EchoSpireApiClient was using IHttpClientFactory DelegatingHandler instances (ApiAuthHandler, ApiTelemetryHandler) for auth and telemetry headers, but IHttpClientFactory creates handlers in a factory scope, not the Blazor circuit scope. This meant every API call got a fresh, empty AdminAuthSession with no access token — causing systematic 401 errors on every page. The fix moved header injection into EchoSpireApiClient itself (which resolves in the circuit scope) and removed the broken handlers from the factory pipeline. This also explains why browser DevTools showed no API calls — Blazor Server makes all HTTP calls server-side over SignalR.
The remaining gap is that validation and persistence contracts still need to move from JSON-assisted editing toward fully shared, type-specific API-owned DTO validation, and the Admin app still does not cover operator account management or deeper run operations beyond inspection.
The immediate concrete outputs for this step are:
- add shared API/Admin validation for
QuestTemplatenested authored payloads so invalid JSON-shaped content is rejected before publish - replace JSON-assisted rift payload editing with persisted, type-specific authoring DTOs once the shared contract layer exists
- add Admin operator account management and deeper run actions such as safe abandon, restore diagnostics, and save-schema inspection tooling
- add publish safeguards such as content validation summaries and stronger rollback confidence checks in the UI
2B. Asset Metadata Is Now In The Database¶
All entities with art assets (Card, Enemy, GameClass, Faction, Relic) now carry two new nullable fields alongside ArtAssetKey:
AssetFilePath— the physical relative path to the asset file (e.g.,assets/factions/crests/faction-valerii.png)AiGenerativeDescription— a prompt-ready description for AI image generation, stored at up to 4000 characters
QuestStoryPanel (which already had ImageAssetPath) also gained AiGenerativeDescription.
The seeder populates these for all 5 canonical classes, 5 factions, and the 5 opening story panels. The migration AddAssetFilePathAndAiDescription adds the columns to the cards, enemies, classes, factions, and relics tables.
Current production status:
- Wave 1 authored card illustrations are now generated for the current 75-card recommendation set under
assets/cards/art - canonical relic art is now generated for the first 12-runtime relic pool under
assets/relics/art - quick-play fallback enemy key art is now generated for the current 15-enemy playable pool under
assets/enemies/art - the canonical relic pool and the current 15 quick-play enemy pool now seed
AssetFilePathandAiGenerativeDescriptioninto the local DB through the shared startup catalog, and the stale duplicate localFerrum Shardrow has been removed - class portraits, faction crests, faction backgrounds, biome combat backgrounds, biome enemy frames, and opening story panels already exist in
assets/ - faction banners and faction leader portraits are still empty folders and remain presentation-polish work rather than immediate runtime blockers
The immediate concrete outputs remaining for this step are:
- add Admin UI fields for editing
AssetFilePathandAiGenerativeDescriptionon card, enemy, and relic authoring screens, then backfill any remaining noncanonical rows that still point at placeholder or missing asset paths - decide whether faction banners and leader portraits justify a separate polish pass now that cards, relics, and the current enemy pool have first-pass generated art
2C. Content Lifecycle V2 — Lineage-Based Immutable Versioning ✅ DONE¶
Completed. The V1 lifecycle system (ReplacesId / PendingRetirement with manual status dropdown) has been replaced by the V2 lineage-based immutable versioning model designed in content-lifecycle-v2.md.
What changed:
ReplacesId(Guid?) andPendingRetirement(bool) removed from all 9 entity modelsLineageId(Guid, non-nullable) added — groups all versions of the same logical content. Set toIdfor new and seeded entitiesILifecycleEntityupdated: now exposesId,Status,LineageId,CreatedAt,UpdatedAt- Composite
(LineageId, Status)indexes added to all 9 content tables - Published assets are now immutable — the API rejects saves for non-Dev entities with 400 Bad Request
- Auto-set:
LineageIdis populated withIdif not provided on save
New API endpoints (5 lifecycle actions + preview):
POST lifecycle/{entityType}/promote/{id}— Dev → Staged (validates max 1 Staged per lineage)POST lifecycle/{entityType}/demote/{id}— Staged → DevPOST lifecycle/{entityType}/discard/{id}— Dev → deletedPOST lifecycle/{entityType}/new-version/{id}— Published → new Dev clone (same LineageId, new Id)GET lifecycle/{entityType}/lineage/{lineageId}— returns all versions ordered by CreatedAt descGET admin/publish/preview— returns staged items split into NewItems and UpdatedItems with lineage context
Admin UI changes:
LifecycleFieldsEditor.razorrewritten from manual dropdown/textbox to status badge + action buttons (Promote to Staged, Demote to Dev, Discard, Create New Version) that appear based on current status- Save button hidden for non-Dev entities on all 9 editor pages
- Snapshots page upgraded to Publish Review with staged items preview table (new vs. updated)
ReplacesId/PendingRetirementbindings removed from all 9 editor pages
Migration: ContentLifecycleV2_LineageId drops ReplacesId and PendingRetirement, adds LineageId with data migration (LineageId = Id for existing rows), and adds composite indexes.
2D. Admin Editor Quality Pass — Structured Sub-Editors and Lifecycle UI¶
Completed. A comprehensive audit of all 16 Admin pages found that the average editor exposed only 36% of model fields, lifecycle fields were missing from all 9 content entities, and several editors used raw JSON textboxes for complex structured data.
New shared components created:
StoryPanelsEditor.razor— structured master/child editor forQuestStoryPanelarrays with order, act label, heading, narrative text, image path, AI description, and move up/down/remove controlsLifecycleFieldsEditor.razor— reusable expansion panel showing content status badge and action buttons (Promote to Staged, Demote to Dev, Discard, Create New Version) based on currentContentStatus; calls lifecycle API endpoints directly and fires a callback on successEventChoicesEditor.razor— structured editor forEventChoicearrays with label, description, weight, and JSON outcome payload
Pages updated:
- QuestTemplates — Story Panels raw JSON replaced with
StoryPanelsEditorsub-editor;LifecycleFieldsEditoradded - RandomEvents — upgraded from view model to full
RandomEventdomain model;EventChoicesEditoradded (previously event choices could not be edited at all);LifecycleFieldsEditoradded - Cards — Status column added to list table;
LifecycleFieldsEditoradded to edit form - Enemies — Status column added;
LifecycleFieldsEditoradded - Factions — Status column added;
LifecycleFieldsEditoradded - Classes — Status column added;
LifecycleFieldsEditoradded - Relics — FactionRestrict bug fixed (field was shown in list but missing from edit form); Status column added;
LifecycleFieldsEditoradded - MapParams — Status column added;
LifecycleFieldsEditoradded; lifecycle fields included in save payload - EconomyParams — Status column added;
LifecycleFieldsEditoradded; lifecycle fields included in save payload
Remaining gaps (tracked for future passes):
- Card effects editor (List\<CardEffect> — cannot assign effects to cards)
- Enemy intent pattern editor (List\<IntentEntry> — cannot script enemy behavior)
- Faction passive mechanic, signature cards, and class restrictions editors
- GameClass mechanic params, innate/ultimate ability, and starter card editors
- Relic passive params and trigger condition editors
- Effect registry parameter schema editor
- ~~Full MudBlazor migration for older Bootstrap-based editor pages~~ ✅ DONE (see 2F)
- ~~Status filter dropdowns on list views for lifecycle state filtering~~ ✅ DONE (see 2F)
2F. Shared Reusable Admin Components — ContentEntityList + ContentEntityDetail ✅ DONE¶
Completed. The duplicated inline-edit admin page pattern (~150–250 lines per page, repeated across 9 entity types) has been extracted into two generic, reusable Blazor components. All 9 lifecycle entity pages now use the shared components.
New shared components (src/EchoSpire.Admin/Components/Shared/):
IContentListItem— interface requiringId,AssetId,Name,Status(ContentStatus),LineageId,UpdatedAt(all get/set)ContentEntityList<TItem>— lineage-grouped, filterable, searchable list component with:- MudCheckBox status filters: Dev + Staged + Published checked by default so the live view is visible immediately
- MudTextField search with immediate binding
- MudTable with sortable columns, expandable child rows showing all versions in a lineage
- RenderFragment parameters:
HeaderColumns,RowColumns,ChildHeaderColumns,ChildRowColumns,ExtraFilters SearchableTextandCustomFilterfunction parameters for entity-specific logic- Lineage grouping picks a representative (Published > Staged > Dev, then newest)
ContentEntityDetail<TItem>— edit page shell with:- Version history bar showing MudChip per lineage version with status coloring
FormContentRenderFragment receivingEditContext<TItem>(Item, IsReadOnly, IsNew)- LifecycleFieldsEditor integration with Promote/Demote/Discard/NewVersion
- Save/Discard buttons, ReadOnly enforcement for non-Dev entities
BuildSavePayloadcallback for custom serialization (e.g. JSON value parsing, GUID parsing)OnItemLoadedcallback for post-load setup
Pages converted (20 files, +1415/−1559 lines):
| Entity | List Page | Edit Page | Notes |
|---|---|---|---|
| Card | Cards.razor (241→60 lines) | CardEdit.razor | CardType, Rarity, EnergyCost |
| Enemy | Enemies.razor | EnemyEdit.razor | EnemyType, HP, Tier range, Gold/XP |
| Faction | Factions.razor | FactionEdit.razor | DisplayName, LeaderName |
| GameClass | Classes.razor | ClassEdit.razor | Life, Energy, HandSize, CoreMechanic |
| Relic | Relics.razor | RelicEdit.razor | BuildSavePayload for FactionRestrict GUID |
| RandomEvent | RandomEvents.razor | RandomEventEdit.razor | EventChoicesEditor sub-editor |
| MapParam | MapParams.razor | MapParamEdit.razor | JSON value parsing via BuildSavePayload |
| EconomyParam | EconomyParams.razor | EconomyParamEdit.razor | JSON value parsing |
| QuestTemplate | (already had lineage grouping) | (already existed) | MudTable T= fix only |
2E. Structured Error Handling Across API And Admin ✅ DONE¶
Completed. The ad-hoc error handling (bare NotFound(), BadRequest(new { error = "..." }), StatusCode(500, ...), and EnsureSuccessStatusCode()) has been replaced with a structured error envelope system across all API controllers and the Admin client.
Core types created (EchoSpire.Core/Errors/):
ApiError— response envelope with Code, Message, Fault (Client/System), Category, CorrelationId, and optional Details (field-level validation)ErrorCodes— ~40 error code constants organized by domain (Auth, Hero, Run, Lifecycle, Telemetry, Entity, Content, System) with a Registry mapping each code to fault, category, and HTTP status, plus DefaultMessages with English message templates supporting format argsErrorMeta— metadata class (Fault, Category, HttpStatus) for each error code
API infrastructure:
ApiErrorExtensions— controller extension methods (ApiError,ApiErrorWithMessage,ApiValidationError) and a staticBuildApiErrorhelper for middlewareTelemetryMiddlewareupdated: catches unhandled exceptions and maps them to structuredApiErrorresponses (e.g.UnauthorizedAccessException→AUTH_TOKEN_INVALID,InvalidOperationException→LIFECYCLE_INVALID_TRANSITION, fallback →SYSTEM_INTERNAL_ERROR). CorrelationId stored inHttpContext.Itemsfor controller access.
Controllers converted (7 controllers, 60+ error paths):
AuthController— login/refresh/register errorsHeroesController— 13+ error paths with specific codes (NotFound, InvalidLevel, InvalidXp, NameRequired, InvalidFaction, InvalidClass, ClassFactionRestricted, NameConflict)RunsController— hero/class/faction required, active run conflict, run not foundAdminController— 9 lifecycle immutability checks, 4 lifecycle transition catches, unknown entity type, hero/campaign validationGameDataController— all NotFound returns (10 endpoints)StoriesController— NotFound returnsTelemetryController— service unavailable + query failures (8 endpoints). Removed privateServiceUnavailablehelper.HealthController— left unchanged (health probes have a distinct response contract for monitoring)
Admin client:
ApiException— wrapsApiErrorwithIsClientFaultclassification;Messageproperty returns the user-friendly textEchoSpireApiClient— allEnsureSuccessStatusCode()andGetFromJsonAsynccalls replaced withEnsureSuccessAsyncwhich deserializes theApiErrorbody and throwsApiException- 15 Razor pages — alert divs updated to severity-aware styling (red for errors, green for success) based on message prefix
Design doc: Full error handling design at docs/official/error-handling-design.md covering error envelope spec, fault classification, ~40 error codes, translation architecture, and implementation sequence.
Tests (25 new, 314 total passing):
ErrorCodesTests— registry completeness, default message resolution, format args, ApiError round-tripErrorHandlingTests— 13 integration tests validating envelope shape, CorrelationId presence, and specific error codes for auth, hero, gamedata, and lifecycle operations
Remaining follow-up (optional, tracked for future passes):
- Add .resx-backed
IErrorMessageProviderfor multi-language translation readiness - Add
ApiValidationErrorusage with field-levelDetailsfor hero creation and quest authoring - Upgrade Razor page catch blocks to distinguish
ApiExceptionfor fault-aware severity (currently uses message prefix heuristic) - Add error codes for Simulation, Meta, and future endpoints as they gain error paths
3. Bring Up The Hosted Tester Environment¶
Execute the low-cost hosted rollout using Neon, Upstash, Render, and Cloudflare Pages.
This means getting the first externally reachable tester environment online, validating API, Admin, and docs connectivity, and turning the hosted early-dev path into a repeatable operational routine instead of a theoretical option.
The concrete execution steps for this are defined in hosted-early-dev-runbook.md.
3A. Stand Up The WPF MVP Client Shell And First Hero Flow¶
The repository now has a disciplined Windows-first WPF client slice with real API connectivity, local structured telemetry, auto-bootstrap into the preview account, hero creation, hero selection, run bootstrap handoff, and the first pass of a real UX flow scaffold.
A full game-feel audit of the WPF client was completed in March 2026 and is documented in wpf-audit-and-next-build-pass.md. That audit found the architecture is sound but the shell feels like a styled business app rather than a game. Only two of nine planned screens exist, animations are nearly absent, and there are no composable game-UI controls.
The immediate path splits into two parallel tracks:
Track A: Game-feel foundation and the Storyteller control
This track addresses the admin-app problem identified in the audit before building new screens.
The immediate concrete outputs are:
- promote duplicated game-screen styles from StartupLoadingView and MainMenuView into Theme.xaml as shared resource keys
- replace the stock ProgressBar on StartupLoadingView with a custom animated loading indicator
- add Storyboard-based easing to hero slot hover and select ScaleTransforms so they animate smoothly
- add screen-transition animations using a fade or crossfade wrapper around the ContentControl in MainWindow
- build a reusable Storyteller control as a composable UserControl that accepts a list of StoryBeat data objects and presents them as a cinematic narrative sequence with background images, optional character portraits on left and right, styled dialogue and narration frames, faction-aware accent coloring, beat advancement, progress indicators, skip support, and keyboard input
- define the StoryBeat and SpeakerPlacement data models in Contracts so narrative sequences are database-authorable through Admin and portable to a later Unity client
- the Storyteller control must support three narrative modes: cinematic narration with full-bleed backgrounds and omniscient text, dialogue with speaker and respondent portraits and faction identity, and triggered beats as lightweight overlays during gameplay
Track B: Missing screens using the Storyteller and game-feel foundation
Once the Storyteller control exists, the immediate screen build order is:
- build the Opening Story screen by hosting the Storyteller control full-screen with five Narration beats loaded from the existing GetOpeningStoryAsync API endpoint, and wire the startup-loading to opening-story to main-menu navigation flow
- build the Hero Builder screen with faction-first selection showing five faction cards with distinct color identity and lore, filtered class selection, name input with AI-generated suggestions, and confirmation panel
- build Run Briefing, Combat Encounter, Realm Map, Reward And Event, Sanctuary And Recovery, and Settings And Accessibility screens as described in the WPF audit doc
This work must respect the existing architecture boundary that Core keeps gameplay-rule truth and the client remains a presentation shell.
The full recommended build order, Storyteller control specification, composable control inventory, and architecture notes are in wpf-audit-and-next-build-pass.md.
3A1. Make Quick Play A Separate Hero-Less Run Mode¶
The Quick Play clarification is now that it should not use the player's hero at all.
Instead, Quick Play should let the player choose a faction and eligible class, inherit the correct passives and starter loadout, and then enter a randomized three-realm run without consuming storyline content.
The completed outputs for this step are now:
- Quick Play run persistence and lookup now use an explicit run mode rather than overloading hero runs
HeroIdis optional for Quick Play run start whileClassIdandFactionIdremain required- the WPF shell now has a real Quick Play Setup screen for faction and class choice plus a real Run Briefing screen that consumes the saved selection
- shared game-data DTOs now expose faction passive text, class mechanic context, starter cards, and starter relic placeholders to the WPF flow
- Quick Play save and resume are isolated from hero-bound campaign runs
- story, tutorial, and lore-heavy onboarding remain out of Quick Play flow
The next concrete outputs for Quick Play are:
- move the current authored realm-map templates out of code constants and into shared content contracts or Admin-owned authoring so path identity can scale without another client refactor
- replace the current biome-specific fallback enemies and events with fully authored seeded realm pools so content flavor is data-driven rather than service-generated
- add a mixed-reality Rift biome pass after the initial three-biome map presentation lands, including second-pass concept backgrounds and a clearer overlap-language for conflicting lighting and terrain
- replace fallback starter cards, enemies, relics, and events with fully authored content contracts and seeded data
- add true mid-combat save and resume if the product still needs that after feedback on node-boundary saves
Immediate realm-map follow-up, in order:
-
- externalize the current authored realm-map templates into shared runtime content or Admin-owned authoring so map identity scales without another WPF-only refactor
-
- replace the current biome-specific fallback enemies and events with seeded authored realm pools so content flavor is data-driven instead of service-generated
- move authored template definitions into shared runtime content so WPF, API, Admin, and any future Unity client consume the same map-authoring contract
- replace service-generated biome fallback encounters and events with seeded authored content that maps cleanly to each biome and objective type
- add a second-pass visual treatment for locked gates and required-path nodes in downstream screens such as node entry, reward, and recovery so the realm-map signaling stays coherent after click-through
- add the mixed-reality Rift biome as the fourth presentation/content pass once the first three biome templates and pools are fully data-driven
The canonical definition for this mode is in quick-play-end-to-end-plan.md.
3B. Lock The Later Unity Migration Seam Early¶
The decision is now to ship the MVP in WPF and treat Unity as a later presentation upgrade if the product proves itself.
That only remains cheap if the current WPF client is disciplined about what it owns and what it does not.
The immediate goal here is to keep the WPF client from becoming a dead-end rewrite trap.
The immediate concrete outputs for this step are:
- define the event, save, and view-model seams that a later Unity client can consume without inheriting WPF-specific state objects
- keep assets, styling, and interaction code isolated from gameplay orchestration so the future migration replaces presentation rather than business logic
- avoid WPF-only shortcuts that would force combat, map, or reward logic to be re-authored later
- define the criteria that would justify the Unity migration: proven external demand, budget for presentation investment, and a clear need for controller-first polish, richer animation, or broader platform targets
4. Expand The Campaign Into Faction Story Arcs¶
Turn the campaign framework into specific faction-by-faction act outlines and quest chains.
This means defining the major beats, recurring characters, revelations, rival encounters, and the run-level single-realm quest structure for each faction after the tutorial.
This work should produce a story spine strong enough to guide content production, encounter design, codex writing, and live implementation.
5. Convert Canon Design Into Shared Runtime And API Contracts¶
Translate the newly promoted canon into implementable data and API shapes.
This includes hero progression records, XP and unlock tracking, quest chain structures, rival hero definitions, faction-conflict encounter data, and the remaining runtime contracts needed by Core, API, Admin, Unity, and modular effect packages.
This work should be treated as the bridge between documentation and implementation.
The immediate concrete outputs for this step are:
- define the canonical
GameplayEvent,EffectResult,TriggeredEffectBatch, andUxEventshapes - define the first module manifest schema for effect packages
- define the authoring DTOs for each rift type so API and Admin can validate them consistently
- define the policy and telemetry contracts for gameplay AI, simulation AI, and live-site agent evidence access
6. Define XP Pacing And Persistent Progression Economy¶
The class progression rows are now canonical.
The next progression task is to define how heroes earn XP, when level bands are expected across the campaign, and how progression interacts with unlocks, persistence, and replayable endgame structure.
This work should turn progression from a static table into a usable live-game economy.
7. Define AI Contracts And Operational Evidence Flows¶
The high-level AI direction is now canonical.
The next AI task is to define the exact interfaces, data contracts, and retrieval tooling boundaries for:
- deterministic gameplay AI policies
- simulation player-policy scoring and telemetry
- live-site incident triage evidence access
- balance recommendation agent inputs and output artifacts
This work should turn the AI direction into an implementable platform instead of a conceptual split.
The immediate concrete outputs for this step are:
- finalize
RunIdandCorrelationIdplumbing across WPF, Admin, API, and response echoes so incident evidence can be joined reliably - bring the local Kusto emulator path back to a known-good state with a documented reset and verification workflow
- expose a first debugging-agent tool surface over the existing telemetry endpoints for health overview, recent errors, request traces, and run timelines
- define the agent output contract for classification, evidence summary, confidence, and recommended next action
That first debugging-agent surface now exists in workspace form via .github/agents/echospire-live-site-debugger.agent.md and .github/prompts/investigate-live-site-incident.prompt.md.
The next immediate concrete outputs after that are:
- exercise the agent against at least one real local incident using
CorrelationIdand one usingRunId - tighten the operator auth and evidence flow with an internal telemetry MCP server once the tool surface is proven stable
- reduce the debugging agent from shell-driven HTTP calls to scoped MCP tools once the server exists
- store reviewed incident analyses as structured artifacts so future retrieval can compare against prior failures
2D. Admin Page Endpoint Test Coverage (Automated)¶
Every Admin Blazor page makes API calls on load (read via gamedata/), save (write via admin/), and delete (via admin/). A new test class AdminPageEndpointTests now systematically exercises every one of these calls against the in-memory API host with correct and incorrect auth, catching the class of bug where clicking an Admin editor page produces a 401 or 403 error at runtime.
The test suite covers:
- Page load tests — every read endpoint succeeds with SuperAdmin auth
- Auth rejection tests — every endpoint correctly returns 401 without auth, and 403 for Player-role tokens on write endpoints
- Full round-trip tests — Save → Load → Delete for enemies, cards, relics, events, effects, map params, economy params (simulates the complete Admin editor workflow)
- Snapshot-specific test — Snapshots page accepts 200 or 404 (no snapshot published yet)
- Admin-specific pages — Audit log, Heroes admin, and Publish all verified
79 tests run in ~3.5 seconds with zero external dependencies (SQLite in-memory).
A second test class AdminPayloadAndSecurityTests now exercises negative payloads, injection attacks, and auth security:
- Negative payloads — empty bodies, null bodies, negative numeric values, inverted tier ranges, extreme integers, invalid enum strings, zero-dimension map params, empty names/IDs, malformed JSON, wrong content types, deleting non-existent entities
- SQL injection — five SQL injection patterns in entity name fields, search query parameters, audit table parameter, and run status parameter; all verified to store literally or reject cleanly (EF Core parameterized queries prevent execution)
- XSS injection — five common XSS payloads (
<script>,<img onerror>,<svg onload>, template injection,javascript:protocol) verified to store as literal strings, not execute - Path traversal — six path traversal patterns in
AssetFilePathandArtAssetKeyfields - Command injection — five OS command injection patterns in name and formula fields
- Oversized payloads — 100KB names, 500KB descriptions to test unbounded string fields
- Auth security — forged JWT tokens, expired tokens, empty bearer tokens, role escalation via login role hint, empty credentials, SQL injection in login, duplicate registration, extremely long usernames, XSS in usernames, invalid/empty refresh tokens
- ID manipulation — client-supplied IDs, extra unknown fields (prototype pollution patterns)
- Content-type attacks — missing content type, Unicode edge cases (CJK, zero-width, null char, BOM, emoji)
56 additional tests, all running in ~3.5 seconds.
The immediate concrete outputs remaining for this step are:
- add RiftRules page endpoint coverage once that page has save/delete actions
- add Simulation page endpoint coverage for batch queue and results retrieval
- add Designer-role tests to verify Designer can write content but cannot publish snapshots
- add server-side validation (data annotations or FluentValidation) to reject empty names, negative HP, inverted tier ranges, and oversized strings — the current tests document that these are accepted today
- add
AssetFilePathvalidation to reject path traversal patterns before they reach storage - add rate limiting or brute-force protection on login and registration endpoints
- consider adding a CI gate that fails the build if a new Admin page is added without corresponding endpoint tests
2E. Admin Portal Telemetry ✅ DONE¶
Completed. The Admin portal now has structured telemetry matching the API's existing NDJSON telemetry pipeline.
ApiTelemetryHandler(DelegatingHandler) records every outgoing Admin→API HTTP call as anApiRequestEventwith timing, HTTP method, path, status code, and CorrelationIdJsonFileTelemetryWriterandGameTelemetrySessionregistered as singletons in Admin DI (avoids circuit-scope/factory-scope mismatch)SystemLifecycleEventemitted on Admin startup and shutdown- Telemetry output goes to
artifacts/telemetry/admin/as NDJSON files Telemetry:JsonFallbackPathconfigured inappsettings.Development.json
2E-2. Admin JSON Deserialization Fix And Complete Error Telemetry ✅ DONE¶
Completed. Exercising the Admin telemetry in practice revealed two issues:
Root cause — JsonStringEnumConverter missing from Admin API client:
EchoSpireApiClient.ReadFromJsonAsync<T>() used default JsonSerializerOptions without JsonStringEnumConverter. The API serializes ContentStatus values as camelCase strings (e.g. "published"), but the Admin client could not deserialize them back into the ContentStatus enum. This caused every editor page to fail on load with "The JSON value could not be converted to EchoSpire.Core.Enums.ContentStatus". Fixed by passing AdminJson.Options (which already had the converter) to all 9 ReadFromJsonAsync, PostAsJsonAsync, and GetFromJsonAsync calls in EchoSpireApiClient.
Telemetry blind spot — catch blocks not tracking errors:
36 catch blocks across 14 Blazor editor pages were swallowing exceptions with _message = $"Error: {ex.Message}" but not calling TrackErrorAsync. This meant Admin client errors (like the deserialization failure above) were invisible in Kusto — the API returned 200 successfully, but the client-side failure was silently eaten. Now all 36 catch blocks call await Telemetry.TrackErrorAsync(ex, component, action, ...) before setting the user-visible message.
32 new AdminDeserializationTests verify that API JSON responses deserialize correctly with Admin-equivalent serializer options, covering all 10 list endpoints, ContentStatus enum round-trips for 9 entity types, and save round-trips for Quest, Enemy, and Card.
Kusto dev workaround: The Kustainer image's .create database ifnotexists EchoSpire silently fails; switched appsettings.Development.json to use NetDefaultDB as a workaround.
2F. Quest Editor UX Overhaul — Lineage-Grouped List And Dedicated Edit Page ✅ DONE¶
Completed. The quest list and quest edit UI have been overhauled to work properly with Content Lifecycle V2 and remove three obsolete boolean fields.
Obsolete fields deprecated:
QuestTemplate.IsActive— replaced byContentStatuslifecycle (Dev/Staged/Published). Marked[Obsolete].QuestTemplate.IsOpeningStory— replaced byQuestType.OpeningStoryenum value. Marked[Obsolete].QuestObjectives.IsTutorial— replaced byQuestType.TutorialonQuestTemplate. Marked[Obsolete].
All runtime queries updated: PostgresGameDataRepository.GetOpeningStoryQuestAsync() now filters on QuestType == QuestType.OpeningStory && Status == ContentStatus.Published. StorytellerService.FindTutorialQuestAsync() now uses Status == ContentStatus.Published instead of IsActive. Seeder uses QuestType = QuestType.OpeningStory instead of boolean flags. IsOpeningStory index removed from DbContext (QuestType index already exists).
Quest list page rewritten (QuestTemplates.razor):
- Groups all quest versions by
LineageId— shows one row per distinct quest, not one row per version - Picks a "representative" version per lineage: Published > Staged > Dev, then newest
- Columns: #, Name, Description, Faction, Type (color-coded chip), Status (color-coded chip), Versions count
- Search box filtering by name, description, or faction
- QuestType filter dropdown
- ContentStatus filter dropdown
- MudTable with sortable columns
- Row click navigates to
/quests/{id}edit page - "+ New Quest" button navigates to
/quests/new - No inline edit form, no delete button on list (handled in edit page via Discard)
Separate quest edit page created (QuestTemplateEdit.razor):
- Routed at
/quests/newand/quests/{Id:guid} - Version history bar showing all lineage versions as clickable chips with status colors
- All form fields with
ReadOnlybinding when status is not Dev - Save button (Dev only), Discard button (Dev only, replaces Delete via lifecycle action)
- Version switching via clickable chips in the history bar
- Structured sub-editors: StoryPanelsEditor, RunPresetsEditor, AuthoredContentEditor, CompletionRewardEditor, LifecycleFieldsEditor
- Full telemetry: TrackPageViewAsync, TrackActionAsync, TrackErrorAsync
Database columns retained: IsActive and IsOpeningStory columns remain in the database for backward compatibility. The [Obsolete] attributes warn new code. A future migration can drop them once all data has been verified migrated.
Remaining follow-up (tracked for future passes):
- Apply the same lineage-grouped list + search/filter pattern to Cards, Enemies, Factions, Classes, Relics, RandomEvents, MapParams, EconomyParams, and Effects admin pages
- Create EF migration to physically drop
IsActiveandIsOpeningStorycolumns once data migration is confirmed - Data migration script to backfill
QuestType = OpeningStoryfor any existing rows whereIsOpeningStory = true
2G. AssetId Numbering Scheme ✅ DONE¶
Completed. All 9 lifecycle entity types now carry a human-readable int AssetId auto-assigned from type-specific 1000-block ranges:
| Range | Entity Type |
|---|---|
| 1000–1999 | Cards |
| 2000–2999 | Enemies |
| 3000–3999 | Factions |
| 4000–4999 | GameClasses |
| 5000–5999 | Relics |
| 6000–6999 | RandomEvents |
| 7000–7999 | QuestTemplates |
| 8000–8999 | EconomyParams |
| 9000–9999 | MapParams |
AssetIdRangesstatic helper class inILifecycleEntity.cswithGetRangeStart<T>()andBlockSize = 1000AssignAssetIdIfNewAsync<T>()inPostgresGameDataRepositoryqueriesMAX(AssetId)within the type's range and assigns the next available- AssetId carries across lineage versions in
CreateNewVersionAsync(same logical asset = same AssetId) - Canonical seed entities have deterministic AssetIds (Classes 4000–4004, Factions 3000–3004, Quest 7000)
AssetIdindexed on all 9 entity tables- All 9 Admin editor pages display AssetId as the
#column
Why These Are First¶
These items are the shortest path from high-level canon to production-ready execution:
- hosted rollout creates the first real external feedback loop
- the WPF MVP shell creates the fastest path to a paid Windows preview without forking the current runtime rules
- the WPF shell still needs to prove it can feel like a game client rather than a line-of-business shell wearing concept art
- the Unity migration seam keeps the MVP client from hardening into an accidental permanent rule owner
- pinned snapshot saves prevent live content edits from corrupting in-progress runs
- tutorial rebuild keeps the first-play experience aligned with live engine truth
- faction story arcs define campaign-scale content production
- data contracts define how the design becomes real software
- XP pacing and progression economy define how long-term player growth actually lands in play
- AI contracts define how gameplay AI and operational agents can be added without breaking determinism or supportability
If these remain vague, the rest of content production and implementation will drift.
8. Implement Rift Run Casual Quick-Play Mode¶
Rift Run is now part of the GDD as a first-class game mode.
This mode depends on the quest system, realm generation, and run bootstrap being functional. It does not depend on campaign narrative, faction story arcs, or tutorial rewrite completion.
The immediate concrete outputs for this step are:
- define a
RiftRunquest template type that generates 3-realm random run parameters with no campaign bindings - add a Rift Run entry point to the game runner that bypasses faction orientation, tutorial checks, and campaign state
- implement class and faction-alignment selection without hero persistence (Rift Run heroes are ephemeral)
- implement 3-realm stitched map generation using the existing
MapGeneratorwith random realm assignment - implement end-of-run stats screen: damage dealt, damage taken, cards played, cards exhausted, relics collected, rooms cleared, time elapsed
- define the progression boundary: Rift Run does not award campaign passives; campaign completion unlocks cosmetic borders and elite starter variants in Rift Run
This work can be parallelized with campaign and tutorial development since Rift Run has no narrative dependencies.
9. Expose A Public Read-Only Card Data API¶
Instead of building an official theory-crafting app, expose the game's card pool, relic pool, class stats, faction passives, and keyword definitions as a versioned read-only JSON API for community tool builders.
This is low-cost (the data already lives in the existing API) and high-impact (it enables community-built deck simulators, build guides, win-rate analyzers, and content creator overlays without consuming dev time).
The immediate concrete outputs for this step are:
- define a public read-only endpoint surface that returns cards, relics, classes, factions, passives, and keywords
- version the endpoint so community tools can detect card pool changes
- add basic documentation (OpenAPI or a lightweight developer guide) so a community builder can start in a weekend
- do not gate this behind authentication — it is public reference data
This can ship as soon as the card pool reaches campaign-scale size.
10. Build In-Game Deck Inspector With Synergy Highlights¶
Once the card pool is large enough to support meaningful deckbuilding decisions, add an in-game deck inspector that lets the player view their current deck composition, see card synergy highlights (e.g., "you have 4 cards that benefit from Density"), and compare their build against the class archetype baseline.
This complements community tools rather than competing with them and lives inside the game where the player is already engaged.
This is a later priority — it depends on the card pool, relic pool, and keyword system reaching production scale.
11. Generate And Place Combat Screen Visual Assets¶
The combat screen now has faction-themed card borders and biome-themed enemy/info borders implemented in code (colors resolve from FactionPassiveId and the current realm's BiomeKey). The next step is generating the actual image assets.
The full asset manifest with AI generation prompts, directory placement, and color palette reference is in combat-asset-manifest.md.
The immediate concrete outputs for this step are:
- generate 20 faction-specific card frame PNGs (5 factions × 4 card types) using the AI prompts in the manifest
- generate 3 biome-specific enemy frame PNGs and 3 combat background PNGs
- generate card type icons (4), intent icons (5), energy orb, block shield, and 5 faction crests
- place all 45 assets in the declared directory paths under
assets/ - wire up image loading in
CombatMapView.xamlto replace procedural SVG paths with the generated assets once they exist
Relationship To Other Official Docs¶
- Use
hero-progression-and-difficulty.mdfor the progression framework and scaling model. - Use
campaign-and-conflict-framework.mdfor the campaign and rival hero structure. - Use
realm-framework.mdfor unstable realm logic, realm passives, and single-realm quest structure. - Use
rift-types-and-variables.mdfor node-type taxonomy and authoring payloads. - Use
effect-pipeline-and-modular-deployment.mdfor effect, trigger, module, and UX event contracts. - Use
hosted-early-dev-runbook.mdfor the exact hosted rollout sequence. - Use
wpf-mvp-kickoff.mdfor the first WPF MVP milestone, screen-flow direction, and migration boundaries. - Use
wpf-audit-and-next-build-pass.mdfor the complete WPF game-feel audit, Storyteller control design, and recommended build order. - Use
unity-client-kickoff.mdfor the later Unity migration target and preserved presentation boundaries. - Use
ludo-asset-prompts.mdfor the stored visual prompt library and later Unity-oriented Ludo revisions. - Use
technical-architecture.mdfor system boundaries and required contracts. - Use
master-todo.mdfor the full backlog once these immediate next steps are clear. - Use
documentation-audit-2026-03-19.mdfor the full March 2026 documentation audit findings and cleanup action items.
DOC. Documentation Cleanup From March 2026 Audit¶
A full documentation audit was completed on March 19, 2026 and is published at documentation-audit-2026-03-19.md.
The immediate concrete outputs for this step are:
- ~~move
docs/olderreference/todocs/archive/and remove it frommkdocs.ymlnavigation~~ ✅ DONE — renamed todocs/archive/ - ~~copy
GDD.mdintodocs/official/game-design-document.mdand add to mkdocs nav~~ ✅ DONE — GDD now publishes with the docs site - add
site/to.gitignoreand stop committing the MkDocs build output - add a
docsservice torender.yaml(or deploy to Cloudflare Pages) so the doc site is publicly accessible - clarify
FactionTutorials/purpose — either consolidate into GDD or add an index explaining they are standalone narrative-design files - verify telemetry architecture story (Kusto local dev vs. ClickHouse production) and document the canonical split
- verify WPF client actual runtime status against doc claims in
DevelopmentStatus.md
Maintenance Rule¶
When the current next steps change, update this page at the same time as production-roadmap.md and master-todo.md.
If a future work summary or response to the user introduces a new short list of next steps, those steps should be added here in the same working pass instead of being left only in chat.
This page is intentionally short and should remain the easiest place to answer: what should happen next?