37 KiB
First Visible Vertical Slice Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the first visible Ashen Realms world screen with a PostgreSQL-backed demo character and a server-authoritative ten-second travel flow between Südtor von Graufurt and Verbrannte Straße.
Architecture: Preserve the npm-workspace modular monolith: Angular renders a signal-driven game shell and NestJS exposes /api REST modules backed by TypeORM/PostgreSQL. Travel start and completion are transactional backend decisions; the browser displays a countdown derived from arrivesAt and refreshes only after the backend reports completion.
Tech Stack: Angular 22, TypeScript, SCSS, Angular Signals, NestJS 11, TypeORM, PostgreSQL, REST, npm Workspaces, Jest, Angular unit-test builder/Vitest.
Spec: docs/superpowers/specs/2026-08-18-first-visible-vertical-slice-design.md
Global Constraints
- Keep
apps/web,apps/api,packages/shared, andpackages/game-content; do not add Nx or Turborepo. - Upgrade the existing Angular 21.2 scaffold to Angular 22; keep NestJS 11.
- Every API route is under
/api; Angular uses only relative/api/...URLs. - TypeORM always uses
synchronize: false; all schema changes are in the checked-in migration. - PostgreSQL is the only runtime data source for character, locations, connections, and travel.
- The browser never supplies
startedAt,arrivesAt, travel duration, origin, or completion status. - Do not implement authentication, hunt, quests, merchants, inventory, equipment, combat, loot, realtime, chat, guilds, CMS, object storage, or microservices.
- Store
ambushChancebut do not evaluate it. - Do not ship any screenshot from
docs/referencesas an application asset. - Preserve the user's existing uncommitted
package-lock.jsonchange and review lockfile diffs before staging. - Before each implementation task, re-read all files in
docs/and inspect all images indocs/references/, as requested by the project prompt.
File map
Backend
apps/api/src/config/database.config.ts: shared Nest/CLI TypeORM options and environment validation.apps/api/src/database/data-source.ts: TypeORM CLI data source.apps/api/src/database/database.module.ts: Nest TypeORM registration.apps/api/src/database/migrations/1787072400000-CreateVisibleVerticalSlice.ts: complete V1 schema.apps/api/src/database/seeds/vertical-slice.seed.ts: exported idempotent seed function.apps/api/src/database/seeds/run-seed.ts: executable seed entry point.apps/api/src/demo/demo-character.constants.ts: backend-only stable demo UUID.apps/api/src/characters/*: Character entity, service, controller, DTO mapping, and module.apps/api/src/world/*: location entities, world service/controller/module, and response mapping.apps/api/src/travel/*: Travel entity/status, clock abstraction, service/controller/module, and DTOs.apps/api/src/health/*: health controller/module.
Frontend
apps/web/src/app/core/api/game-api.service.ts: typed relative HTTP calls.apps/web/src/app/core/api/game-api.models.ts: API response interfaces.apps/web/src/app/features/world/world.store.ts: signals, loading orchestration, countdown, and polling.apps/web/src/app/features/world/world-page.component.*: world composition and user decisions.apps/web/src/app/features/world/location-node.component.*: accessible current/reachable nodes.apps/web/src/app/features/world/travel-panel.component.*: selection/travel/countdown states.apps/web/src/app/layout/*: shell, topbar, side navigation, footer, and context panel.apps/web/src/styles.scss: tokens, reset, typography, and global material rules.apps/web/public/assets/locations/south-gate.webp: original generated scene.apps/web/public/assets/characters/aric-portrait.webp: restrained original generated portrait.
Task 1: Toolchain, scripts, and database configuration
Files:
- Modify:
package.json - Modify:
apps/web/package.json - Modify:
apps/web/angular.json - Modify:
apps/api/package.json - Modify:
.env.example - Create:
apps/web/proxy.conf.json - Create:
apps/api/src/config/database.config.ts - Create:
apps/api/src/config/database.config.spec.ts - Create:
apps/api/src/database/database.module.ts - Create:
apps/api/src/database/data-source.ts - Modify:
apps/api/src/app.module.ts
Interfaces:
-
Consumes:
process.env.DATABASE_URL -
Produces:
createDatabaseConfig(databaseUrl: string): TypeOrmModuleOptions,AppDataSource, root scriptsdb:migrate,db:revert,db:seed,dev:api,dev:web -
Step 1: Re-read project documentation and inspect references
Run:
Get-Content -Raw docs\*.md
Get-ChildItem docs\references -File
Open all three PNG references with the available image viewer. Confirm no implementation file has been touched before this check.
- Step 2: Write the failing database configuration test
import { createDatabaseConfig } from './database.config';
describe('createDatabaseConfig', () => {
it('uses postgres and permanently disables synchronization', () => {
expect(createDatabaseConfig('postgresql://test:test@localhost/test')).toMatchObject({
type: 'postgres',
url: 'postgresql://test:test@localhost/test',
synchronize: false,
autoLoadEntities: true,
});
});
it('rejects an empty database URL', () => {
expect(() => createDatabaseConfig('')).toThrow('DATABASE_URL is required');
});
});
- Step 3: Run the focused test and confirm RED
Run: npm test --workspace=@ashen-realms/api -- database.config.spec.ts --runInBand
Expected: FAIL because database.config.ts does not exist.
- Step 4: Upgrade Angular and install backend persistence dependencies
Run:
npm install --workspace=@ashen-realms/web @angular/common@^22.0.0 @angular/compiler@^22.0.0 @angular/core@^22.0.0 @angular/forms@^22.0.0 @angular/platform-browser@^22.0.0 @angular/router@^22.0.0 @angular/build@^22.0.0 @angular/cli@^22.0.0 @angular/compiler-cli@^22.0.0
npm install --workspace=@ashen-realms/api @nestjs/typeorm typeorm pg dotenv class-validator class-transformer
Review package-lock.json; retain pre-existing content and stage it only with the dependency changes that npm actually produced.
- Step 5: Implement strict database configuration
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export function createDatabaseConfig(databaseUrl: string): TypeOrmModuleOptions {
if (!databaseUrl.trim()) {
throw new Error('DATABASE_URL is required');
}
return {
type: 'postgres',
url: databaseUrl,
autoLoadEntities: true,
synchronize: false,
};
}
Register TypeOrmModule.forRootAsync in DatabaseModule, load .env in the CLI data source with dotenv/config, and use source paths in ts-node plus compiled paths in production:
entities: [__dirname + '/../**/*.entity.{ts,js}'],
migrations: [__dirname + '/migrations/*.{ts,js}'],
synchronize: false,
- Step 6: Add relative Angular proxy and stable workspace scripts
apps/web/proxy.conf.json:
{
"/api": {
"target": "http://localhost:3000",
"secure": false,
"changeOrigin": true
}
}
Configure the Angular development serve target with proxyConfig. Add API TypeORM scripts using typeorm-ts-node-commonjs or the repository's working ts-node CLI form, then expose them at the root.
- Step 7: Run test and configuration builds
Run:
npm test --workspace=@ashen-realms/api -- database.config.spec.ts --runInBand
npm run build:web
npm run build:api
Expected: test PASS and both builds PASS.
- Step 8: Commit the coherent toolchain change
git add -- package.json package-lock.json apps/web/package.json apps/web/angular.json apps/web/proxy.conf.json apps/api/package.json apps/api/src/config apps/api/src/database/database.module.ts apps/api/src/database/data-source.ts apps/api/src/app.module.ts .env.example
git commit -m "chore: configure angular and postgres foundation"
Task 2: TypeORM entities and explicit migration
Files:
- Create:
apps/api/src/world/entities/location-definition.entity.ts - Create:
apps/api/src/world/entities/location-connection.entity.ts - Create:
apps/api/src/characters/entities/character.entity.ts - Create:
apps/api/src/travel/entities/travel.entity.ts - Create:
apps/api/src/travel/travel-status.enum.ts - Create:
apps/api/src/database/migrations/1787072400000-CreateVisibleVerticalSlice.ts - Create:
apps/api/src/database/migrations/visible-vertical-slice.migration.spec.ts
Interfaces:
-
Consumes: TypeORM
Repository,MigrationInterface, PostgreSQL UUID andtimestamptz -
Produces:
Character,LocationDefinition,LocationConnection,Travel,TravelStatus -
Step 1: Re-read docs and inspect references
Repeat the Task 1 documentation/reference check before editing.
- Step 2: Write failing entity metadata and migration tests
Use TypeORM metadata storage to assert the unique location key and relation join columns, and inspect the migration source to assert it creates all tables while never enabling synchronization:
expect(locationMetadata.indices.some((index) => index.columns?.includes('key') && index.options.unique)).toBe(true);
expect(migrationText).toContain('CREATE TABLE "characters"');
expect(migrationText).toContain('CREATE UNIQUE INDEX "IDX_active_travel_per_character"');
expect(migrationText).not.toContain('synchronize');
- Step 3: Run focused tests and confirm RED
Run: npm test --workspace=@ashen-realms/api -- visible-vertical-slice.migration.spec.ts --runInBand
Expected: FAIL because entities and migration do not exist.
- Step 4: Implement the four entities
Use explicit table/column names, UUID primary keys, @CreateDateColumn({ type: 'timestamptz' }), @UpdateDateColumn, and relations with scalar FK fields. The connection probability is:
@Column({ name: 'ambush_chance', type: 'numeric', precision: 5, scale: 4 })
ambushChance!: string;
The travel status is:
export enum TravelStatus {
TRAVELLING = 'TRAVELLING',
COMPLETED = 'COMPLETED',
}
- Step 5: Write the complete reversible migration
Create enum/table/index SQL in dependency order. Include:
CREATE UNIQUE INDEX "IDX_location_connection_direction"
ON "location_connections" ("from_location_id", "to_location_id");
CREATE UNIQUE INDEX "IDX_active_travel_per_character"
ON "travels" ("character_id")
WHERE "status" = 'TRAVELLING';
The down method drops indexes, tables in reverse FK order, and the travel enum.
- Step 6: Run tests and compile the migration
Run:
npm test --workspace=@ashen-realms/api -- visible-vertical-slice.migration.spec.ts --runInBand
npm run build:api
Expected: PASS.
- Step 7: Commit schema and migration
git add -- apps/api/src/world/entities apps/api/src/characters/entities apps/api/src/travel/entities apps/api/src/travel/travel-status.enum.ts apps/api/src/database/migrations
git commit -m "feat: add world travel database schema"
Task 3: Idempotent demo seed
Files:
- Create:
apps/api/src/demo/demo-character.constants.ts - Create:
apps/api/src/database/seeds/vertical-slice.constants.ts - Create:
apps/api/src/database/seeds/vertical-slice.seed.ts - Create:
apps/api/src/database/seeds/vertical-slice.seed.spec.ts - Create:
apps/api/src/database/seeds/run-seed.ts - Modify:
apps/api/package.json - Modify:
package.json
Interfaces:
-
Consumes:
DataSource, entity repositories -
Produces:
DEMO_CHARACTER_ID,seedVisibleVerticalSlice(dataSource: DataSource): Promise<void>,npm run db:seed -
Step 1: Re-read docs and inspect references
Repeat the required documentation/reference check.
- Step 2: Write a failing idempotency test with mocked repositories
Assert that locations are upserted by key, both directed connections are upserted by the unique pair, and the character insert uses the stable demo UUID. Call the seed twice and verify the fake repository still contains exactly two locations, two connections, and one character.
- Step 3: Confirm RED
Run: npm test --workspace=@ashen-realms/api -- vertical-slice.seed.spec.ts --runInBand
Expected: FAIL because the seed function is missing.
- Step 4: Implement stable constants and content
Use fixed valid UUIDs:
export const DEMO_CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
export const SOUTH_GATE_ID = '20000000-0000-4000-8000-000000000001';
export const BURNED_ROAD_ID = '20000000-0000-4000-8000-000000000002';
Seed German descriptions consistent with the world-content design. Set artwork paths to /assets/locations/south-gate.webp and /assets/locations/burned-road.webp; both initially resolve to the same panoramic scene so the path contract is ready for later replacement.
- Step 5: Preserve live demo state on re-seed
Upsert location definitions and connections. Insert the character only when absent:
const existing = await characterRepository.findOneBy({ id: DEMO_CHARACTER_ID });
if (!existing) {
await characterRepository.insert({
id: DEMO_CHARACTER_ID,
name: 'Aric Duskwalker',
level: 1,
experience: 0,
baseHp: 100,
baseAttack: 6,
currentHp: 100,
currentLocationId: SOUTH_GATE_ID,
});
}
- Step 6: Add executable seed command and verify GREEN
Run: npm test --workspace=@ashen-realms/api -- vertical-slice.seed.spec.ts --runInBand
Expected: PASS.
- Step 7: Commit seed
git add -- package.json apps/api/package.json apps/api/src/demo apps/api/src/database/seeds
git commit -m "feat: seed demo character and first locations"
Task 4: Health and demo-character API
Files:
- Delete:
apps/api/src/app.controller.ts - Delete:
apps/api/src/app.controller.spec.ts - Delete:
apps/api/src/app.service.ts - Create:
apps/api/src/health/health.controller.ts - Create:
apps/api/src/health/health.controller.spec.ts - Create:
apps/api/src/health/health.module.ts - Create:
apps/api/src/characters/characters.service.ts - Create:
apps/api/src/characters/characters.service.spec.ts - Create:
apps/api/src/characters/characters.controller.ts - Create:
apps/api/src/characters/characters.module.ts - Modify:
apps/api/src/app.module.ts - Modify:
apps/api/src/main.ts
Interfaces:
-
Consumes:
Repository<Character>,DEMO_CHARACTER_ID -
Produces:
GET /api/health,GET /api/characters/me,CharactersService.getDemoCharacter() -
Step 1: Re-read docs and inspect references
Repeat the required check.
- Step 2: Write failing health and character tests
expect(new HealthController().getHealth()).toEqual({ status: 'ok' });
expect(await service.getDemoCharacter()).toEqual({
id: DEMO_CHARACTER_ID,
name: 'Aric Duskwalker',
level: 1,
experience: 0,
currentHp: 100,
maxHp: 100,
attack: 6,
currentLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'Südtor von Graufurt',
},
});
The fake repository returns a character with its currentLocation relation.
- Step 3: Confirm RED
Run: npm test --workspace=@ashen-realms/api -- health.controller.spec.ts characters.service.spec.ts --runInBand
Expected: FAIL because controllers/services do not exist.
- Step 4: Implement focused modules and DTO mapping
Use findOne({ where: { id: DEMO_CHARACTER_ID }, relations: { currentLocation: true } }); throw NotFoundException if the seed has not run. Map baseHp to maxHp and baseAttack to attack in the service response.
- Step 5: Set global prefix
In main.ts:
app.setGlobalPrefix('api');
app.enableShutdownHooks();
await app.listen(process.env.PORT ?? 3000);
- Step 6: Verify GREEN and build
Run:
npm test --workspace=@ashen-realms/api -- health.controller.spec.ts characters.service.spec.ts --runInBand
npm run build:api
- Step 7: Commit API foundation
git add -- apps/api/src
git commit -m "feat: expose health and demo character APIs"
Task 5: Server-authoritative TravelService and endpoints
Files:
- Create:
apps/api/src/travel/clock.ts - Create:
apps/api/src/travel/dto/start-travel.dto.ts - Create:
apps/api/src/travel/travel.errors.ts - Create:
apps/api/src/travel/travel.service.ts - Create:
apps/api/src/travel/travel.service.spec.ts - Create:
apps/api/src/travel/travel.controller.ts - Create:
apps/api/src/travel/travel.module.ts - Modify:
apps/api/src/app.module.ts
Interfaces:
-
Consumes:
DataSource,Clock.now(): Date,DEMO_CHARACTER_ID -
Produces:
TravelService.startTravel(characterId: string, targetLocationId: string),TravelService.getCurrentTravel(characterId: string),TravelService.completeTravelIfDue(characterId: string),POST /api/travel,GET /api/travel/current -
Step 1: Re-read docs and inspect references
Repeat the required check.
- Step 2: Write the five required failing service tests
Create deterministic fixtures and an in-memory fake transaction manager. Cover:
it('starts travel for an enabled directed connection');
it('rejects a target without an enabled connection');
it('derives arrivesAt from the injected clock and connection duration');
it('does not complete or move the character before arrivesAt');
it('completes due travel and updates character location atomically');
For the clock test, inject now = 2026-08-18T10:00:00.000Z, duration 10, and expect 2026-08-18T10:00:10.000Z regardless of request content.
- Step 3: Confirm RED
Run: npm test --workspace=@ashen-realms/api -- travel.service.spec.ts --runInBand
Expected: FAIL because TravelService is missing.
- Step 4: Implement clock and exact request validation
export const CLOCK = Symbol('CLOCK');
export interface Clock { now(): Date; }
export const systemClock: Clock = { now: () => new Date() };
export class StartTravelDto {
@IsUUID()
targetLocationId!: string;
}
Enable a global ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }) so timestamps and duration fields are rejected.
- Step 5: Implement start and completion transactions
Within dataSource.transaction, lock character and active travel rows. Validate target existence, connection direction, enabled, and absence of active travel. Calculate:
const startedAt = this.clock.now();
const arrivesAt = new Date(startedAt.getTime() + connection.travelDurationSeconds * 1000);
Completion compares travel.arrivesAt.getTime() with clock.now().getTime(). Only when due, set status and character location and save both in the same transaction.
- Step 6: Map public responses
Return location summaries from loaded relations. Use { status: 'IDLE' } when no travel exists, the full travelling shape while active, and { status: 'COMPLETED', targetLocation } on the request that completes it.
- Step 7: Verify all five tests and controller compilation
Run:
npm test --workspace=@ashen-realms/api -- travel.service.spec.ts --runInBand
npm run build:api
Expected: five behavior tests PASS.
- Step 8: Commit travel domain
git add -- apps/api/src/travel apps/api/src/main.ts apps/api/src/app.module.ts
git commit -m "feat: add server authoritative travel flow"
Task 6: Current-location WorldService and endpoint
Files:
- Create:
apps/api/src/world/world.service.ts - Create:
apps/api/src/world/world.service.spec.ts - Create:
apps/api/src/world/world.controller.ts - Create:
apps/api/src/world/world.module.ts - Modify:
apps/api/src/app.module.ts
Interfaces:
-
Consumes:
TravelService.completeTravelIfDue(characterId),Repository<Character>,Repository<LocationConnection> -
Produces:
WorldService.getCurrentLocation(characterId),GET /api/world/current-location -
Step 1: Re-read docs and inspect references
Repeat the required check.
- Step 2: Write failing world mapping tests
Test that the service invokes travel completion before reading the character, returns the complete location DTO, filters disabled connections, and maps a 0.05 ambush chance to LOW without exposing the raw probability.
expect(result.connections[0]).toEqual({
targetLocation: {
id: BURNED_ROAD_ID,
key: 'burned-road',
name: 'Verbrannte Straße',
},
travelDurationSeconds: 10,
danger: 'LOW',
});
- Step 3: Confirm RED
Run: npm test --workspace=@ashen-realms/api -- world.service.spec.ts --runInBand
- Step 4: Implement WorldService and module relationship
Export TravelService from TravelModule, import it in WorldModule, and query enabled outgoing connections with targetLocation. Avoid circular imports: TravelModule depends on entities, not WorldService.
- Step 5: Verify GREEN and all backend tests
Run:
npm test --workspace=@ashen-realms/api -- world.service.spec.ts --runInBand
npm test --workspace=@ashen-realms/api -- --runInBand
npm run build:api
- Step 6: Commit world API
git add -- apps/api/src/world apps/api/src/app.module.ts
git commit -m "feat: expose current world location"
Task 7: Angular typed API and signal-driven WorldStore
Files:
- Create:
apps/web/src/app/core/api/game-api.models.ts - Create:
apps/web/src/app/core/api/game-api.service.ts - Create:
apps/web/src/app/core/api/game-api.service.spec.ts - Create:
apps/web/src/app/features/world/world.store.ts - Create:
apps/web/src/app/features/world/world.store.spec.ts - Modify:
apps/web/src/app/app.config.ts
Interfaces:
-
Consumes:
/api/characters/me,/api/world/current-location,/api/travel,/api/travel/current -
Produces:
GameApiService,WorldStore.load(),WorldStore.selectConnection(),WorldStore.startTravel(), read-only state signals -
Step 1: Re-read docs and inspect references
Repeat the required check.
- Step 2: Write failing relative-URL and request-body tests
With Angular HTTP testing utilities:
service.startTravel('target-uuid').subscribe();
const request = http.expectOne('/api/travel');
expect(request.request.method).toBe('POST');
expect(request.request.body).toEqual({ targetLocationId: 'target-uuid' });
Also verify all GET URLs begin with /api/ and contain no hostname.
- Step 3: Write failing store behavior tests
Use a fake API service and fake timer clock. Cover:
-
load()requests character, location, and current travel. -
startTravel()passes only the selected target ID. -
remainingSecondsis computed fromarrivesAt. -
reaching zero calls
getCurrentTravel()and does not assign a new location locally. -
only
COMPLETEDcauses character/location reload. -
Step 4: Confirm RED
Run: npm test --workspace=@ashen-realms/web -- --watch=false
Expected: FAIL because services/store do not exist.
- Step 5: Implement typed contracts and API service
Define discriminated travel types:
export type CurrentTravel =
| { status: 'IDLE' }
| { status: 'TRAVELLING'; originLocation: LocationSummary; targetLocation: LocationSummary; startedAt: string; arrivesAt: string }
| { status: 'COMPLETED'; targetLocation: LocationSummary };
Use HttpClient only with literal relative paths.
- Step 6: Implement WorldStore countdown without local completion
Use writable private signals and public asReadonly() signals. Calculate presentation time with:
Math.max(0, Math.ceil((Date.parse(arrivesAt) - Date.now()) / 1000));
When zero is reached, clear the interval and await the API response. Never set currentLocation to targetLocation inside the timer.
- Step 7: Verify GREEN
Run:
npm test --workspace=@ashen-realms/web -- --watch=false
npm run build:web
- Step 8: Commit frontend data flow
git add -- apps/web/src/app/core apps/web/src/app/features/world/world.store.ts apps/web/src/app/features/world/world.store.spec.ts apps/web/src/app/app.config.ts
git commit -m "feat: add world api state store"
Task 8: Reusable Angular application shell
Files:
- Replace:
apps/web/src/app/app.ts - Replace:
apps/web/src/app/app.html - Replace:
apps/web/src/app/app.scss - Replace:
apps/web/src/app/app.spec.ts - Modify:
apps/web/src/app/app.routes.ts - Create:
apps/web/src/app/layout/app-shell/app-shell.component.* - Create:
apps/web/src/app/layout/top-bar/top-bar.component.* - Create:
apps/web/src/app/layout/side-navigation/side-navigation.component.* - Create:
apps/web/src/app/layout/game-footer/game-footer.component.* - Create:
apps/web/src/app/layout/context-panel/context-panel.component.* - Modify:
apps/web/src/styles.scss
Interfaces:
-
Consumes:
WorldStore.character, Angular Router -
Produces:
AppShellComponent,TopBarComponent,SideNavigationComponent,GameFooterComponent,ContextPanelComponent,/ -> /world -
Step 1: Re-read docs and inspect references
Repeat the required check.
- Step 2: Write the failing app-shell test
expect(fixture.nativeElement.querySelector('app-top-bar')).not.toBeNull();
expect(fixture.nativeElement.querySelector('app-side-navigation')).not.toBeNull();
expect(fixture.nativeElement.querySelector('main')).not.toBeNull();
expect(fixture.nativeElement.querySelector('app-game-footer')).not.toBeNull();
Also assert Karte is enabled/active and Jagd, Quests, Inventar, and Charakter are disabled. Assert Shop is absent.
- Step 3: Confirm RED
Run: npm test --workspace=@ashen-realms/web -- --watch=false
- Step 4: Define global design tokens
At :root, define at minimum:
--ar-bg: #090b0d;
--ar-panel: #101316;
--ar-panel-muted: #17191b;
--ar-border: #554a39;
--ar-border-highlight: #9b7a42;
--ar-text: #ede7da;
--ar-text-muted: #aaa397;
--ar-gold: #c9a45f;
--ar-blue: #5ca9d8;
--ar-success: #78b96e;
--ar-warning: #d69445;
--ar-danger: #c74b42;
Add spacing, radius, shadow, and motion tokens plus a dark global reset.
- Step 5: Implement focused standalone shell components
Topbar receives character data from the store and renders no hardcoded stats. Use semantic <nav>, <main>, and <footer>. Disabled nav buttons use disabled and explanatory accessible labels.
- Step 6: Configure routing
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'world' },
{
path: '',
component: AppShellComponent,
children: [
{ path: 'world', loadComponent: () => import('./features/world/world-page.component').then((m) => m.WorldPageComponent) },
],
},
{ path: '**', redirectTo: 'world' },
];
- Step 7: Verify shell tests and build
Run:
npm test --workspace=@ashen-realms/web -- --watch=false
npm run build:web
- Step 8: Commit shell
git add -- apps/web/src/app apps/web/src/styles.scss
git commit -m "feat: build dark fantasy application shell"
Task 9: Original artwork and interactive world screen
Files:
- Create:
apps/web/public/assets/locations/south-gate.webp - Create:
apps/web/public/assets/locations/burned-road.webp - Create:
apps/web/public/assets/characters/aric-portrait.webp - Create:
apps/web/src/app/features/world/world-page.component.* - Create:
apps/web/src/app/features/world/world-page.component.spec.ts - Create:
apps/web/src/app/features/world/location-node.component.* - Create:
apps/web/src/app/features/world/travel-panel.component.* - Modify:
apps/web/src/app/layout/context-panel/context-panel.component.*
Interfaces:
-
Consumes:
WorldStore, generated bitmap assets, API-provided artwork paths -
Produces: selected node state, travel panel, countdown presentation, responsive dark-fantasy world screen
-
Step 1: Re-read docs and inspect all three references
Record the world-reference inventory before coding: exact requested copy, layout regions, dominant colors, panel density, node states, button treatment, and the prohibition on using the screenshot as an asset.
- Step 2: Invoke the imagegen skill and create original assets
Generate a 16:9 or wider dark-fantasy panorama with this content direction:
Original premium dark-fantasy game environment, no UI and no text: the fortified south gate of a grim medieval city on the left opens onto a scorched road crossing ash-covered fields toward distant ruined watchtowers and a smoking volcanic horizon. Charcoal stone, cold storm light, sparse warm embers and braziers, readable travel path, atmospheric smoke, realistic painterly game-key-art detail, restrained colors, empty central/right areas suitable for interactive map nodes. Do not reproduce any supplied screenshot composition exactly.
Generate a matching hooded male adventurer portrait without logos or text. Generate a second distinct burned-road scene using the same palette and lighting direction. Convert/export all three results as WebP: panoramas at 1920×1080 and portrait at 512×512.
- Step 3: Write failing WorldPage tests
Test that ngOnInit calls store.load(), clicking the burned-road node calls selectConnection, the panel displays Ziel/Reisezeit/Gefahr from the selected connection, and clicking Reise beginnen calls store.startTravel().
- Step 4: Confirm RED
Run: npm test --workspace=@ashen-realms/web -- --watch=false
- Step 5: Implement accessible world composition
Use a scene <section> with API artwork as the background. Render two code-native <button> nodes and a code-native SVG path overlay. Current and reachable states must contain both text and visual state. Do not embed labels into the bitmap.
- Step 6: Implement travel and context states
The travel panel has three modes:
No selection -> concise instruction
Selected -> Ziel, Reisezeit, Gefahr, Reise beginnen
TRAVELLING -> destination, arrivesAt-derived countdown, disabled action
The context panel shows description, recommended level, safe/hunting status, and the selected/current location. Errors appear as an inline framed status with retry.
- Step 7: Implement responsive scene styling
Match the reference hierarchy: artwork dominates; the right panel is dense but subordinate; bronze borders are thin; radii are restrained; nodes are legible at 1366×768. Add subtle smoke/ember motion only with CSS opacity/transform and disable it under prefers-reduced-motion.
- Step 8: Verify tests and build
Run:
npm test --workspace=@ashen-realms/web -- --watch=false
npm run build:web
- Step 9: Commit world UI and assets
git add -- apps/web/public/assets apps/web/src/app/features/world apps/web/src/app/layout/context-panel
git commit -m "feat: render interactive ashen fields world"
Task 10: Migration/seed smoke test and README handoff
Files:
- Create:
README.md - Modify:
.env.example - Modify:
package.json - Create:
apps/api/test/visible-slice.e2e-spec.ts - Modify:
apps/api/test/jest-e2e.json
Interfaces:
-
Consumes: PostgreSQL
ashen_realms, all API endpoints and root scripts -
Produces: reproducible
Migration -> Seed -> API -> Webinstructions and API smoke evidence -
Step 1: Re-read docs and inspect references
Repeat the required check.
- Step 2: Write an API e2e smoke test
Using Nest testing and Supertest, assert /api/health returns { status: 'ok' }. When DATABASE_URL is available, also assert seeded character/world responses. Keep unit tests independent from a developer database.
- Step 3: Run migration and seed against a clean local PostgreSQL database
Run:
npm run db:migrate
npm run db:seed
npm run db:seed
Query counts or call APIs to confirm the second seed did not create duplicates.
- Step 4: Start API and verify routes
Run API in a hidden/background process, then request:
GET /api/health
GET /api/characters/me
GET /api/world/current-location
GET /api/travel/current
POST /api/travel
Confirm a POST body containing arrivesAt is rejected by the validation pipe.
- Step 5: Write exact README instructions
Document:
npm install
Copy-Item .env.example .env
npm run db:migrate
npm run db:seed
npm run dev:api
npm run dev:web
State database name ashen_realms, default URL, required PostgreSQL availability, ports 3000/4200, and the known first-slice limitations.
- Step 6: Run API tests and both builds
Run:
npm test --workspace=@ashen-realms/api -- --runInBand
npm run test:e2e --workspace=@ashen-realms/api -- --runInBand
npm run build:api
npm run build:web
- Step 7: Commit operation docs and smoke test
git add -- README.md .env.example package.json apps/api/test
git commit -m "docs: add visible slice local workflow"
Task 11: Browser workflow and visual fidelity verification
Files:
- Modify only files with evidenced functional or visual defects discovered during this task.
- Create:
docs/verification/first-visible-slice-fidelity.md - Create:
docs/verification/first-visible-slice-1920x1080.png - Create:
docs/verification/first-visible-slice-1440x900.png - Create:
docs/verification/first-visible-slice-1366x768.png
Interfaces:
-
Consumes: running API, running Angular app,
docs/references/world-travel-screen.png -
Produces: verified browser travel flow, three viewport captures, fidelity ledger, final clean test/build evidence
-
Step 1: Invoke frontend-testing-debugging and verification-before-completion skills
Use Browser/IAB first when available; otherwise record that the Browser plugin is unavailable and use local Playwright. Do not claim completion from build output alone.
- Step 2: Verify the complete interaction path
At http://localhost:4200:
- Confirm
/redirects to/world. - Confirm Aric, level, and HP came from the API.
- Select Verbrannte Straße.
- Confirm Ziel, 10 seconds, and Niedrig.
- Start travel and inspect the network body; it contains only
targetLocationId. - Confirm countdown follows the returned
arrivesAt. - Confirm UI does not switch location at local zero before the completion response.
- Confirm the backend completes travel and UI reloads Verbrannte Straße.
- Use the reverse connection to repeat the flow toward Südtor.
- Stop API and confirm an in-shell error replaces browser alerts.
- Step 3: Capture required desktop viewports
Capture 1920×1080, 1440×900, and 1366×768 after data loads. Ensure primary content, travel panel, context panel, and footer are not clipped.
- Step 4: Inspect reference and implementation images together
Use the image viewer on docs/references/world-travel-screen.png and the latest implementation capture in the same QA pass.
- Step 5: Write the fidelity ledger
Record at least these comparison points with reference evidence, render evidence, and fix/result:
- shell proportions and persistent regions
- artwork dominance and crop
- typography hierarchy and control typography
- dark palette, bronze borders, and blue/current state
- node/path legibility
- travel panel placement and action prominence
- context-panel information density
- responsive behavior at all three sizes
Also record the above-the-fold copy diff. Allowed copy is limited to API content, requested navigation labels, travel labels, and restrained system/footer text.
- Step 6: Fix every material mismatch and rerun visual checks
Do not accept clipping, accidental wrapping, unreadable node labels, generic dashboard cards, missing focus states, broken assets, or invented promotional copy. Re-capture affected sizes after fixes.
- Step 7: Run final verification from a fresh command
Run:
npm test
npm run build:api
npm run build:web
git diff --check
git status --short
Expected: all tests PASS, both builds PASS, no whitespace errors, and only intentional tracked/untracked files remain.
- Step 8: Commit verification evidence and final fixes
git add -- apps docs/verification README.md package.json package-lock.json
git commit -m "test: verify first visible vertical slice"
Final handoff checklist
- Report implemented backend, frontend, migration, seed, assets, and tests.
- Provide exact migration, seed, API, and web commands.
- List the deliberate limitations: demo character, two locations, no ambush evaluation, no later gameplay systems, desktop-first responsive scope.
- Include test/build command results from the final fresh run.
- Include browser method, screenshot paths, native viewport sizes, fidelity ledger summary, copy-diff result, and remaining intentional deviations.
- State whether the implementation was faithfully verified against the accepted design and whether any material mismatch remains.