docs
This commit is contained in:
@@ -2,16 +2,33 @@ import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap, Router, provideRouter } from '@angular/router';
|
||||
import { vi } from 'vitest';
|
||||
import type { Combat } from '../../../core/api/game-api.models';
|
||||
import type {
|
||||
Combat,
|
||||
CombatRewardItem,
|
||||
LootCapacity,
|
||||
} from '../../../core/api/game-api.models';
|
||||
import { CombatStore } from '../combat.store';
|
||||
import { WorldStore } from '../../world/world.store';
|
||||
import { CombatPageComponent } from './combat-page.component';
|
||||
|
||||
// Roomy enough that no test trips the "full" styling unless it asks to.
|
||||
const FULL_CAPACITIES: LootCapacity[] = [
|
||||
{ category: 'HIDE', current: 1, capacity: 5, bag: null },
|
||||
{ category: 'RAIDER_TROPHY', current: 0, capacity: 5, bag: null },
|
||||
];
|
||||
|
||||
const activeCombat: Combat = {
|
||||
id: 'combat-1',
|
||||
status: 'ACTIVE',
|
||||
round: 2,
|
||||
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 95, potionsRemaining: 2, potionsMax: 2 },
|
||||
player: {
|
||||
name: 'Aric Duskwalker',
|
||||
maxHp: 100,
|
||||
currentHp: 95,
|
||||
potionsRemaining: 2,
|
||||
potionsMax: 2,
|
||||
statusEffects: [],
|
||||
},
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Ash Rat',
|
||||
@@ -426,7 +443,7 @@ describe('CombatPageComponent', () => {
|
||||
...activeCombat,
|
||||
status: 'WON',
|
||||
monster: { ...activeCombat.monster, currentHp: 0 },
|
||||
rewards: { silver: 6, items: [] },
|
||||
rewards: { items: [], capacities: FULL_CAPACITIES },
|
||||
events: [
|
||||
...activeCombat.events,
|
||||
{ round: 2, sequence: 3, type: 'DAMAGE', source: 'PLAYER', target: 'MONSTER', amount: 31 },
|
||||
@@ -522,57 +539,292 @@ describe('CombatPageComponent', () => {
|
||||
expect(combatStore.loadCombat).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
describe('Bleeding (spec §4)', () => {
|
||||
it('shows an active effect with the rounds it still has to run', async () => {
|
||||
const fixture = await setup({
|
||||
...activeCombat,
|
||||
player: {
|
||||
...activeCombat.player,
|
||||
statusEffects: [{ type: 'BLEED', remainingRounds: 2, damagePerRound: 5 }],
|
||||
},
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
const badge = element.querySelector('[data-combat-status="BLEED"]');
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge?.textContent).toContain('Bleeding');
|
||||
expect(badge?.textContent).toContain('2');
|
||||
});
|
||||
|
||||
it('shows no effect strip while the player is unafflicted', async () => {
|
||||
const fixture = await setup(activeCombat);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelector('[data-combat-statuses]')).toBeNull();
|
||||
});
|
||||
|
||||
it('reads the status events in the combat log', async () => {
|
||||
const fixture = await setup({
|
||||
...activeCombat,
|
||||
player: {
|
||||
...activeCombat.player,
|
||||
statusEffects: [{ type: 'BLEED', remainingRounds: 1, damagePerRound: 5 }],
|
||||
},
|
||||
events: [
|
||||
{
|
||||
round: 1,
|
||||
sequence: 1,
|
||||
type: 'STATUS_APPLIED',
|
||||
source: 'MONSTER',
|
||||
target: 'PLAYER',
|
||||
amount: 2,
|
||||
statusEffect: 'BLEED',
|
||||
},
|
||||
{
|
||||
round: 1,
|
||||
sequence: 2,
|
||||
type: 'STATUS_DAMAGE',
|
||||
source: 'MONSTER',
|
||||
target: 'PLAYER',
|
||||
amount: 5,
|
||||
statusEffect: 'BLEED',
|
||||
},
|
||||
{
|
||||
round: 1,
|
||||
sequence: 3,
|
||||
type: 'STATUS_EXPIRED',
|
||||
source: 'MONSTER',
|
||||
target: 'PLAYER',
|
||||
statusEffect: 'BLEED',
|
||||
},
|
||||
],
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
const log = element.querySelector('.combat__log-body')?.textContent ?? '';
|
||||
|
||||
expect(log).toContain('inflicts Bleeding');
|
||||
expect(log).toContain('Bleeding costs Aric Duskwalker 5 HP');
|
||||
expect(log).toContain('Bleeding fades');
|
||||
});
|
||||
});
|
||||
|
||||
const wonWithRewards: Combat = {
|
||||
...activeCombat,
|
||||
status: 'WON',
|
||||
monster: { ...activeCombat.monster, currentHp: 0 },
|
||||
rewards: { silver: 6, items: [] },
|
||||
rewards: { items: [], capacities: FULL_CAPACITIES },
|
||||
};
|
||||
|
||||
it('shows the granted silver on the victory screen', async () => {
|
||||
const fixture = await setup(wonWithRewards);
|
||||
const ashenPelt: CombatRewardItem = {
|
||||
characterItemId: 'character-item-pelt',
|
||||
item: {
|
||||
key: 'ash-pelt',
|
||||
name: 'Ashen Pelt',
|
||||
type: 'TRADE_GOOD',
|
||||
lootCategory: 'HIDE',
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/ash-pelt.png',
|
||||
},
|
||||
quantity: 1,
|
||||
quantityLeftBehind: 0,
|
||||
};
|
||||
|
||||
const banditBlade: CombatRewardItem = {
|
||||
characterItemId: 'character-item-blade',
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Bandit Blade',
|
||||
type: 'EQUIPMENT',
|
||||
lootCategory: null,
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/bandit-blade.png',
|
||||
},
|
||||
quantity: 1,
|
||||
quantityLeftBehind: 0,
|
||||
};
|
||||
|
||||
const healingPotion: CombatRewardItem = {
|
||||
characterItemId: 'character-item-potion',
|
||||
item: {
|
||||
key: 'small-healing-potion',
|
||||
name: 'Small Healing Potion',
|
||||
type: 'CONSUMABLE',
|
||||
lootCategory: null,
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/small-healing-potion.png',
|
||||
},
|
||||
quantity: 1,
|
||||
quantityLeftBehind: 0,
|
||||
};
|
||||
|
||||
it('shows the reward panel without any currency row', async () => {
|
||||
const fixture = await setup({ ...wonWithRewards, rewards: { items: [ashenPelt], capacities: FULL_CAPACITIES } });
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelector('[data-combat-rewards]')).toBeTruthy();
|
||||
expect(element.querySelector('[data-reward-silver]')?.textContent).toContain('6');
|
||||
// R16: the XP block is removed entirely, not hidden -- guard against it
|
||||
// reappearing in the markup.
|
||||
// Spec §9: no XP or Silver rows. Both blocks are removed from the markup
|
||||
// rather than hidden, so guard against either reappearing.
|
||||
expect(element.querySelector('[data-reward-silver]')).toBeNull();
|
||||
expect(element.querySelector('[data-reward-experience]')).toBeNull();
|
||||
});
|
||||
|
||||
it('hides the silver tile entirely when the kill granted none', async () => {
|
||||
// Every monster seeded in this slice rolls silverMin/silverMax = 0 (design
|
||||
// R7), so without this guard the victory screen advertises "Silver +0"
|
||||
// after every single fight in the shipped content.
|
||||
it('separates trade goods, equipment, and consumables in the loot summary', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: { silver: 0, items: [] },
|
||||
rewards: { items: [banditBlade, ashenPelt, healingPotion], capacities: FULL_CAPACITIES },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelector('[data-reward-silver]')).toBeNull();
|
||||
expect(element.querySelector('[data-combat-rewards]')).toBeTruthy();
|
||||
const groups = [...element.querySelectorAll('[data-reward-group]')];
|
||||
expect(groups.map((group) => group.getAttribute('data-reward-group'))).toEqual([
|
||||
'trade-goods',
|
||||
'equipment',
|
||||
'consumables',
|
||||
]);
|
||||
expect(groups.map((group) => group.textContent?.trim())).toEqual([
|
||||
'Trade Goods',
|
||||
'Equipment',
|
||||
'Consumables',
|
||||
]);
|
||||
});
|
||||
|
||||
it('omits a category the fight did not drop anything for', async () => {
|
||||
const fixture = await setup({ ...wonWithRewards, rewards: { items: [ashenPelt], capacities: FULL_CAPACITIES } });
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
const groups = [...element.querySelectorAll('[data-reward-group]')];
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0].getAttribute('data-reward-group')).toBe('trade-goods');
|
||||
});
|
||||
|
||||
it('files a trophy with the trade goods, because both are merchant fodder', async () => {
|
||||
const insignia: CombatRewardItem = {
|
||||
characterItemId: 'character-item-insignia',
|
||||
item: {
|
||||
key: 'bandit-insignia',
|
||||
name: 'Raider Insignia',
|
||||
type: 'TROPHY',
|
||||
lootCategory: 'RAIDER_TROPHY',
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/bandit-insignia.png',
|
||||
},
|
||||
quantity: 1,
|
||||
quantityLeftBehind: 0,
|
||||
};
|
||||
const fixture = await setup({ ...wonWithRewards, rewards: { items: [insignia], capacities: FULL_CAPACITIES } });
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(
|
||||
element.querySelector('[data-reward-group="trade-goods"]'),
|
||||
).toBeTruthy();
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain(
|
||||
'Raider Insignia',
|
||||
);
|
||||
});
|
||||
|
||||
describe('full loot bags (slice 0.7.5 §9, §10)', () => {
|
||||
const refusedPelt: CombatRewardItem = {
|
||||
...ashenPelt,
|
||||
characterItemId: null,
|
||||
quantity: 0,
|
||||
quantityLeftBehind: 1,
|
||||
};
|
||||
|
||||
const fullHides: LootCapacity[] = [
|
||||
{ category: 'HIDE', current: 5, capacity: 5, bag: null },
|
||||
{ category: 'RAIDER_TROPHY', current: 0, capacity: 5, bag: null },
|
||||
];
|
||||
|
||||
it('names what the bag refused instead of dropping it silently', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: { items: [refusedPelt], capacities: fullHides },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
const refused = element.querySelector('[data-reward-left-behind]');
|
||||
expect(refused).toBeTruthy();
|
||||
expect(refused?.textContent).toContain('Ashen Pelt');
|
||||
expect(refused?.textContent).toContain('1');
|
||||
});
|
||||
|
||||
it('keeps a fully refused drop out of the loot the player actually got', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: { items: [refusedPelt], capacities: fullHides },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
// Nothing was banked, so no Trade Goods heading and no item card.
|
||||
expect(element.querySelector('[data-reward-group="trade-goods"]')).toBeNull();
|
||||
expect(element.querySelector('[data-item-name]')).toBeNull();
|
||||
});
|
||||
|
||||
it('lists a partial grant under both loot and left behind', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: {
|
||||
items: [{ ...ashenPelt, quantity: 1, quantityLeftBehind: 1 }],
|
||||
capacities: fullHides,
|
||||
},
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
// §10: granted 1, left behind 1 -- the player has to see both halves.
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain(
|
||||
'Ashen Pelt',
|
||||
);
|
||||
expect(
|
||||
element.querySelector('[data-reward-refused="ash-pelt"]')?.textContent,
|
||||
).toContain('1');
|
||||
});
|
||||
|
||||
it('still shows equipment that landed while the hide bag was full', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: { items: [refusedPelt, banditBlade], capacities: fullHides },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
// §9: equipment must not be lost to a full trade-good bag.
|
||||
expect(element.querySelector('[data-reward-group="equipment"]')).toBeTruthy();
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain(
|
||||
'Bandit Blade',
|
||||
);
|
||||
expect(element.querySelector('[data-reward-left-behind]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('says nothing about left-behind loot when everything fit', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: { items: [ashenPelt], capacities: FULL_CAPACITIES },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.querySelector('[data-reward-left-behind]')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the carrying state after the fight', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: { items: [refusedPelt], capacities: fullHides },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
// §11/§12: the player learns the trip is over on the victory screen,
|
||||
// without a second request.
|
||||
expect(element.querySelector('[data-loot-capacities]')).toBeTruthy();
|
||||
expect(
|
||||
element.querySelector('[data-loot-capacity="HIDE"] [data-loot-capacity-full]'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a dropped item with its icon, name, and rarity', async () => {
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
monster: { ...activeCombat.monster, key: 'road-bandit', name: 'Road Bandit', currentHp: 0 },
|
||||
rewards: {
|
||||
silver: 12,
|
||||
items: [
|
||||
{
|
||||
characterItemId: 'character-item-1',
|
||||
item: {
|
||||
key: 'bandit-blade',
|
||||
name: 'Bandit Blade',
|
||||
rarity: 'COMMON',
|
||||
iconPath: '/images/items/bandit-blade.png',
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
rewards: { items: [banditBlade], capacities: FULL_CAPACITIES },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
@@ -599,12 +851,12 @@ describe('CombatPageComponent', () => {
|
||||
// exactly what the server persisted, without rerolling anything.
|
||||
const fixture = await setup({
|
||||
...wonWithRewards,
|
||||
rewards: { silver: 12, items: [] },
|
||||
rewards: { items: [ashenPelt], capacities: FULL_CAPACITIES },
|
||||
});
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(combatStore.loadCombat).toHaveBeenCalledWith('combat-1');
|
||||
expect(element.querySelector('[data-reward-silver]')?.textContent).toContain('12');
|
||||
expect(element.querySelector('[data-item-name]')?.textContent).toContain('Ashen Pelt');
|
||||
});
|
||||
|
||||
it('still shows a plain victory when the server reports no reward record', async () => {
|
||||
|
||||
Reference in New Issue
Block a user