diff --git a/.superpowers/sdd/2026-08-18-first-visible-vertical-slice/task-9-report.md b/.superpowers/sdd/2026-08-18-first-visible-vertical-slice/task-9-report.md new file mode 100644 index 0000000..0ccab9a --- /dev/null +++ b/.superpowers/sdd/2026-08-18-first-visible-vertical-slice/task-9-report.md @@ -0,0 +1,36 @@ +# Task 9 Report: World UI + +## Reference inventory + +- `world-travel-screen.png`: persistent top bar and left navigation, a dominant central map, labelled current/reachable nodes with a dotted path, dense right context, and a bottom-centred travel decision panel. +- `hunting-screen.png`: confirms the shared dark iron/bronze material treatment, restrained gold highlights, readable serif display hierarchy, and dense information without white cards or modern pills. +- `combat-screen.png`: confirms the same persistent shell, dark desaturated palette, thin ornate borders, and blue only as a controlled active-state accent. + +The reference screenshots were inspected but are not shipped or referenced by the application. + +## Artwork and layout decision + +- No ImageGen was used: the controller ruling makes the delivered user artwork final. +- `/images/backgrounds/map_ashen_realm.png` is the dominant WorldPage scene. Its wide path, city, watchtower, and ash-field composition provides a clearer interactive map than either location image. +- The server-delivered location paths are `/images/backgrounds/Suedtor.png` and `/images/backgrounds/Aschestrasse.png`; the ContextPanel renders the authoritative current-location image, so it changes only after server-confirmed arrival. +- This deliberately supersedes the older brief wording that asked for generated assets and API artwork as the main scene background. It preserves the binding controller ruling while retaining the server-provided artwork as context. + +## Delivered behavior + +- `WorldPageComponent` loads `WorldStore` on init, renders a code-native SVG connection and two code-native location buttons, shows loading/error/retry states, and never advances location locally. +- `LocationNodeComponent` exposes current, selected, reachable, disabled, hover, and focus states with textual labels. +- `TravelPanelComponent` presents no-selection guidance, selected target/travel-time/danger with `Reise beginnen`, and an arrivesAt-derived travelling countdown with disabled action. +- `ContextPanelComponent` renders the current/selected location, description, recommended level, safe status, hunting status, and server artwork. +- Seed paths now match the delivered assets and are covered by the API seed test. + +## Verification + +- RED confirmed: four new WorldPage contracts failed against the empty placeholder; seed artwork-path assertion failed against the former WebP paths. +- Focused WorldPage test: 4/4 pass. +- Full web tests: 18/18 pass. +- Affected API seed tests: 2/2 pass. +- `npm run build:web` and `npm run build:api` pass. + +## Remaining concern + +Rendered browser fidelity QA is intentionally deferred to Task 11, as instructed. The Browser plugin is unavailable and no Playwright fallback was started for this task. diff --git a/apps/api/src/database/seeds/vertical-slice.seed.spec.ts b/apps/api/src/database/seeds/vertical-slice.seed.spec.ts index e4f3a66..d13e2bb 100644 --- a/apps/api/src/database/seeds/vertical-slice.seed.spec.ts +++ b/apps/api/src/database/seeds/vertical-slice.seed.spec.ts @@ -115,6 +115,18 @@ describe('seedVisibleVerticalSlice', () => { expect.objectContaining({ id: DEMO_CHARACTER_ID }), ); expect(locationRepository.rows).toHaveLength(2); + expect(locationRepository.rows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: 'south-gate', + artworkPath: '/images/backgrounds/Suedtor.png', + }), + expect.objectContaining({ + key: 'burned-road', + artworkPath: '/images/backgrounds/Aschestrasse.png', + }), + ]), + ); expect(connectionRepository.rows).toHaveLength(2); expect(characterRepository.rows).toHaveLength(1); expect(characterRepository.rows[0]).toEqual( diff --git a/apps/api/src/database/seeds/vertical-slice.seed.ts b/apps/api/src/database/seeds/vertical-slice.seed.ts index 2f28ded..3909a45 100644 --- a/apps/api/src/database/seeds/vertical-slice.seed.ts +++ b/apps/api/src/database/seeds/vertical-slice.seed.ts @@ -25,7 +25,7 @@ export async function seedVisibleVerticalSlice( dangerLevel: 0, isSafe: true, huntingEnabled: false, - artworkPath: '/assets/locations/south-gate.webp', + artworkPath: '/images/backgrounds/Suedtor.png', }, { id: BURNED_ROAD_ID, @@ -39,7 +39,7 @@ export async function seedVisibleVerticalSlice( dangerLevel: 1, isSafe: false, huntingEnabled: true, - artworkPath: '/assets/locations/burned-road.webp', + artworkPath: '/images/backgrounds/Aschestrasse.png', }, ]; let southGateId = SOUTH_GATE_ID; diff --git a/apps/web/public/images/backgrounds/Aschestrasse.png b/apps/web/public/images/backgrounds/Aschestrasse.png new file mode 100644 index 0000000..43904a7 Binary files /dev/null and b/apps/web/public/images/backgrounds/Aschestrasse.png differ diff --git a/apps/web/public/images/backgrounds/Suedtor.png b/apps/web/public/images/backgrounds/Suedtor.png new file mode 100644 index 0000000..2ac5f95 Binary files /dev/null and b/apps/web/public/images/backgrounds/Suedtor.png differ diff --git a/apps/web/public/images/backgrounds/map_ashen_realm.png b/apps/web/public/images/backgrounds/map_ashen_realm.png new file mode 100644 index 0000000..7789d57 Binary files /dev/null and b/apps/web/public/images/backgrounds/map_ashen_realm.png differ diff --git a/apps/web/src/app/app.spec.ts b/apps/web/src/app/app.spec.ts index 04c4311..1bcb192 100644 --- a/apps/web/src/app/app.spec.ts +++ b/apps/web/src/app/app.spec.ts @@ -8,9 +8,13 @@ import { routes } from './app.routes'; describe('App', () => { let character: WritableSignal; + let currentLocation: WritableSignal; + let selectedConnection: WritableSignal; beforeEach(async () => { character = signal(null); + currentLocation = signal(null); + selectedConnection = signal(null); await TestBed.configureTestingModule({ imports: [AppShellComponent], @@ -18,7 +22,7 @@ describe('App', () => { provideRouter([]), { provide: WorldStore, - useValue: { character }, + useValue: { character, currentLocation, selectedConnection }, }, ], }).compileComponents(); diff --git a/apps/web/src/app/features/world/location-node.component.html b/apps/web/src/app/features/world/location-node.component.html new file mode 100644 index 0000000..3a9499d --- /dev/null +++ b/apps/web/src/app/features/world/location-node.component.html @@ -0,0 +1,17 @@ + diff --git a/apps/web/src/app/features/world/location-node.component.scss b/apps/web/src/app/features/world/location-node.component.scss new file mode 100644 index 0000000..b6d17ae --- /dev/null +++ b/apps/web/src/app/features/world/location-node.component.scss @@ -0,0 +1,65 @@ +:host { + display: block; +} + +.location-node { + display: grid; + justify-items: center; + gap: 0.15rem; + min-inline-size: 8.5rem; + padding: 0; + border: 0; + color: var(--ar-text); + background: transparent; + cursor: pointer; + font: inherit; + text-align: center; + text-shadow: 0 0.15rem 0.55rem rgb(0 0 0 / 0.95); +} + +.location-node:disabled { + cursor: default; + opacity: 1; +} + +.location-node__marker { + inline-size: 1.35rem; + block-size: 1.35rem; + border: 0.18rem solid var(--ar-gold); + border-radius: 50%; + background: radial-gradient(circle, #211b13 26%, #8d6533 30%, #17191b 68%); + box-shadow: + 0 0 0 0.15rem rgb(6 8 9 / 0.75), + 0 0 0.75rem rgb(208 162 84 / 0.42); +} + +.location-node__name { + font-family: Georgia, 'Times New Roman', serif; + font-size: 1rem; + line-height: 1.2; +} + +.location-node__state { + color: var(--ar-gold); + font-size: var(--ar-font-sm); + font-weight: 700; +} + +.location-node--current .location-node__marker { + border-color: var(--ar-blue); + box-shadow: + 0 0 0 0.15rem rgb(6 8 9 / 0.75), + 0 0 0.9rem rgb(92 169 216 / 0.8); +} + +.location-node--current .location-node__state { + color: var(--ar-success); +} + +.location-node--selected .location-node__marker, +.location-node:not(:disabled):hover .location-node__marker { + border-color: #e1bd72; + box-shadow: + 0 0 0 0.15rem rgb(6 8 9 / 0.75), + 0 0 1rem rgb(225 189 114 / 0.85); +} diff --git a/apps/web/src/app/features/world/location-node.component.ts b/apps/web/src/app/features/world/location-node.component.ts new file mode 100644 index 0000000..49af051 --- /dev/null +++ b/apps/web/src/app/features/world/location-node.component.ts @@ -0,0 +1,15 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { LocationSummary } from '../../core/api/game-api.models'; + +@Component({ + selector: 'app-location-node', + templateUrl: './location-node.component.html', + styleUrl: './location-node.component.scss', +}) +export class LocationNodeComponent { + @Input({ required: true }) location!: LocationSummary; + @Input() current = false; + @Input() selected = false; + @Input() disabled = false; + @Output() readonly choose = new EventEmitter(); +} diff --git a/apps/web/src/app/features/world/travel-panel.component.html b/apps/web/src/app/features/world/travel-panel.component.html new file mode 100644 index 0000000..e8b7146 --- /dev/null +++ b/apps/web/src/app/features/world/travel-panel.component.html @@ -0,0 +1,42 @@ +
+ @if (travellingTravel; as travel) { + REISE LÄUFT +

Reiseziel: {{ travel.targetLocation.name }}

+
+
+
Ankunft in
+
{{ formatDuration(remainingSeconds ?? 0) }}
+
+
+
Ziel
+
{{ travel.targetLocation.name }}
+
+
+ + } @else if (selectedConnection; as connection) { + REISEN +

Zur {{ connection.targetLocation.name }} reisen?

+
+
+
Ziel
+
{{ connection.targetLocation.name }}
+
+
+
Reisezeit
+
{{ formatDuration(connection.travelDurationSeconds) }}
+
+
+
Gefahr
+
+ {{ dangerLabel(connection.danger) }} +
+
+
+ + } @else { + REISEN +

Wähle einen erreichbaren Ort auf der Karte.

+ } +
diff --git a/apps/web/src/app/features/world/travel-panel.component.scss b/apps/web/src/app/features/world/travel-panel.component.scss new file mode 100644 index 0000000..68eed2f --- /dev/null +++ b/apps/web/src/app/features/world/travel-panel.component.scss @@ -0,0 +1,90 @@ +:host { + display: block; +} + +.travel-panel { + padding: var(--ar-space-3) var(--ar-space-4); + border: 1px solid var(--ar-border-highlight); + border-radius: var(--ar-radius-sm); + background: + linear-gradient(125deg, rgb(255 255 255 / 0.045), transparent 42%), rgb(12 15 17 / 0.96); + box-shadow: var(--ar-shadow-raised); + text-align: center; +} + +.travel-panel__eyebrow { + color: var(--ar-gold); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.1em; +} + +.travel-panel h2 { + margin: var(--ar-space-1) 0 var(--ar-space-3); + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.25rem; + font-weight: 400; +} + +.travel-panel__details { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--ar-space-2); + margin: 0 0 var(--ar-space-3); + padding-block: var(--ar-space-2); + border-block: 1px solid rgb(155 122 66 / 0.45); +} + +.travel-panel__details div { + min-inline-size: 0; +} + +.travel-panel dt { + color: var(--ar-text-muted); + font-size: 0.7rem; +} + +.travel-panel dd { + margin: var(--ar-space-1) 0 0; + font-family: Georgia, 'Times New Roman', serif; + font-size: 0.95rem; +} + +.travel-panel__danger--low { + color: var(--ar-success); +} + +.travel-panel button { + inline-size: 100%; + padding: var(--ar-space-2) var(--ar-space-4); + border: 1px solid var(--ar-border-highlight); + border-radius: var(--ar-radius-sm); + color: var(--ar-text); + background: linear-gradient(180deg, #263b4b, #17232d); + cursor: pointer; + font-family: Georgia, 'Times New Roman', serif; + font-size: 1rem; +} + +.travel-panel button:hover:not(:disabled) { + border-color: #d6b26b; + background: linear-gradient(180deg, #315067, #1a2c3a); +} + +.travel-panel button:disabled { + border-color: var(--ar-border); + color: var(--ar-text-muted); + background: #1a1c1d; + cursor: not-allowed; +} + +.travel-panel__instruction { + margin: var(--ar-space-2) 0 0; + color: var(--ar-text-muted); +} + +@media (width < 720px) { + .travel-panel__details { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} diff --git a/apps/web/src/app/features/world/travel-panel.component.ts b/apps/web/src/app/features/world/travel-panel.component.ts new file mode 100644 index 0000000..812e0b5 --- /dev/null +++ b/apps/web/src/app/features/world/travel-panel.component.ts @@ -0,0 +1,32 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CurrentLocationConnection, CurrentTravel } from '../../core/api/game-api.models'; + +@Component({ + selector: 'app-travel-panel', + templateUrl: './travel-panel.component.html', + styleUrl: './travel-panel.component.scss', +}) +export class TravelPanelComponent { + @Input() selectedConnection: CurrentLocationConnection | null = null; + @Input() currentTravel: CurrentTravel | null = null; + @Input() remainingSeconds: number | null = null; + @Input() busy = false; + @Output() readonly travelStart = new EventEmitter(); + + protected get travellingTravel(): Extract | null { + return this.currentTravel?.status === 'TRAVELLING' ? this.currentTravel : null; + } + + protected formatDuration(seconds: number): string { + const clampedSeconds = Math.max(0, seconds); + const hours = Math.floor(clampedSeconds / 3_600); + const minutes = Math.floor((clampedSeconds % 3_600) / 60); + const remainder = clampedSeconds % 60; + + return [hours, minutes, remainder].map((value) => value.toString().padStart(2, '0')).join(':'); + } + + protected dangerLabel(danger: CurrentLocationConnection['danger']): string { + return danger === 'LOW' ? 'Niedrig' : 'Hoch'; + } +} diff --git a/apps/web/src/app/features/world/world-page.component.html b/apps/web/src/app/features/world/world-page.component.html new file mode 100644 index 0000000..35f3a48 --- /dev/null +++ b/apps/web/src/app/features/world/world-page.component.html @@ -0,0 +1,64 @@ +
+ @if (worldStore.currentLocation(); as location) { +
+ +

{{ location.name }}

+ + + + + + @for (connection of location.connections; track connection.targetLocation.id) { + + } + + +
+ } @else { +
+

Weltkarte wird vorbereitet.

+
+ } + + @if (worldStore.loading()) { +

Weltzustand wird geladen…

+ } + + @if (worldStore.error(); as error) { + + } +
diff --git a/apps/web/src/app/features/world/world-page.component.scss b/apps/web/src/app/features/world/world-page.component.scss new file mode 100644 index 0000000..0cbf8e8 --- /dev/null +++ b/apps/web/src/app/features/world/world-page.component.scss @@ -0,0 +1,164 @@ +:host { + display: block; + min-block-size: 100%; +} + +.world-page { + position: relative; + min-block-size: 100%; +} + +.world-page__scene { + position: relative; + isolation: isolate; + min-block-size: clamp(30rem, 67vh, 44rem); + overflow: hidden; + border: 1px solid var(--ar-border); + background-color: #151718; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + box-shadow: var(--ar-shadow-raised); +} + +.world-page__scene::after { + position: absolute; + z-index: -1; + inset: 0; + content: ''; + background: linear-gradient(180deg, rgb(4 6 8 / 0.1), rgb(4 6 8 / 0.45)); + pointer-events: none; +} + +.world-page__atmosphere { + position: absolute; + z-index: -1; + inset: 16% -16%; + background: + radial-gradient(circle at 28% 58%, rgb(210 103 47 / 0.24) 0 0.1rem, transparent 0.22rem), + radial-gradient(circle at 72% 43%, rgb(220 131 56 / 0.2) 0 0.08rem, transparent 0.18rem), + linear-gradient(90deg, transparent, rgb(183 188 194 / 0.08), transparent); + opacity: 0.7; + pointer-events: none; + animation: world-atmosphere-drift 10s ease-in-out infinite alternate; +} + +.world-page__location-title { + position: absolute; + inset: var(--ar-space-5) auto auto var(--ar-space-5); + max-inline-size: calc(100% - 3rem); + margin: 0; + color: var(--ar-text); + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(1.35rem, 2.3vw, 2.2rem); + text-shadow: 0 0.15rem 0.7rem rgb(0 0 0 / 0.9); +} + +.world-page__path { + position: absolute; + z-index: 1; + inline-size: 100%; + block-size: 100%; + overflow: visible; + pointer-events: none; +} + +.world-page__path path { + fill: none; + stroke: var(--ar-gold); + stroke-width: 0.55; + stroke-dasharray: 0.012 0.02; + filter: drop-shadow(0 0 0.3rem rgb(0 0 0 / 0.85)); + opacity: 0.9; +} + +.world-page__node { + position: absolute; + z-index: 2; +} + +.world-page__node--current { + inset: 58% auto auto 12%; +} + +.world-page__node--reachable { + inset: 39% auto auto 60%; +} + +.world-page__travel-panel { + position: absolute; + z-index: 3; + inset: auto 50% var(--ar-space-5) auto; + inline-size: min(29rem, calc(100% - 2rem)); + transform: translateX(50%); +} + +.world-page__loading, +.world-page__error, +.world-page__empty { + margin: var(--ar-space-4) 0 0; + border: 1px solid var(--ar-border); + background: var(--ar-panel); + box-shadow: var(--ar-shadow-raised); +} + +.world-page__loading, +.world-page__empty { + padding: var(--ar-space-4); + color: var(--ar-text-muted); +} + +.world-page__error { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--ar-space-4); + padding: var(--ar-space-3) var(--ar-space-4); + border-color: var(--ar-danger); +} + +.world-page__error p { + margin: 0; +} + +.world-page__error button { + flex: 0 0 auto; + padding: var(--ar-space-2) var(--ar-space-3); + border: 1px solid var(--ar-border-highlight); + border-radius: var(--ar-radius-sm); + color: var(--ar-text); + background: #1a2023; + cursor: pointer; +} + +@keyframes world-atmosphere-drift { + from { + opacity: 0.35; + transform: translateX(-2%); + } + + to { + opacity: 0.75; + transform: translateX(2%); + } +} + +@media (width < 720px) { + .world-page__scene { + min-block-size: 34rem; + } + + .world-page__node--current { + inset-inline-start: 6%; + } + + .world-page__node--reachable { + inset-inline-start: 51%; + } +} + +@media (prefers-reduced-motion: reduce) { + .world-page__atmosphere { + animation: none; + } +} diff --git a/apps/web/src/app/features/world/world-page.component.spec.ts b/apps/web/src/app/features/world/world-page.component.spec.ts new file mode 100644 index 0000000..8db897b --- /dev/null +++ b/apps/web/src/app/features/world/world-page.component.spec.ts @@ -0,0 +1,127 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; +import type { + CurrentLocationConnection, + CurrentLocationResponse, + CurrentTravel, +} from '../../core/api/game-api.models'; +import { WorldStore } from './world.store'; +import { WorldPageComponent } from './world-page.component'; + +const burnedRoadConnection: CurrentLocationConnection = { + targetLocation: { + id: 'burned-road-id', + key: 'burned-road', + name: 'Verbrannte Straße', + }, + travelDurationSeconds: 10, + danger: 'LOW', +}; + +const southGate: CurrentLocationResponse = { + id: 'south-gate-id', + key: 'south-gate', + name: 'Südtor von Graufurt', + description: 'Der letzte sichere Schritt vor den Aschenfeldern.', + regionKey: 'ashen-fields', + minRecommendedLevel: 1, + maxRecommendedLevel: 1, + dangerLevel: 0, + isSafe: true, + huntingEnabled: false, + artworkPath: '/images/backgrounds/Suedtor.png', + connections: [burnedRoadConnection], +}; + +describe('WorldPageComponent', () => { + let selectedConnection: ReturnType>; + let store: { + currentLocation: ReturnType>; + selectedConnection: ReturnType>; + currentTravel: ReturnType>; + remainingSeconds: ReturnType>; + loading: ReturnType>; + error: ReturnType>; + load: () => Promise; + selectConnection: (connection: CurrentLocationConnection | null) => void; + startTravel: () => Promise; + }; + + beforeEach(async () => { + selectedConnection = signal(null); + store = { + currentLocation: signal(southGate), + selectedConnection, + currentTravel: signal({ status: 'IDLE' }), + remainingSeconds: signal(null), + loading: signal(false), + error: signal(null), + load: vi.fn(() => Promise.resolve()), + selectConnection: vi.fn((connection: CurrentLocationConnection | null) => + selectedConnection.set(connection), + ), + startTravel: vi.fn(() => Promise.resolve()), + }; + + await TestBed.configureTestingModule({ + imports: [WorldPageComponent], + providers: [{ provide: WorldStore, useValue: store }], + }).compileComponents(); + }); + + it('loads world state on initialization', () => { + const fixture = TestBed.createComponent(WorldPageComponent); + + fixture.detectChanges(); + + expect(store.load).toHaveBeenCalledOnce(); + }); + + it('selects the burned-road connection and displays its travel details', () => { + const fixture = TestBed.createComponent(WorldPageComponent); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + element.querySelector('[data-location-key="burned-road"]')?.click(); + fixture.detectChanges(); + + expect(store.selectConnection).toHaveBeenCalledWith(burnedRoadConnection); + expect(element.textContent).toContain('Ziel'); + expect(element.textContent).toContain('Verbrannte Straße'); + expect(element.textContent).toContain('00:00:10'); + expect(element.textContent).toContain('Niedrig'); + }); + + it('starts server-authoritative travel from the travel panel', () => { + const fixture = TestBed.createComponent(WorldPageComponent); + fixture.detectChanges(); + store.selectConnection(burnedRoadConnection); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + element.querySelector('[data-travel-start]')?.click(); + + expect(store.startTravel).toHaveBeenCalledOnce(); + }); + + it('renders the server-derived countdown while travel is active without enabling another start', () => { + store.currentTravel.set({ + status: 'TRAVELLING', + originLocation: { id: 'south-gate-id', key: 'south-gate', name: 'Südtor von Graufurt' }, + targetLocation: burnedRoadConnection.targetLocation, + startedAt: '2026-08-19T06:00:00.000Z', + arrivesAt: '2026-08-19T06:00:06.000Z', + }); + store.remainingSeconds.set(6); + const fixture = TestBed.createComponent(WorldPageComponent); + + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + expect(element.textContent).toContain('Reiseziel: Verbrannte Straße'); + expect(element.textContent).toContain('Ankunft in'); + expect(element.textContent).toContain('00:00:06'); + expect(element.querySelector('[data-travel-start]')?.disabled).toBe(true); + }); +}); diff --git a/apps/web/src/app/features/world/world-page.component.ts b/apps/web/src/app/features/world/world-page.component.ts index cd54632..3e950d3 100644 --- a/apps/web/src/app/features/world/world-page.component.ts +++ b/apps/web/src/app/features/world/world-page.component.ts @@ -1,14 +1,30 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; +import { CurrentLocationConnection } from '../../core/api/game-api.models'; +import { LocationNodeComponent } from './location-node.component'; +import { TravelPanelComponent } from './travel-panel.component'; +import { WorldStore } from './world.store'; @Component({ selector: 'app-world-page', - template: '
', - styles: [ - ` - .world-route { - min-block-size: 100%; - } - `, - ], + imports: [LocationNodeComponent, TravelPanelComponent], + templateUrl: './world-page.component.html', + styleUrl: './world-page.component.scss', }) -export class WorldPageComponent {} +export class WorldPageComponent implements OnInit { + protected readonly worldStore = inject(WorldStore); + protected readonly mapBackground = "url('/images/backgrounds/map_ashen_realm.png')"; + + ngOnInit(): void { + void this.worldStore.load(); + } + + protected selectConnection(connection: CurrentLocationConnection): void { + if (this.worldStore.currentTravel()?.status !== 'TRAVELLING') { + this.worldStore.selectConnection(connection); + } + } + + protected retry(): void { + void this.worldStore.load(); + } +} diff --git a/apps/web/src/app/layout/context-panel/context-panel.component.html b/apps/web/src/app/layout/context-panel/context-panel.component.html index b50db00..86d4229 100644 --- a/apps/web/src/app/layout/context-panel/context-panel.component.html +++ b/apps/web/src/app/layout/context-panel/context-panel.component.html @@ -1,4 +1,37 @@ diff --git a/apps/web/src/app/layout/context-panel/context-panel.component.scss b/apps/web/src/app/layout/context-panel/context-panel.component.scss index e71058a..9e5f1ad 100644 --- a/apps/web/src/app/layout/context-panel/context-panel.component.scss +++ b/apps/web/src/app/layout/context-panel/context-panel.component.scss @@ -10,6 +10,14 @@ background: linear-gradient(180deg, rgb(255 255 255 / 0.025), transparent 16%), var(--ar-panel); } +.context-panel h2 { + margin: var(--ar-space-2) 0 var(--ar-space-3); + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.55rem; + font-weight: 400; + line-height: 1.15; +} + .context-panel__eyebrow { display: block; padding-block-end: var(--ar-space-3); @@ -24,3 +32,53 @@ font-size: var(--ar-font-sm); line-height: 1.55; } + +.context-panel__selection { + margin: 0 0 var(--ar-space-3); + color: var(--ar-gold); + font-size: var(--ar-font-sm); +} + +.context-panel__artwork { + display: block; + inline-size: 100%; + aspect-ratio: 16 / 9; + margin-block-end: var(--ar-space-3); + border: 1px solid var(--ar-border); + object-fit: cover; +} + +.context-panel__description { + margin: 0 0 var(--ar-space-4); + color: var(--ar-text-muted); + font-size: var(--ar-font-sm); + line-height: 1.55; +} + +.context-panel__facts { + display: grid; + gap: var(--ar-space-3); + margin: 0; + padding-block-start: var(--ar-space-3); + border-block-start: 1px solid var(--ar-border); +} + +.context-panel__facts div { + display: flex; + justify-content: space-between; + gap: var(--ar-space-3); +} + +.context-panel__facts dt { + color: var(--ar-text-muted); + font-size: var(--ar-font-sm); +} + +.context-panel__facts dd { + margin: 0; + font-family: Georgia, 'Times New Roman', serif; +} + +.context-panel__safe { + color: var(--ar-success); +} diff --git a/apps/web/src/app/layout/context-panel/context-panel.component.ts b/apps/web/src/app/layout/context-panel/context-panel.component.ts index 542ba28..f3e48e4 100644 --- a/apps/web/src/app/layout/context-panel/context-panel.component.ts +++ b/apps/web/src/app/layout/context-panel/context-panel.component.ts @@ -1,8 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; +import { WorldStore } from '../../features/world/world.store'; @Component({ selector: 'app-context-panel', templateUrl: './context-panel.component.html', styleUrl: './context-panel.component.scss', }) -export class ContextPanelComponent {} +export class ContextPanelComponent { + protected readonly worldStore = inject(WorldStore); +} diff --git a/docs/references/Ashen_Realms_Visual_Asset_Style_Guide_V1.md b/docs/references/Ashen_Realms_Visual_Asset_Style_Guide_V1.md new file mode 100644 index 0000000..cbeb20c --- /dev/null +++ b/docs/references/Ashen_Realms_Visual_Asset_Style_Guide_V1.md @@ -0,0 +1,1138 @@ +# Ashen Realms – Visual Asset Style Guide V1 + +## Zweck dieses Dokuments + +Dieses Dokument fasst die visuelle Richtung von **Ashen Realms** für die weitere Grafik- und Asset-Erstellung zusammen. + +Es ist als verbindliche Referenz für zukünftige Bildgenerierungen gedacht. Neue Hintergründe, Monster, Charaktere, NPCs, Items, Icons und UI-Ornamente sollen sich an diesen Regeln orientieren und so wirken, als wären sie von **demselben Art-Team für dieselbe Spielwelt** erstellt worden. + +Die Leitidee lautet: + +> **Klassische Browser-RPG-Struktur, moderne Premium-Dark-Fantasy-Präsentation.** + +Ashen Realms ist kein Pixel-Art-Spiel, kein Cartoon-RPG und kein Mobile-Game. Die Bildsprache ist hochwertig, atmosphärisch, düster, realistisch bis leicht stilisiert und malerisch. + +--- + +# 1. Globale Art Direction + +## 1.1 Grundgefühl + +Alle Assets sollen folgende Eigenschaften kombinieren: + +- moderne Dark-Fantasy-Ästhetik +- hochwertige, realistische bis leicht stilisierte Malerei +- glaubwürdige Materialien +- ernste, erwachsene Stimmung +- kontrollierte Farbpalette +- starke Lichtführung +- hohe Detailqualität +- klare Silhouetten +- keine visuelle Überladung +- keine generische High-Fantasy-Stock-Art + +Die Welt soll alt, beansprucht und gefährlich wirken, aber nicht vollständig zerstört oder apokalyptisch sein. + +## 1.2 Stilniveau + +Der Zielstil liegt zwischen: + +- realistischer Fantasy-Illustration +- hochwertigem Concept Art +- leicht malerischer Game-Art +- detaillierter Premium-Browser-RPG-Präsentation + +Nicht fotorealistisch im Sinne einer modernen Kameraaufnahme, sondern bewusst als hochwertige Fantasy-Spielgrafik erkennbar. + +## 1.3 Was vermieden werden muss + +Nicht verwenden: + +- Pixel Art +- Cartoon-Stil +- Anime-Stil +- Comic-Look +- Mobile-Game-Optik +- Chibi-Proportionen +- übertrieben bunte Farben +- Neon-Cyberpunk +- Sci-Fi-Elemente +- moderne Alltagsgegenstände +- saubere Hochglanz-Fantasy ohne Gebrauchsspuren +- sterile 3D-Render-Optik +- generische KI-Stock-Fantasy +- übertriebene Magieeffekte ohne spielerischen Grund +- World-of-Warcraft-artige Überproportionierung + +--- + +# 2. Farbwelt + +## 2.1 Grundpalette + +Die Hauptfarben von Ashen Realms sind: + +- Anthrazit +- Schwarzbraun +- dunkles Eisen +- dunkles Blau-Grau +- verwitterter Stein +- dunkles Leder +- gedämpftes Messing +- gealtertes Gold + +Große Flächen bleiben überwiegend dunkel und entsättigt. + +## 2.2 Funktionsfarben + +Akzentfarben werden gezielt eingesetzt und behalten überall dieselbe Bedeutung: + +### Rot + +Für: + +- Leben +- Blutung +- Schaden +- Gefahr +- aggressive Zustände + +Rot darf glühend wirken, aber nicht neonartig. + +### Grün + +Für: + +- Sicherheit +- passende Gefahr +- Heilung +- positive Zustände +- Natur oder Gift, wenn eindeutig unterscheidbar + +### Orange / Gold + +Für: + +- Warnungen +- Elites +- Auswahl +- wichtige Aktionen +- wertvolle UI-Zustände + +### Blau + +Für: + +- aktive Navigation +- defensive Zustände +- Magie +- besondere Ressourcen +- kalte Effekte + +### Violett + +Für: + +- seltene oder besondere Systeme +- spätere Sonderwährungen +- arkanere Inhalte + +Violett soll selten bleiben. + +--- + +# 3. Materialien + +Die UI- und Asset-Welt verwendet bevorzugt: + +- gealtertes Eisen +- dunklen Stahl +- Bronze +- gedämpftes Gold +- dunkles Leder +- verwittertes Holz +- gebrochenen Stein +- Stoff mit sichtbarer Abnutzung +- Asche +- Ruß +- trockenen Schlamm +- verkohltes Holz + +Alle Materialien sollen glaubwürdig gealtert sein. + +Metall besitzt: + +- Kratzer +- matte Reflexionen +- leichte Oxidation +- Kantenabrieb + +Leder besitzt: + +- Falten +- Gebrauchsspuren +- unregelmäßige Oberfläche + +Stein besitzt: + +- Risse +- dunkle Ablagerungen +- Ruß +- kleine Abplatzungen + +--- + +# 4. Beleuchtung + +## 4.1 Grundprinzip + +Licht soll Stimmung und Lesbarkeit unterstützen. + +Typisch sind: + +- kaltes diffuses Umgebungslicht +- warmes Feuer- oder Glutlicht als Kontrast +- gerichtetes Seitenlicht +- atmosphärische Lichtstrahlen +- leichter Nebel oder Rauch +- klare Vordergrund-/Hintergrundtrennung + +## 4.2 Vermeiden + +Nicht verwenden: + +- flaches Studiolicht +- gleichmäßig ausgeleuchtete Szenen +- übertriebene Bloom-Effekte +- grelle Neonränder +- unnötig starke Lens Flares + +--- + +# 5. Komposition für Browser-RPG-Assets + +Ashen Realms ist kein frei begehbares 3D-Spiel. Die Artworks werden in einer festen Browser-Oberfläche verwendet. + +Daher gilt: + +- Hauptmotive müssen sofort lesbar sein. +- Der zentrale Fokus darf nicht an UI-Kanten kleben. +- Hintergründe benötigen ruhige Flächen für spätere UI-Overlays. +- Große Figuren brauchen klare Silhouetten. +- Kompositionen dürfen nicht überall gleich detailreich sein. +- Wichtige Motive sollen nicht durch starke Kontraste im Hintergrund konkurriert werden. + +--- + +# 6. Gebietshintergründe + +## 6.1 Allgemeine Regeln + +Gebietshintergründe sind große atmosphärische Illustrationen. + +Sie sollen: + +- starke Tiefenwirkung besitzen +- Vordergrund, Mittelgrund und Hintergrund klar trennen +- breite Bildflächen bieten +- als Spielhintergrund funktionieren +- hochwertige Beleuchtung verwenden +- keine UI enthalten +- keine Schrift enthalten +- keine Rahmen enthalten +- keine Buttons enthalten +- keine Logos enthalten +- keine HUD-Elemente enthalten + +Empfohlene Komposition: + +- 16:9 oder breites Landscape-Format +- dominante horizontale Tiefe +- ruhige Bereiche links oder rechts für Panels +- visuelle Führung durch Wege, Mauern, Pfade oder Architektur + +## 6.2 Aschenfelder + +Visuelle Kernmerkmale: + +- trockene verbrannte Erde +- Asche +- verkohlte Bäume +- Ruinenreste +- zerstörte alte Handelsroute +- Rauch in der Ferne +- vereinzelte Glut +- dunkle kalte Grautöne +- warme orange-rote Glutakzente +- trockene Luft +- leicht vulkanische Stimmung + +Die Region soll bedrohlich wirken, aber nicht wie ein vollständig untergegangenes Weltende. + +### Verbrannte Straße + +- alte zerstörte Handelsstraße +- verkohlte Wegpfosten +- tote Bäume +- Ruinenreste +- Asche auf dem Boden +- Ferne mit Rauch oder Glut +- breite, gut lesbare Kampf- und UI-Flächen + +### Südtor von Graufurt + +- Übergang zwischen sicherer Stadt und gefährlicher Wildnis +- befestigtes Stadttor +- dunkler Stein und Holz +- Banner oder Grenzwacht-Symbole +- wenige warme Feuerstellen +- dahinter karge Aschenlandschaft + +### Verlassener Wachtposten + +- kleiner militärischer Außenposten +- teilweise beschädigt +- verlassene Barrikaden +- Waffenständer oder Vorratsreste +- nicht vollständig zerstört +- Gefühl eines aufgegebenen Grenzpostens + +### Aschengrube + +- gefährlichster Teil der Aschenfelder +- tiefere verbrannte Senke oder Grube +- mehr Rauch und Glut +- dichteres Gefühl +- stärkere Kontraste +- Boss-taugliche Komposition + +## 6.3 Graufurt + +Graufurt ist ein sicherer Hub. + +Die Stadt soll: + +- rau und glaubwürdig +- befestigt +- bewohnt +- funktionsfähig +- alt, aber nicht zerstört + +wirken. + +Typische Elemente: + +- dunkler Stein +- schwere Holztore +- Händlerstände +- Schmiede +- Lagerkisten +- Laternen +- Fackeln +- Markt-/Hofbereich +- Grenzwacht-Banner +- warme Lichtquellen + +Kontrast zu den Aschenfeldern: + +> Außenwelt = kalt, verbrannt, gefährlich. +> Graufurt = dunkel, rau, aber warm, organisiert und sicher. + +--- + +# 7. Kampf-Hintergründe + +Kampf-Hintergründe verwenden dieselbe Region wie die Weltansicht, werden aber speziell für die Duellkomposition gebaut. + +Grundstruktur: + +- Spieler links +- Gegner rechts +- Mitte bleibt visuell lesbar +- Hintergrund unterstützt statt konkurriert + +Daher: + +- weniger starke Hauptmotive im Zentrum +- klare Bodenfläche +- genug Platz um beide Figuren +- Tiefe im Hintergrund +- lokale Lichtquelle passend zum Ort + +--- + +# 8. Monster + +## 8.1 Allgemeine Regeln + +Monster sollen: + +- glaubwürdig in ihrer Umwelt existieren +- eine sofort lesbare Silhouette haben +- keine übertriebene Comic-Anatomie besitzen +- sichtbar gefährlich sein, ohne zwangsläufig riesig zu sein +- Abnutzung, Narben oder Umweltspuren zeigen + +Rendering: + +- realistisch bis leicht stilisiert +- detailreich +- malerisch +- starke Materialwirkung +- gerichtetes Licht + +## 8.2 Normale Gegner + +Normale Gegner bleiben relativ bodenständig. + +Beispiele aus den Aschenfeldern: + +- Aschenratte +- verwilderter Straßenhund +- Straßenräuber +- Plünderer-Späher +- Plünderer-Veteran +- Aschenhund +- Aschenwühler +- verbrannter Jagdhund + +Sie dürfen gefährlich wirken, aber nicht wie Endbosse. + +## 8.3 Humanoide Gegner + +Humanoide Gegner tragen: + +- abgenutzte Lederteile +- einfache Metallplatten +- gestohlene oder improvisierte Ausrüstung +- dunkle Stoffe +- praktische Waffen + +Keine sauberen Paladin-Rüstungen. + +## 8.4 Elitegegner + +Elites sollen gegenüber normalen Gegnern sichtbar aufgewertet werden durch: + +- stärkere Silhouette +- bessere Ausrüstung +- erkennbare Rangzeichen +- mehr Schutz +- markantere Waffe +- gezielte Akzentfarbe + +Aber: + +> Eine Elite ist noch kein gigantischer Raid-Boss. + +## 8.5 Bosse + +Bosse erhalten: + +- stärkere visuelle Hierarchie +- charakteristische Silhouette +- hochwertige, einzigartige Ausrüstung +- klar erkennbare Spezialelemente +- mehr Lichtdramaturgie +- leicht größere Proportionen, wenn sinnvoll + +Sie sollen trotzdem in derselben glaubwürdigen Welt existieren. + +--- + +# 9. Spielercharaktere + +## 9.1 Grundstil + +Spielercharaktere sind keine strahlenden High-Fantasy-Helden. + +Sie wirken wie erfahrene Grenzland-Abenteurer. + +Typische Gestaltung: + +- dunkler Kapuzenmantel +- Lederpanzerung +- leichte Metallteile +- praktische Gürtel +- Taschen +- robuste Stiefel +- Schwert +- Schild +- dunkle Blau-, Schwarz- und Brauntöne + +Gesichter: + +- menschlich +- glaubwürdig +- leicht wettergegerbt +- ernst +- nicht übertrieben attraktiv oder glamourös + +Keine: + +- leuchtenden Augen ohne Grund +- riesigen Schulterplatten +- neonfarbenen Waffen +- modernen Frisuren als Stilbruch +- Sci-Fi-Elemente + +## 9.2 Kampfartwork + +- Ganzkörper +- 3/4-Pose +- kampfbereit +- klare Silhouette +- neutraler oder transparenter Hintergrund +- für Platzierung links im Kampf geeignet + +## 9.3 Portrait + +- Kopf und Schultern oder Brustbild +- starke Gesichtserkennung +- dunkler neutraler Hintergrund +- für Topbar oder Charakterpanel geeignet + +--- + +# 10. NPCs + +NPCs müssen zur Welt gehören und ihre Funktion visuell lesbar machen. + +## Händler + +- ältere oder erfahrene Person +- wettergegerbt +- praktische Kleidung +- Taschen, Schlüssel, kleine Flaschen, Gürtel +- keine luxuriöse Händlerkarikatur + +## Grenzwacht / Questgeber + +- dunkle blau-graue Stoffe +- gealterte Metall-/Lederrüstung +- Grenzwacht-Emblem +- glaubwürdige militärische Haltung + +## Verletzter Wachmann + +- beschädigte Version derselben Grenzwacht-Ausrüstung +- Verbände +- Staub +- Ruß +- Müdigkeit +- aber klar kein Gegner + +--- + +# 11. Item-Icons + +## 11.1 Allgemeine Regeln + +Item-Icons sollen: + +- hochwertig illustriert sein +- klar erkennbar sein +- bei kleiner Größe funktionieren +- eine starke Silhouette haben +- dieselbe Licht- und Materialwelt nutzen + +Format: + +- quadratisch +- zentriertes Objekt +- konsistente Perspektive +- konsistente Objektgröße + +## 11.2 Materialien + +Waffen: + +- Stahl +- Eisen +- Ledergriff +- Gebrauchsspuren + +Rüstung: + +- dunkles Leder +- gealtertes Metall +- Stoff + +Amulette: + +- Bronze +- gedämpftes Gold +- dunkles Metall + +Tränke: + +- glaubwürdiges Glas +- Korken +- dezentes magisches Leuchten + +## 11.3 Seltenheit + +Seltenheit wird nicht durch extreme Regenbogenfarben dargestellt. + +Stattdessen: + +- Common: ruhig, wenig Glow +- Rare: kontrollierter Akzent +- Epic/Boss: stärkere Lichtkante, besonderer Rahmen, hochwertigere Materialwirkung + +--- + +# 12. UI-Icons + +UI-Icons sollen stilistisch ein geschlossenes Set bilden. + +Eigenschaften: + +- detailliert +- klare Silhouette +- mittelalterlich / Dark Fantasy +- gut lesbar bei kleiner Größe +- keine Flat-Icon-Optik +- keine modernen App-Icons + +Beispiele: + +- Angriff: gekreuzte Schwerter +- Schwerer Hieb: schwere Klinge oder Axt +- Schildstoß: Schild mit Einschlag +- Verteidigen: massiver Schild +- Heiltrank: rote Trankflasche +- Karte: alte Karte / Kompasssymbol +- Jagd: Jagdwaffen oder Fährtenmotiv +- Quests: Pergamentrolle +- Inventar: Tasche oder Truhe +- Charakter: Kriegerbüste + +--- + +# 13. Combat-Statusicons + +Statusicons dürfen stärker symbolisch sein als Item-Icons. + +Empfohlene Grundform: + +- rundes oder ovales Medaillon +- dunkles Metall +- feine Gold-/Bronzekante +- starkes zentrales Symbol +- kontrollierter Glow +- transparente Außenfläche + +Beispiele: + +### Blutung + +- Blutstropfen +- Kratz-/Schnittspuren +- dunkles Rot + +### Verteidigen + +- Schild +- Blau oder kaltes Stahllicht + +### Rüstung erhöht + +- Schild oder Brustpanzer +- aufsteigender Pfeil +- Gold / Orange + +### Heilung + +- Trank oder Kreuzsymbol +- Grün + +### Unterbrochen + +- gebrochene Rune +- zersplitterte Klinge +- gekappte Bewegungslinie + +### Telegraph / Warnung + +- schwere Klinge +- Warnsymbol +- orange / rot + +--- + +# 14. Danger Ratings + +Die fünf Danger Ratings lauten: + +1. Schwach +2. Passend +3. Stark +4. Sehr gefährlich +5. Tödlich + +Visuelle Hierarchie: + +### Schwach + +- kaltes Grau / leichtes Blau +- zurückhaltend +- wenig Glow + +### Passend + +- Grün +- stabil +- klar lesbar + +### Stark + +- Orange / Gold +- deutliche Warnung + +### Sehr gefährlich + +- starkes Orange-Rot +- höhere visuelle Spannung + +### Tödlich + +- tiefes Rot +- stärkster Glow +- aggressivste Rahmendetails + +Empfohlene Form: + +- horizontales Dark-Fantasy-Badge +- links Medaillon oder Symbol +- rechts Textfeld +- dunkles Metall / Stein +- gealterte Gold-/Bronzekante +- zentrale Schrift gut lesbar + +Keine modernen Pill-Badges. + +--- + +# 15. UI-Ornamente + +Ornamente dürfen die UI aufwerten, aber niemals wichtiger als Funktion werden. + +Geeignete Motive: + +- Drachenköpfe +- Wolfsköpfe +- Grenzwacht-Wappen +- Dornen +- Klingen +- gotische Spitzen +- kleine Runen +- symmetrische Metallornamente + +Geeignete Positionen: + +- Panel-Ecken +- Tabbar-Enden +- horizontale Trenner +- Navigation +- Footer-Mittelpunkt +- Header-Mitte + +Material: + +- dunkles Metall +- Bronze +- gedämpftes Gold + +Keine übergroßen Ornamente, die Content verdecken. + +--- + +# 16. Transparente Assets – verbindliche Regel + +Für alle Assets, die später frei auf UI oder Artwork gelegt werden, muss der Hintergrund **wirklich transparent** sein. + +Das gilt besonders für: + +- UI-Icons +- Combat-Statusicons +- Danger Badges +- Ornamente +- Item-Icons, wenn freigestellt +- Charakter-Cutouts +- Monster-Cutouts + +## 16.1 Generierungsanforderung + +Im Prompt ausdrücklich verwenden: + +> isolated game UI asset on a true transparent background, alpha transparency, no background plate outside the asset, no black background, no white background, no checkerboard pattern, no scene, no floor, no environment + +## 16.2 Verboten + +Nicht akzeptieren: + +- schwarzer Hintergrund, der nur wie Transparenz wirkt +- weißer Hintergrund +- Schachbrettmuster +- farbige Fläche hinter dem Asset +- rechteckiger Schattenkasten +- diffuse Vignette bis zum Bildrand +- Umgebung außerhalb des eigentlichen Assets + +## 16.3 Technische Zielausgabe + +Bevorzugt: + +- PNG +- RGBA +- Alpha-Kanal mit transparenten Außenbereichen +- vollständiges Asset innerhalb der Canvas +- keine abgeschnittenen Kanten + +## 16.4 Kontrolle + +Nach der Generierung prüfen: + +- Außenbereiche haben Alpha = 0 +- keine sichtbare Hintergrundplatte +- keine schwarzen Pixel-Halos +- keine abgeschnittenen Glows + +--- + +# 17. UI-Rahmen und Panels + +Panel-Stil: + +- dunkle Innenfläche +- leichte Stein-, Leder- oder Metalltextur +- klarer metallischer Rahmen +- feine Gold-/Bronzelinie +- kleine gotische Ecken +- keine übertriebene Verzierung + +Button-Stil: + +- dunkler Hintergrund +- gealterter Metallrahmen +- klare Textfläche +- Primary: stärkerer Gold-/Blau-Akzent +- Hover: leichte Aufhellung +- Active: sichtbare gedrückte Wirkung +- Disabled: entsättigt + +--- + +# 18. Schrift in generierten Assets + +Text sollte möglichst **nicht direkt in Bildassets eingebrannt** werden. + +Ausnahmen: + +- Danger-Rating-Badges +- bewusst grafische Labels + +Für normale UI-Komponenten gilt: + +> Rahmen und Icon generieren, Text später in Angular rendern. + +Vorteile: + +- bessere Lesbarkeit +- Lokalisierung möglich +- keine KI-Tippfehler +- flexible Größen + +--- + +# 19. Prompt-Grundbaustein + +Der folgende Block sollte bei zukünftigen Asset-Prompts möglichst immer enthalten sein: + +```text +Ashen Realms visual style. Modern premium dark-fantasy browser RPG. +Realistic to lightly stylized painterly game art, highly detailed but readable. +Dark desaturated palette, aged iron, dark steel, leather, weathered stone, +subtle bronze and muted gold accents. Dramatic cinematic lighting, +credible materials, worn surfaces, grounded medieval fantasy design. +Serious atmosphere, no cartoon, no anime, no pixel art, no mobile-game style, +no neon colors, no sci-fi, no modern objects, no generic stock high fantasy. +The asset must look like it belongs to the same game and same art team as all +existing Ashen Realms assets. +``` + +--- + +# 20. Prompt-Vorlage – Gebietshintergrund + +```text +Create a large atmospheric environment background for Ashen Realms. + +Location: [NAME] +Region: [REGION] +Purpose: browser RPG world / combat background + +Ashen Realms visual style: modern premium dark fantasy, realistic to lightly +stylized painterly game art, cinematic lighting, strong depth, grounded medieval +fantasy, dark desaturated colors with controlled warm accents. + +Scene requirements: +- [LOCATION-SPECIFIC ELEMENTS] +- strong foreground, midground and background separation +- large atmospheric depth +- clear focal hierarchy +- enough visually quiet areas for UI overlays +- no UI +- no text +- no logos +- no borders +- no buttons +- no HUD +- no modern objects +- threatening but not visually overloaded + +Wide landscape composition suitable for a desktop browser RPG. +``` + +--- + +# 21. Prompt-Vorlage – Monster + +```text +Create a standalone monster artwork for Ashen Realms. + +Monster: [NAME] +Region: [REGION] +Role: [NORMAL / RARE / ELITE / BOSS] + +Visual style: +modern premium dark fantasy, realistic to lightly stylized painterly game art, +highly detailed, grounded anatomy, believable materials, strong silhouette, +dramatic directional lighting, dark desaturated palette. + +Design notes: +- [BODY / ARMOR / DAMAGE / ENVIRONMENTAL TRAITS] +- clearly readable silhouette +- visually consistent with the region +- dangerous but not cartoonishly exaggerated +- no sci-fi +- no neon +- no anime +- no text + +Composition: +full body or near-full body, isolated subject, suitable for browser RPG combat +or encounter cards. +``` + +--- + +# 22. Prompt-Vorlage – Spieler oder NPC + +```text +Create a character artwork for Ashen Realms. + +Character: [NAME / ROLE] +Function: [PLAYER / MERCHANT / GUARD / QUEST NPC] + +Modern premium dark-fantasy browser RPG style. +Realistic to lightly stylized painterly illustration. +Grounded medieval fantasy clothing and equipment. +Dark leather, worn steel, weathered cloth, muted bronze and gold accents. +Serious expression, believable human proportions, cinematic side lighting. + +Character-specific details: +- [DETAILS] + +No anime. +No cartoon. +No sci-fi. +No modern clothing. +No oversized fantasy armor. +No glowing eyes unless explicitly required. + +Composition: +[PORTRAIT / THREE-QUARTER / FULL BODY] +``` + +--- + +# 23. Prompt-Vorlage – Item-Icon + +```text +Create a single item icon for Ashen Realms. + +Item: [NAME] +Type: [WEAPON / ARMOR / AMULET / POTION] +Rarity: [COMMON / RARE / EPIC] + +Modern premium dark-fantasy RPG item icon. +Realistic to lightly stylized painterly rendering. +Strong silhouette, readable at small size, centered object, believable aged +materials, worn steel, dark leather, muted gold or bronze accents. +Controlled lighting, no excessive glow. + +Square composition. +No text. +No logo. +No modern objects. +No cartoon or mobile-game style. +``` + +--- + +# 24. Prompt-Vorlage – UI-Icon / Status-Icon + +```text +Create a single Ashen Realms UI icon. + +Function: [STATUS / ACTION] +Symbol: [SYMBOL] +Accent color: [COLOR] + +Modern premium dark-fantasy game UI. +Ornate but readable circular medallion, aged dark iron and bronze frame, +subtle muted gold accents, strong central symbol, controlled glow, crisp +silhouette, detailed but readable at small UI size. + +True transparent background with alpha transparency. +No black background. +No white background. +No checkerboard. +No scene. +No floor. +No rectangular plate outside the icon. +No text. +``` + +--- + +# 25. Prompt-Vorlage – Ornament + +```text +Create a decorative UI ornament for Ashen Realms. + +Ornament type: [CORNER / DIVIDER / DRAGON / WOLF / CREST / TAB END] + +Dark gothic medieval fantasy UI ornament. +Aged dark iron, bronze and muted gold, fine engraved details, subtle wear, +symmetrical or intentionally balanced design, suitable for a premium browser RPG. + +True transparent PNG background, isolated asset, no background plate, no text, +no scene, no modern styling, no neon. +``` + +--- + +# 26. Prompt-Vorlage – Danger Rating + +```text +Create one danger-rating badge for Ashen Realms. + +Rating: [SCHWACH / PASSEND / STARK / SEHR GEFÄHRLICH / TÖDLICH] +Accent: [COLOR] + +Horizontal dark-fantasy UI badge. +Left side: circular medallion with a skull or danger symbol. +Right side: long dark stone/metal label plate. +Aged iron frame, bronze and muted gold trim, gothic points and small ornamental +spikes, controlled accent glow, highly detailed but readable. + +The text must be exactly: "[TEXT]" + +True transparent background outside the complete badge. +RGBA alpha transparency. +No black canvas. +No white canvas. +No checkerboard. +No environment. +No extra text. +No logo. +``` + +--- + +# 27. Konsistenzregeln für Serien + +Wenn mehrere Assets als Set erstellt werden: + +- gleiche Perspektive +- gleiche Lichtquelle +- gleiche Detaildichte +- gleiche Rahmenstärke +- gleiche Materialwelt +- gleiche Icon-Skalierung +- gleiche Canvas-Größe +- gleiche Positionierung +- gleiche Glow-Stärke + +Bei einer Serie niemals jedes Asset stilistisch neu interpretieren. + +Beispiel: + +> Wenn ein Statusicon einen runden Bronze-/Eisenrahmen besitzt, müssen die übrigen Statusicons denselben Grundrahmen verwenden und nur Symbol und Akzentfarbe wechseln. + +--- + +# 28. Qualitätscheck vor Freigabe + +Jedes neue Asset soll vor der Nutzung gegen diese Fragen geprüft werden: + +### Stil + +- Sieht es nach Ashen Realms aus? +- Ist es Dark Fantasy ohne Cartoon-/Mobile-Look? +- Sind Materialien glaubwürdig? +- Ist die Farbpalette kontrolliert? + +### Funktion + +- Ist das Hauptmotiv sofort lesbar? +- Funktioniert es in der vorgesehenen UI-Größe? +- Ist die Silhouette klar? + +### Konsistenz + +- Passt es zu bestehenden Assets? +- Haben Rahmen und Licht dieselbe Sprache? +- Ist der Detailgrad vergleichbar? + +### Technik + +- Ist das Format passend? +- Ist ein transparenter Hintergrund wirklich transparent? +- Sind Kanten vollständig sichtbar? +- Gibt es keine schwarzen oder weißen Halos? + +--- + +# 29. Kurzfassung für neue Bildgenerierungs-Chats + +Wenn nur wenig Kontext mitgegeben werden soll, kann folgender Block verwendet werden: + +```text +Du arbeitest am Projekt Ashen Realms, einem modernen Dark-Fantasy-Browser-RPG. +Alle neuen Grafiken müssen wie Bestandteile derselben Spielwelt und derselben +Art Direction aussehen. + +Stil: hochwertig, düster, atmosphärisch, realistisch bis leicht stilisiert, +malerisch, glaubwürdige Materialien, starke Lichtführung, dunkle entsättigte +Farben, gealtertes Eisen, Leder, Stein, Bronze und gedämpftes Gold. + +Nicht erlaubt: Pixel Art, Cartoon, Anime, Mobile-Game-Look, Neonfarben, +Sci-Fi, moderne Gegenstände, generische High-Fantasy-Stock-Art oder übertriebene +Magieeffekte. + +Bei transparenten Assets muss der Hintergrund technisch wirklich transparent +sein: PNG/RGBA mit Alpha, keine schwarze oder weiße Fläche, kein Checkerboard, +keine Hintergrundszene. + +Erfinde keine neue visuelle Richtung unabhängig von dieser Vorgabe. +``` + +--- + +# 30. Oberste Regel + +> **Ein neues Asset ist nur dann erfolgreich, wenn es nicht wie ein neues Experiment aussieht, sondern wie ein bereits immer zu Ashen Realms gehörender Bestandteil des Spiels.**