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 { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { CharacterCombatStatsService } from './character-combat-stats.service';
|
import { CharacterStatsService } from './character-stats.service';
|
||||||
import { CharactersController } from './characters.controller';
|
import { CharactersController } from './characters.controller';
|
||||||
import { CharactersService } from './characters.service';
|
import { CharactersService } from './characters.service';
|
||||||
import { Character } from './entities/character.entity';
|
import { Character } from './entities/character.entity';
|
||||||
@@ -8,7 +8,7 @@ import { Character } from './entities/character.entity';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Character])],
|
imports: [TypeOrmModule.forFeature([Character])],
|
||||||
controllers: [CharactersController],
|
controllers: [CharactersController],
|
||||||
providers: [CharactersService, CharacterCombatStatsService],
|
providers: [CharactersService, CharacterStatsService],
|
||||||
exports: [CharacterCombatStatsService],
|
exports: [CharacterStatsService],
|
||||||
})
|
})
|
||||||
export class CharactersModule {}
|
export class CharactersModule {}
|
||||||
|
|||||||
@@ -2,11 +2,27 @@ import { NotFoundException } from '@nestjs/common';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
||||||
import { SOUTH_GATE_ID } from '../database/seeds/vertical-slice.constants';
|
import { SOUTH_GATE_ID } from '../database/seeds/vertical-slice.constants';
|
||||||
|
import { CharacterStatsService } from './character-stats.service';
|
||||||
import { Character } from './entities/character.entity';
|
import { Character } from './entities/character.entity';
|
||||||
import { CharactersService } from './characters.service';
|
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', () => {
|
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 = {
|
const repository = {
|
||||||
findOne: jest.fn().mockResolvedValue({
|
findOne: jest.fn().mockResolvedValue({
|
||||||
id: DEMO_CHARACTER_ID,
|
id: DEMO_CHARACTER_ID,
|
||||||
@@ -20,11 +36,12 @@ describe('CharactersService', () => {
|
|||||||
currentLocation: {
|
currentLocation: {
|
||||||
id: SOUTH_GATE_ID,
|
id: SOUTH_GATE_ID,
|
||||||
key: 'south-gate',
|
key: 'south-gate',
|
||||||
name: 'S\u00fcdtor von Graufurt',
|
name: 'Südtor von Graufurt',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
} as unknown as Repository<Character>;
|
} 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({
|
await expect(service.getDemoCharacter()).resolves.toEqual({
|
||||||
id: DEMO_CHARACTER_ID,
|
id: DEMO_CHARACTER_ID,
|
||||||
@@ -33,18 +50,17 @@ describe('CharactersService', () => {
|
|||||||
experience: 0,
|
experience: 0,
|
||||||
silver: 0,
|
silver: 0,
|
||||||
currentHp: 100,
|
currentHp: 100,
|
||||||
maxHp: 100,
|
maxHp: 115,
|
||||||
attack: 6,
|
attack: 7,
|
||||||
currentLocation: {
|
currentLocation: {
|
||||||
id: SOUTH_GATE_ID,
|
id: SOUTH_GATE_ID,
|
||||||
key: 'south-gate',
|
key: 'south-gate',
|
||||||
name: 'S\u00fcdtor von Graufurt',
|
name: 'Südtor von Graufurt',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(repository.findOne).toHaveBeenCalledWith({
|
expect(characterStats.calculate).toHaveBeenCalledWith(
|
||||||
where: { id: DEMO_CHARACTER_ID },
|
expect.objectContaining({ id: DEMO_CHARACTER_ID }),
|
||||||
relations: { currentLocation: true },
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exposes the persisted silver so the HUD never has to guess', async () => {
|
it('exposes the persisted silver so the HUD never has to guess', async () => {
|
||||||
@@ -65,7 +81,7 @@ describe('CharactersService', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
} as unknown as Repository<Character>;
|
} as unknown as Repository<Character>;
|
||||||
const service = new CharactersService(repository);
|
const service = new CharactersService(repository, fakeCharacterStats());
|
||||||
|
|
||||||
await expect(service.getDemoCharacter()).resolves.toEqual(
|
await expect(service.getDemoCharacter()).resolves.toEqual(
|
||||||
expect.objectContaining({ experience: 24, silver: 18 }),
|
expect.objectContaining({ experience: 24, silver: 18 }),
|
||||||
@@ -76,7 +92,7 @@ describe('CharactersService', () => {
|
|||||||
const repository = {
|
const repository = {
|
||||||
findOne: jest.fn().mockResolvedValue(null),
|
findOne: jest.fn().mockResolvedValue(null),
|
||||||
} as unknown as Repository<Character>;
|
} as unknown as Repository<Character>;
|
||||||
const service = new CharactersService(repository);
|
const service = new CharactersService(repository, fakeCharacterStats());
|
||||||
|
|
||||||
await expect(service.getDemoCharacter()).rejects.toBeInstanceOf(
|
await expect(service.getDemoCharacter()).rejects.toBeInstanceOf(
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
|
||||||
|
import { CharacterStatsService } from './character-stats.service';
|
||||||
import { Character } from './entities/character.entity';
|
import { Character } from './entities/character.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -9,6 +10,7 @@ export class CharactersService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Character)
|
@InjectRepository(Character)
|
||||||
private readonly characters: Repository<Character>,
|
private readonly characters: Repository<Character>,
|
||||||
|
private readonly characterStats: CharacterStatsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getDemoCharacter() {
|
async getDemoCharacter() {
|
||||||
@@ -21,6 +23,8 @@ export class CharactersService {
|
|||||||
throw new NotFoundException('Demo character has not been seeded');
|
throw new NotFoundException('Demo character has not been seeded');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stats = await this.characterStats.calculate(character);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: character.id,
|
id: character.id,
|
||||||
name: character.name,
|
name: character.name,
|
||||||
@@ -28,8 +32,8 @@ export class CharactersService {
|
|||||||
experience: character.experience,
|
experience: character.experience,
|
||||||
silver: character.silver,
|
silver: character.silver,
|
||||||
currentHp: character.currentHp,
|
currentHp: character.currentHp,
|
||||||
maxHp: character.baseHp,
|
maxHp: stats.maxHp,
|
||||||
attack: character.baseAttack,
|
attack: stats.attack,
|
||||||
currentLocation: {
|
currentLocation: {
|
||||||
id: character.currentLocation.id,
|
id: character.currentLocation.id,
|
||||||
key: character.currentLocation.key,
|
key: character.currentLocation.key,
|
||||||
|
|||||||
Reference in New Issue
Block a user