Merge branch 'master' into worktree-local-location-view

Brings in the encounter-status feature (cleared/resumed hunt encounters)
and its own independent Verwilderter Straßenhund / Verkohlter Plünderer
assets. Both branches added the same two monsters at the same time;
resolved by keeping master's asset set as canonical (images/combat/icons/,
images/combat/sprites/) rather than maintaining a parallel copy under
images/monsters/icons/ — dropped that directory and pointed the seed's
MonsterDefinition.iconPath, the local-view test fixtures and the frontend
icon lookup at the existing combat/icons paths instead. Kept this
branch's COMBAT_MONSTER_SCALE entries for the two monsters, since master
never added them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-20 16:03:59 +02:00
45 changed files with 889 additions and 88 deletions

View File

@@ -25,6 +25,7 @@ const threeEncounterHunt: HuntResult = {
id: 'encounter-1',
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
dangerRating: 'WEAK',
status: 'AVAILABLE',
},
{
id: 'encounter-2',
@@ -35,11 +36,13 @@ const threeEncounterHunt: HuntResult = {
artworkPath: '/images/enemies/RoadBandit.png',
},
dangerRating: 'MATCH',
status: 'AVAILABLE',
},
{
id: 'encounter-3',
monster: { key: 'ash-rat', name: 'Aschenratte', level: 1, artworkPath: '/images/enemies/AshRat.png' },
dangerRating: 'WEAK',
status: 'AVAILABLE',
},
],
};
@@ -73,6 +76,7 @@ describe('HuntPageComponent', () => {
encounters: () => HuntResult['encounters'];
startHunt: ReturnType<typeof vi.fn>;
refreshHunt: ReturnType<typeof vi.fn>;
loadActiveHunt: ReturnType<typeof vi.fn>;
selectEncounter: ReturnType<typeof vi.fn>;
};
let combatStore: {
@@ -95,6 +99,7 @@ describe('HuntPageComponent', () => {
encounters: () => currentHunt()?.encounters ?? [],
startHunt: vi.fn(() => Promise.resolve()),
refreshHunt: vi.fn(() => Promise.resolve()),
loadActiveHunt: vi.fn(() => Promise.resolve()),
selectEncounter: vi.fn(),
};
combatStore = {
@@ -281,6 +286,75 @@ describe('HuntPageComponent', () => {
expect(huntingStore.startHunt).not.toHaveBeenCalled();
});
it('adopts the resumable hunt on page entry so cleared encounters stay marked', async () => {
await setup(burnedRoad);
expect(huntingStore.loadActiveHunt).toHaveBeenCalledOnce();
expect(huntingStore.startHunt).not.toHaveBeenCalled();
});
it('marks a defeated encounter and takes away its attack action', async () => {
const clearedHunt: HuntResult = {
...threeEncounterHunt,
encounters: [
{ ...threeEncounterHunt.encounters[0], status: 'DEFEATED' },
threeEncounterHunt.encounters[1],
threeEncounterHunt.encounters[2],
],
};
const fixture = await setup(burnedRoad, clearedHunt);
const element = fixture.nativeElement as HTMLElement;
const cards = element.querySelectorAll('app-encounter-card');
expect(cards[0].querySelector('.encounter-card__defeated-mark')).not.toBeNull();
expect(cards[1].querySelector('.encounter-card__defeated-mark')).toBeNull();
const attackButtons = Array.from(
element.querySelectorAll<HTMLButtonElement>('.encounter-card__attack'),
);
expect(attackButtons[0].disabled).toBe(true);
expect(attackButtons[1].disabled).toBe(false);
});
it('keeps an in-progress encounter unmarked but locked', async () => {
const fightingHunt: HuntResult = {
...threeEncounterHunt,
encounters: [
{ ...threeEncounterHunt.encounters[0], status: 'IN_PROGRESS' },
threeEncounterHunt.encounters[1],
threeEncounterHunt.encounters[2],
],
};
const fixture = await setup(burnedRoad, fightingHunt);
const element = fixture.nativeElement as HTMLElement;
const cards = element.querySelectorAll('app-encounter-card');
expect(cards[0].querySelector('.encounter-card__defeated-mark')).toBeNull();
const attackButtons = Array.from(
element.querySelectorAll<HTMLButtonElement>('.encounter-card__attack'),
);
expect(attackButtons[0].disabled).toBe(true);
});
it('does not start a combat from a defeated encounter card', async () => {
const clearedHunt: HuntResult = {
...threeEncounterHunt,
encounters: [
{ ...threeEncounterHunt.encounters[0], status: 'DEFEATED' },
threeEncounterHunt.encounters[1],
threeEncounterHunt.encounters[2],
],
};
const fixture = await setup(burnedRoad, clearedHunt);
const element = fixture.nativeElement as HTMLElement;
element.querySelectorAll<HTMLButtonElement>('.encounter-card__attack')[0].click();
await Promise.resolve();
expect(combatStore.startCombat).not.toHaveBeenCalled();
});
it('loads the world state on init when no location has been loaded yet (direct navigation/hard refresh)', async () => {
await setup(null);