feat(api): retire CharacterCombatStatsService; characters/me returns effective stats
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import { CharacterCombatStatsService } from './character-combat-stats.service';
|
||||
import { Character } from './entities/character.entity';
|
||||
|
||||
describe('CharacterCombatStatsService', () => {
|
||||
it('derives combat stats from the character, with a temporary fixed weapon/armor stand-in', () => {
|
||||
const service = new CharacterCombatStatsService();
|
||||
const character = { baseHp: 100, baseAttack: 6 } as Character;
|
||||
|
||||
expect(service.getStats(character)).toEqual({
|
||||
maxHp: 100,
|
||||
attack: 6,
|
||||
weaponDamage: 8,
|
||||
armor: 6,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Character } from './entities/character.entity';
|
||||
|
||||
export interface CharacterCombatStats {
|
||||
maxHp: number;
|
||||
attack: number;
|
||||
weaponDamage: number;
|
||||
armor: number;
|
||||
}
|
||||
|
||||
// TEMPORARY (Slice 0.3): there is no equipment system yet. These constants
|
||||
// stand in for the starting weapon/armor until Slice 0.5 introduces real
|
||||
// equipment. Replacing them there must not change this method's signature
|
||||
// or the combat API it feeds (spec §10).
|
||||
const TEMPORARY_WEAPON_DAMAGE = 8;
|
||||
const TEMPORARY_ARMOR = 6;
|
||||
|
||||
@Injectable()
|
||||
export class CharacterCombatStatsService {
|
||||
getStats(character: Character): CharacterCombatStats {
|
||||
return {
|
||||
maxHp: character.baseHp,
|
||||
attack: character.baseAttack,
|
||||
weaponDamage: TEMPORARY_WEAPON_DAMAGE,
|
||||
armor: TEMPORARY_ARMOR,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CharacterCombatStatsService } from './character-combat-stats.service';
|
||||
import { CharacterStatsService } from './character-stats.service';
|
||||
import { CharactersController } from './characters.controller';
|
||||
import { CharactersService } from './characters.service';
|
||||
import { Character } from './entities/character.entity';
|
||||
@@ -8,7 +8,7 @@ import { Character } from './entities/character.entity';
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Character])],
|
||||
controllers: [CharactersController],
|
||||
providers: [CharactersService, CharacterCombatStatsService],
|
||||
exports: [CharacterCombatStatsService],
|
||||
providers: [CharactersService, CharacterStatsService],
|
||||
exports: [CharacterStatsService],
|
||||
})
|
||||
export class CharactersModule {}
|
||||
|
||||
@@ -2,11 +2,27 @@ import { NotFoundException } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
||||
import { SOUTH_GATE_ID } from '../database/seeds/vertical-slice.constants';
|
||||
import { CharacterStatsService } from './character-stats.service';
|
||||
import { Character } from './entities/character.entity';
|
||||
import { CharactersService } from './characters.service';
|
||||
|
||||
function fakeCharacterStats(
|
||||
overrides: Partial<{ maxHp: number; attack: number }> = {},
|
||||
): CharacterStatsService {
|
||||
return {
|
||||
calculate: jest.fn().mockResolvedValue({
|
||||
maxHp: overrides.maxHp ?? 100,
|
||||
currentHp: 100,
|
||||
attack: overrides.attack ?? 6,
|
||||
weaponDamage: 8,
|
||||
armor: 0,
|
||||
combatPower: 0,
|
||||
}),
|
||||
} as unknown as CharacterStatsService;
|
||||
}
|
||||
|
||||
describe('CharactersService', () => {
|
||||
it('returns the demo character with its current location summary', async () => {
|
||||
it('returns the demo character with effective attack/HP and its location summary', async () => {
|
||||
const repository = {
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
id: DEMO_CHARACTER_ID,
|
||||
@@ -20,11 +36,12 @@ describe('CharactersService', () => {
|
||||
currentLocation: {
|
||||
id: SOUTH_GATE_ID,
|
||||
key: 'south-gate',
|
||||
name: 'S\u00fcdtor von Graufurt',
|
||||
name: 'Südtor von Graufurt',
|
||||
},
|
||||
}),
|
||||
} as unknown as Repository<Character>;
|
||||
const service = new CharactersService(repository);
|
||||
const characterStats = fakeCharacterStats({ maxHp: 115, attack: 7 });
|
||||
const service = new CharactersService(repository, characterStats);
|
||||
|
||||
await expect(service.getDemoCharacter()).resolves.toEqual({
|
||||
id: DEMO_CHARACTER_ID,
|
||||
@@ -33,18 +50,17 @@ describe('CharactersService', () => {
|
||||
experience: 0,
|
||||
silver: 0,
|
||||
currentHp: 100,
|
||||
maxHp: 100,
|
||||
attack: 6,
|
||||
maxHp: 115,
|
||||
attack: 7,
|
||||
currentLocation: {
|
||||
id: SOUTH_GATE_ID,
|
||||
key: 'south-gate',
|
||||
name: 'S\u00fcdtor von Graufurt',
|
||||
name: 'Südtor von Graufurt',
|
||||
},
|
||||
});
|
||||
expect(repository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: DEMO_CHARACTER_ID },
|
||||
relations: { currentLocation: true },
|
||||
});
|
||||
expect(characterStats.calculate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: DEMO_CHARACTER_ID }),
|
||||
);
|
||||
});
|
||||
|
||||
it('exposes the persisted silver so the HUD never has to guess', async () => {
|
||||
@@ -65,7 +81,7 @@ describe('CharactersService', () => {
|
||||
},
|
||||
}),
|
||||
} as unknown as Repository<Character>;
|
||||
const service = new CharactersService(repository);
|
||||
const service = new CharactersService(repository, fakeCharacterStats());
|
||||
|
||||
await expect(service.getDemoCharacter()).resolves.toEqual(
|
||||
expect.objectContaining({ experience: 24, silver: 18 }),
|
||||
@@ -76,7 +92,7 @@ describe('CharactersService', () => {
|
||||
const repository = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as Repository<Character>;
|
||||
const service = new CharactersService(repository);
|
||||
const service = new CharactersService(repository, fakeCharacterStats());
|
||||
|
||||
await expect(service.getDemoCharacter()).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
||||
import { CharacterStatsService } from './character-stats.service';
|
||||
import { Character } from './entities/character.entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -9,6 +10,7 @@ export class CharactersService {
|
||||
constructor(
|
||||
@InjectRepository(Character)
|
||||
private readonly characters: Repository<Character>,
|
||||
private readonly characterStats: CharacterStatsService,
|
||||
) {}
|
||||
|
||||
async getDemoCharacter() {
|
||||
@@ -21,6 +23,8 @@ export class CharactersService {
|
||||
throw new NotFoundException('Demo character has not been seeded');
|
||||
}
|
||||
|
||||
const stats = await this.characterStats.calculate(character);
|
||||
|
||||
return {
|
||||
id: character.id,
|
||||
name: character.name,
|
||||
@@ -28,8 +32,8 @@ export class CharactersService {
|
||||
experience: character.experience,
|
||||
silver: character.silver,
|
||||
currentHp: character.currentHp,
|
||||
maxHp: character.baseHp,
|
||||
attack: character.baseAttack,
|
||||
maxHp: stats.maxHp,
|
||||
attack: stats.attack,
|
||||
currentLocation: {
|
||||
id: character.currentLocation.id,
|
||||
key: character.currentLocation.key,
|
||||
|
||||
Reference in New Issue
Block a user