diff --git a/apps/web/src/app/features/combat/combat-page/combat-page.component.html b/apps/web/src/app/features/combat/combat-page/combat-page.component.html
index 3155686..f221a36 100644
--- a/apps/web/src/app/features/combat/combat-page/combat-page.component.html
+++ b/apps/web/src/app/features/combat/combat-page/combat-page.component.html
@@ -84,7 +84,37 @@
Sieg
{{ combat.monster.name }} wurde besiegt.
-
Belohnungen werden im nächsten Schritt verarbeitet.
+
+ @if (combat.rewards; as rewards) {
+
+ Belohnungen
+
+
+
+
- Erfahrung
+ - +{{ rewards.experience }} XP
+
+
+
- Silber
+ - +{{ rewards.silver }}
+
+
+
+ @if (rewards.items.length) {
+ Beute
+
+ @for (reward of rewards.items; track reward.characterItemId) {
+ -
+
+
+ }
+
+ } @else {
+ Keine besondere Beute gefunden.
+ }
+
+ }
+
diff --git a/apps/web/src/app/features/combat/combat-page/combat-page.component.scss b/apps/web/src/app/features/combat/combat-page/combat-page.component.scss
index b487044..172f412 100644
--- a/apps/web/src/app/features/combat/combat-page/combat-page.component.scss
+++ b/apps/web/src/app/features/combat/combat-page/combat-page.component.scss
@@ -328,6 +328,10 @@
text-transform: uppercase;
}
+.outcome--won {
+ inline-size: min(32rem, 90%);
+}
+
.outcome--won .outcome__title {
color: var(--ar-gold);
}
@@ -503,3 +507,61 @@
max-inline-size: none;
}
}
+
+/* Reward summary: dark stone and bronze, large readable values, restrained
+ rarity emphasis. No confetti, no popups, no slot-machine reveal (spec §50). */
+.rewards {
+ display: grid;
+ gap: var(--ar-space-3);
+ justify-items: center;
+ inline-size: 100%;
+ margin-block-start: var(--ar-space-3);
+ padding-block-start: var(--ar-space-3);
+ border-block-start: 1px solid var(--ar-border);
+}
+
+.rewards__title {
+ margin: 0;
+ color: var(--ar-text-muted);
+ font-family: Georgia, 'Times New Roman', serif;
+ font-size: var(--ar-font-sm);
+ font-weight: 400;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+}
+
+.rewards__currencies {
+ display: flex;
+ gap: var(--ar-space-6);
+ margin: 0;
+}
+
+.rewards__currency {
+ display: grid;
+ gap: var(--ar-space-1);
+ justify-items: center;
+}
+
+.rewards__currency dt {
+ color: var(--ar-text-muted);
+ font-size: var(--ar-font-sm);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.rewards__currency dd {
+ margin: 0;
+ color: var(--ar-gold);
+ font-family: Georgia, 'Times New Roman', serif;
+ font-size: clamp(1.25rem, 2.5vw, 1.6rem);
+}
+
+.rewards__loot {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--ar-space-4);
+ justify-content: center;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
diff --git a/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts b/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts
index 931db81..e20fd29 100644
--- a/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts
+++ b/apps/web/src/app/features/combat/combat-page/combat-page.component.spec.ts
@@ -239,4 +239,85 @@ describe('CombatPageComponent', () => {
expect(combatStore.loadCombat).toHaveBeenCalledTimes(2);
});
+
+ const wonWithRewards: Combat = {
+ ...activeCombat,
+ status: 'WON',
+ monster: { ...activeCombat.monster, currentHp: 0 },
+ rewards: { experience: 8, silver: 6, items: [] },
+ };
+
+ it('shows the granted XP and silver on the victory screen', async () => {
+ const fixture = await setup(wonWithRewards);
+ const element = fixture.nativeElement as HTMLElement;
+
+ expect(element.querySelector('[data-combat-rewards]')).toBeTruthy();
+ expect(element.querySelector('[data-reward-experience]')?.textContent).toContain('8');
+ expect(element.querySelector('[data-reward-silver]')?.textContent).toContain('6');
+ });
+
+ it('renders a dropped item with its icon, name, and rarity', async () => {
+ const fixture = await setup({
+ ...wonWithRewards,
+ monster: { ...activeCombat.monster, key: 'road-bandit', name: 'Straßenräuber', currentHp: 0 },
+ rewards: {
+ experience: 16,
+ silver: 12,
+ items: [
+ {
+ characterItemId: 'character-item-1',
+ item: {
+ key: 'bandit-blade',
+ name: 'Räuberklinge',
+ rarity: 'COMMON',
+ iconPath: '/images/items/bandit-blade.png',
+ },
+ quantity: 1,
+ },
+ ],
+ },
+ });
+ const element = fixture.nativeElement as HTMLElement;
+
+ expect(element.querySelector
('[data-item-icon]')?.getAttribute('src')).toBe(
+ '/images/items/bandit-blade.png',
+ );
+ expect(element.querySelector('[data-item-name]')?.textContent).toContain('Räuberklinge');
+ expect(element.querySelector('[data-item-rarity]')?.textContent).toContain('Gewöhnlich');
+ expect(element.querySelector('[data-reward-empty]')).toBeNull();
+ });
+
+ it('treats a victory without loot as complete, not as a failure', async () => {
+ const fixture = await setup(wonWithRewards);
+ const element = fixture.nativeElement as HTMLElement;
+
+ expect(element.querySelector('[data-reward-empty]')?.textContent).toContain(
+ 'Keine besondere Beute gefunden.',
+ );
+ expect(element.querySelector('[role="alert"]')).toBeNull();
+ // XP and silver still carry the screen.
+ expect(element.querySelector('[data-reward-experience]')).toBeTruthy();
+ });
+
+ it('renders the persisted rewards of an already-won combat loaded from the server', async () => {
+ // Simulates a browser refresh: the page loads the combat by id and shows
+ // exactly what the server persisted, without rerolling anything.
+ const fixture = await setup({
+ ...wonWithRewards,
+ rewards: { experience: 16, silver: 12, items: [] },
+ });
+ const element = fixture.nativeElement as HTMLElement;
+
+ expect(combatStore.loadCombat).toHaveBeenCalledWith('combat-1');
+ expect(element.querySelector('[data-reward-experience]')?.textContent).toContain('16');
+ expect(element.querySelector('[data-reward-silver]')?.textContent).toContain('12');
+ });
+
+ it('still shows a plain victory when the server reports no reward record', async () => {
+ const fixture = await setup({ ...wonWithRewards, rewards: null });
+ const element = fixture.nativeElement as HTMLElement;
+
+ expect(element.querySelector('[data-combat-result="WON"]')).toBeTruthy();
+ expect(element.querySelector('[data-combat-rewards]')).toBeNull();
+ });
});
diff --git a/apps/web/src/app/features/combat/combat-page/combat-page.component.ts b/apps/web/src/app/features/combat/combat-page/combat-page.component.ts
index 8eea821..51187a6 100644
--- a/apps/web/src/app/features/combat/combat-page/combat-page.component.ts
+++ b/apps/web/src/app/features/combat/combat-page/combat-page.component.ts
@@ -7,6 +7,7 @@ import {
combatMonsterSpriteScale,
runtimeMonsterArtworkPath,
} from '../../../shared/monster-artwork';
+import { ItemCardComponent } from '../../../shared/item-card/item-card.component';
import { CombatStore } from '../combat.store';
interface CombatLogRound {
@@ -29,6 +30,7 @@ const RIPOSTE_DELAY_MS = 260;
selector: 'app-combat-page',
templateUrl: './combat-page.component.html',
styleUrl: './combat-page.component.scss',
+ imports: [ItemCardComponent],
})
export class CombatPageComponent implements OnInit {
protected readonly combatStore = inject(CombatStore);