Files
ashen-realms/apps/api/src/characters/character-stats.service.ts
Bastian Wagner 02037b2917 feat(api): CharacterStatsService reports effective (regenerated) HP
- Update CharacterStatsService constructor to accept CharacterVitalsService
- Compute currentHp via CharacterVitalsService.effectiveHp() instead of raw pass-through
- Add hpRegenPerSecond and hpRegenSince fields to EffectiveCharacterStats return type
- Update spec with new test cases for regeneration calculation and field pass-through
- Equipment and combat tests now fail as expected (separate tasks will fix constructor calls)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 14:41:17 +02:00

74 lines
2.2 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { CharacterEquipment } from '../equipment/entities/character-equipment.entity';
import { EquipmentSlot } from '../items/equipment-slot.enum';
import { HP_REGEN_PER_SECOND } from './character-vitals.constants';
import { CharacterVitalsService } from './character-vitals.service';
import { Character } from './entities/character.entity';
export interface EffectiveCharacterStats {
maxHp: number;
currentHp: number;
attack: number;
weaponDamage: number;
armor: number;
combatPower: number;
hpRegenPerSecond: number;
hpRegenSince: Date | null;
}
type RepositoryScope = Pick<DataSource, 'getRepository'>;
/**
* Single authoritative source of effective character stats (spec §18).
* Replaces the Slice 0.3 `CharacterCombatStatsService` shortcut.
*/
@Injectable()
export class CharacterStatsService {
constructor(
private readonly dataSource: DataSource,
private readonly characterVitals: CharacterVitalsService,
) {}
async calculate(
character: Character,
scope?: RepositoryScope,
): Promise<EffectiveCharacterStats> {
const db = scope ?? this.dataSource;
const equipped = await db.getRepository(CharacterEquipment).find({
where: { characterId: character.id },
relations: { characterItem: { itemDefinition: true } },
});
let weaponDamage = 0;
let bonusHp = 0;
let bonusAttack = 0;
let bonusArmor = 0;
for (const slot of equipped) {
const definition = slot.characterItem.itemDefinition;
if (slot.slot === EquipmentSlot.WEAPON) {
weaponDamage = definition.weaponDamage;
}
bonusHp += definition.bonusHp;
bonusAttack += definition.bonusAttack;
bonusArmor += definition.bonusArmor;
}
const maxHp = character.baseHp + bonusHp;
const attack = character.baseAttack + bonusAttack;
const armor = bonusArmor;
return {
maxHp,
currentHp: this.characterVitals.effectiveHp(character, maxHp),
attack,
weaponDamage,
armor,
combatPower: maxHp / 10 + attack * 2 + weaponDamage * 2 + armor * 1.5,
hpRegenPerSecond: HP_REGEN_PER_SECOND,
hpRegenSince: character.hpRegenSince,
};
}
}