1021 lines
37 KiB
Markdown
1021 lines
37 KiB
Markdown
# 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`, and `packages/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 `ambushChance` but do not evaluate it.
|
||
- Do not ship any screenshot from `docs/references` as an application asset.
|
||
- Preserve the user's existing uncommitted `package-lock.json` change and review lockfile diffs before staging.
|
||
- Before each implementation task, re-read all files in `docs/` and inspect all images in `docs/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 scripts `db:migrate`, `db:revert`, `db:seed`, `dev:api`, `dev:web`
|
||
|
||
- [ ] **Step 1: Re-read project documentation and inspect references**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```ts
|
||
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:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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`:
|
||
|
||
```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:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```powershell
|
||
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 and `timestamptz`
|
||
- 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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
@Column({ name: 'ambush_chance', type: 'numeric', precision: 5, scale: 4 })
|
||
ambushChance!: string;
|
||
```
|
||
|
||
The travel status is:
|
||
|
||
```ts
|
||
export enum TravelStatus {
|
||
TRAVELLING = 'TRAVELLING',
|
||
COMPLETED = 'COMPLETED',
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Write the complete reversible migration**
|
||
|
||
Create enum/table/index SQL in dependency order. Include:
|
||
|
||
```sql
|
||
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:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```powershell
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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**
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```ts
|
||
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`:
|
||
|
||
```ts
|
||
app.setGlobalPrefix('api');
|
||
app.enableShutdownHooks();
|
||
await app.listen(process.env.PORT ?? 3000);
|
||
```
|
||
|
||
- [ ] **Step 6: Verify GREEN and build**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
npm test --workspace=@ashen-realms/api -- health.controller.spec.ts characters.service.spec.ts --runInBand
|
||
npm run build:api
|
||
```
|
||
|
||
- [ ] **Step 7: Commit API foundation**
|
||
|
||
```powershell
|
||
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:
|
||
|
||
```ts
|
||
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**
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```powershell
|
||
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.
|
||
|
||
```ts
|
||
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:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```powershell
|
||
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:
|
||
|
||
```ts
|
||
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.
|
||
- `remainingSeconds` is computed from `arrivesAt`.
|
||
- reaching zero calls `getCurrentTravel()` and does not assign a new location locally.
|
||
- only `COMPLETED` causes 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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```powershell
|
||
npm test --workspace=@ashen-realms/web -- --watch=false
|
||
npm run build:web
|
||
```
|
||
|
||
- [ ] **Step 8: Commit frontend data flow**
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```ts
|
||
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:
|
||
|
||
```scss
|
||
--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**
|
||
|
||
```ts
|
||
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:
|
||
|
||
```powershell
|
||
npm test --workspace=@ashen-realms/web -- --watch=false
|
||
npm run build:web
|
||
```
|
||
|
||
- [ ] **Step 8: Commit shell**
|
||
|
||
```powershell
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```powershell
|
||
npm test --workspace=@ashen-realms/web -- --watch=false
|
||
npm run build:web
|
||
```
|
||
|
||
- [ ] **Step 9: Commit world UI and assets**
|
||
|
||
```powershell
|
||
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 -> Web` instructions 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:
|
||
|
||
```powershell
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```powershell
|
||
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:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```powershell
|
||
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`:
|
||
|
||
1. Confirm `/` redirects to `/world`.
|
||
2. Confirm Aric, level, and HP came from the API.
|
||
3. Select Verbrannte Straße.
|
||
4. Confirm Ziel, 10 seconds, and Niedrig.
|
||
5. Start travel and inspect the network body; it contains only `targetLocationId`.
|
||
6. Confirm countdown follows the returned `arrivesAt`.
|
||
7. Confirm UI does not switch location at local zero before the completion response.
|
||
8. Confirm the backend completes travel and UI reloads Verbrannte Straße.
|
||
9. Use the reverse connection to repeat the flow toward Südtor.
|
||
10. 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:
|
||
|
||
```powershell
|
||
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**
|
||
|
||
```powershell
|
||
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.
|