feat(hunting): show cleared encounters and resume interrupted fights
The hunt screen kept whatever roll was last in memory, so a player coming back from a fight saw every encounter as fresh. Encounters now carry their own status, which the combat module advances as fights start and end. - hunt_encounters.status replaces consumed_at, which only recorded that a fight had begun and could not distinguish a win from a loss - a lost fight hands the encounter back as AVAILABLE, so it can be retried; the unique index tying one combat to one encounter goes with it - GET /hunts/active serves the resumable hunt, which the hunt page adopts on entry rather than trusting its in-memory roll - defeated encounters are crossed out and lose their hover and attack action - a fresh page load rejoins a combat the server still holds open Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
66
apps/web/src/app/core/resume-combat.spec.ts
Normal file
66
apps/web/src/app/core/resume-combat.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router, provideRouter } from '@angular/router';
|
||||
import { vi } from 'vitest';
|
||||
import type { Combat } from './api/game-api.models';
|
||||
import { App } from '../app';
|
||||
import { CombatStore } from '../features/combat/combat.store';
|
||||
|
||||
const runningCombat: Combat = {
|
||||
id: 'combat-running',
|
||||
status: 'ACTIVE',
|
||||
round: 4,
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 62 },
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
level: 3,
|
||||
maxHp: 75,
|
||||
currentHp: 30,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
},
|
||||
events: [],
|
||||
};
|
||||
|
||||
describe('resuming an interrupted combat', () => {
|
||||
let combatStore: { loadActiveCombat: ReturnType<typeof vi.fn> };
|
||||
let router: Router;
|
||||
|
||||
async function bootstrap() {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
providers: [
|
||||
provideRouter([
|
||||
{ path: 'world', children: [] },
|
||||
{ path: 'combat/:combatId', children: [] },
|
||||
]),
|
||||
{ provide: CombatStore, useValue: combatStore },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
it('drops the player straight back into the fight they left running', async () => {
|
||||
combatStore = { loadActiveCombat: vi.fn(() => Promise.resolve(runningCombat)) };
|
||||
|
||||
await bootstrap();
|
||||
|
||||
expect(combatStore.loadActiveCombat).toHaveBeenCalledOnce();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/combat', 'combat-running']);
|
||||
});
|
||||
|
||||
it('leaves navigation alone when no fight is running', async () => {
|
||||
combatStore = { loadActiveCombat: vi.fn(() => Promise.resolve(null)) };
|
||||
|
||||
await bootstrap();
|
||||
|
||||
expect(combatStore.loadActiveCombat).toHaveBeenCalledOnce();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user