Files
ashen-realms/apps/api/src/inventory/inventory.service.ts
Bastian Wagner cedd87f648 Merge branch 'master' into worktree-playable-slice-0.6.5-renown-reputation
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>
2026-08-21 19:23:10 +02:00

72 lines
2.3 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
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';
export interface InventoryItemDto {
id: string;
quantity: number;
equipped: boolean;
item: {
key: string;
name: string;
description: string;
rarity: ItemRarity;
equipmentSlot: EquipmentSlot | null;
weaponDamage: number;
bonusAttack: number;
bonusHp: number;
bonusArmor: number;
iconPath: string;
};
}
export interface InventoryResponseDto {
items: InventoryItemDto[];
}
@Injectable()
export class InventoryService {
constructor(
@InjectRepository(CharacterItem)
private readonly characterItems: Repository<CharacterItem>,
@InjectRepository(CharacterEquipment)
private readonly equipment: Repository<CharacterEquipment>,
) {}
async getInventory(characterId: string): Promise<InventoryResponseDto> {
const [items, equipped] = await Promise.all([
this.characterItems.find({
where: { characterId },
relations: { itemDefinition: true },
order: { createdAt: 'ASC' },
}),
this.equipment.find({ where: { characterId } }),
]);
const equippedIds = new Set(equipped.map((row) => row.characterItemId));
return {
items: items.map((characterItem) => ({
id: characterItem.id,
quantity: characterItem.quantity,
equipped: equippedIds.has(characterItem.id),
item: {
key: characterItem.itemDefinition.key,
name: characterItem.itemDefinition.name,
description: characterItem.itemDefinition.description,
rarity: characterItem.itemDefinition.rarity,
equipmentSlot: characterItem.itemDefinition.equipmentSlot,
weaponDamage: characterItem.itemDefinition.weaponDamage,
bonusAttack: characterItem.itemDefinition.bonusAttack,
bonusHp: characterItem.itemDefinition.bonusHp,
bonusArmor: characterItem.itemDefinition.bonusArmor,
iconPath: characterItem.itemDefinition.iconPath,
},
})),
};
}
}