Reconciles Slice 0.6.5 (Renown & Reputation Foundation) against master's persistent-HP-and-regeneration slice, which landed independently and touches several of the same files (Character entity, CombatService, EquipmentService, the inventory detail panel). Conflict resolutions: - CharacterStatsService/EquipmentService constructor wiring: kept master's CharacterVitalsService injection, which this branch's version of the same files didn't have yet. - CombatService.performAction: kept master's HP-guard logic (characterTooWounded, vitals pause-on-enter) alongside this branch's multi-line calculate() call style. - Inventory detail panel (.html/.ts/.scss/.spec.ts): master had redesigned the panel (wrapping section, rarity styling, flavour text, a shared inventory.labels.ts) on top of the OLD level-gated component, since this branch's removal of the level gate (R4, Task 8/14) hadn't reached master yet. Kept master's visual redesign in full, but with the level-gate concept removed throughout: no requiredLevel stat block, no meetsLevelRequirement() branch in the equip button, no now-dead .detail__value--unmet SCSS rule. Kept both branches' independent tests (non-equippable-item, flavour-text). - inventory-page.component.ts: dropped master's dead characterLevel computed (nothing in the template read it, and the level concept is gone); kept its independent bagCells/bagUsed/bagCapacity grid feature, which has nothing to do with renown or level. Post-merge fixture repairs (three files failed the Angular bundle compile because they predate master's hpRegenPerSecond/hpRegenSince fields or master's item description field, neither conflict-marked since git considered them non-overlapping edits): - app.spec.ts: a 'renders loaded character values' test added on master after this branch forked still used the abolished level/ experience fields on its decoy fixture -- retargeted to renown. - inventory-detail-panel.component.spec.ts: the ashPelt fixture added by this branch's final-review follow-up predates master's required description field. - top-bar.component.spec.ts: this branch's fixture predates master's required hpRegenPerSecond/hpRegenSince fields. No database migration touches the same column: master's 1792000000000-AddHpRegeneration only adds characters.hp_regen_since, independent of this slice's 1791000000000-CreateRenownAndReputation. Timestamp ordering between the two was already correct with no rename needed. Verified: API 288/288 (267 from this slice + 21 from master), API build zero errors, web 237/237 (230 from this slice + 7 from master). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import { Repository } from 'typeorm';
|
|
import { CharacterEquipment } from '../equipment/entities/character-equipment.entity';
|
|
import { CharacterItem } from '../items/entities/character-item.entity';
|
|
import { EquipmentSlot } from '../items/equipment-slot.enum';
|
|
import { ItemRarity } from '../items/item-rarity.enum';
|
|
import { ItemType } from '../items/item-type.enum';
|
|
import { InventoryService } from './inventory.service';
|
|
|
|
const CHARACTER_ID = 'character-1';
|
|
|
|
function characterItem(overrides: Partial<CharacterItem> = {}): CharacterItem {
|
|
return {
|
|
id: 'item-1',
|
|
characterId: CHARACTER_ID,
|
|
itemDefinitionId: 'def-1',
|
|
quantity: 1,
|
|
createdAt: new Date('2026-08-18T09:00:00.000Z'),
|
|
updatedAt: new Date('2026-08-18T09:00:00.000Z'),
|
|
itemDefinition: {
|
|
key: 'worn-short-sword',
|
|
name: 'Abgenutztes Kurzschwert',
|
|
description: 'Die Klinge eines Rekruten, öfter geschliffen als geführt.',
|
|
rarity: ItemRarity.COMMON,
|
|
type: ItemType.EQUIPMENT,
|
|
equipmentSlot: EquipmentSlot.WEAPON,
|
|
weaponDamage: 8,
|
|
bonusAttack: 0,
|
|
bonusHp: 0,
|
|
bonusArmor: 0,
|
|
iconPath: '/images/items/worn-short-sword.png',
|
|
},
|
|
...overrides,
|
|
} as CharacterItem;
|
|
}
|
|
|
|
describe('InventoryService', () => {
|
|
it("returns only the current character's items with definition data, quantity, and equipped state", async () => {
|
|
const items = [
|
|
characterItem({ id: 'item-1', quantity: 1 }),
|
|
characterItem({ id: 'item-2', quantity: 3, itemDefinitionId: 'def-2' }),
|
|
];
|
|
const characterItems = {
|
|
find: jest.fn().mockResolvedValue(items),
|
|
} as unknown as Repository<CharacterItem>;
|
|
const equipment = {
|
|
find: jest.fn().mockResolvedValue([
|
|
{
|
|
characterItemId: 'item-1',
|
|
slot: EquipmentSlot.WEAPON,
|
|
} as CharacterEquipment,
|
|
]),
|
|
} as unknown as Repository<CharacterEquipment>;
|
|
const service = new InventoryService(characterItems, equipment);
|
|
|
|
const result = await service.getInventory(CHARACTER_ID);
|
|
|
|
expect(characterItems.find).toHaveBeenCalledWith({
|
|
where: { characterId: CHARACTER_ID },
|
|
relations: { itemDefinition: true },
|
|
order: { createdAt: 'ASC' },
|
|
});
|
|
expect(result.items).toEqual([
|
|
{
|
|
id: 'item-1',
|
|
quantity: 1,
|
|
equipped: true,
|
|
item: {
|
|
key: 'worn-short-sword',
|
|
name: 'Abgenutztes Kurzschwert',
|
|
description: 'Die Klinge eines Rekruten, öfter geschliffen als geführt.',
|
|
rarity: 'COMMON',
|
|
equipmentSlot: 'WEAPON',
|
|
weaponDamage: 8,
|
|
bonusAttack: 0,
|
|
bonusHp: 0,
|
|
bonusArmor: 0,
|
|
iconPath: '/images/items/worn-short-sword.png',
|
|
},
|
|
},
|
|
{
|
|
id: 'item-2',
|
|
quantity: 3,
|
|
equipped: false,
|
|
item: expect.objectContaining({ key: 'worn-short-sword' }),
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns an empty list when the character owns nothing', async () => {
|
|
const characterItems = {
|
|
find: jest.fn().mockResolvedValue([]),
|
|
} as unknown as Repository<CharacterItem>;
|
|
const equipment = {
|
|
find: jest.fn().mockResolvedValue([]),
|
|
} as unknown as Repository<CharacterEquipment>;
|
|
const service = new InventoryService(characterItems, equipment);
|
|
|
|
await expect(service.getInventory(CHARACTER_ID)).resolves.toEqual({
|
|
items: [],
|
|
});
|
|
});
|
|
});
|