feat(api): re-anchor HP regeneration before an equipment-driven max-HP change
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { DataSource, EntityManager, EntityTarget } from 'typeorm';
|
import { DataSource, EntityManager, EntityTarget } from 'typeorm';
|
||||||
import { CharacterStatsService } from '../characters/character-stats.service';
|
import { CharacterStatsService } from '../characters/character-stats.service';
|
||||||
|
import { CharacterVitalsService } from '../characters/character-vitals.service';
|
||||||
import { Character } from '../characters/entities/character.entity';
|
import { Character } from '../characters/entities/character.entity';
|
||||||
import { CombatStatus } from '../combat/combat-status.enum';
|
import { CombatStatus } from '../combat/combat-status.enum';
|
||||||
import { Combat } from '../combat/entities/combat.entity';
|
import { Combat } from '../combat/entities/combat.entity';
|
||||||
@@ -164,6 +165,7 @@ function character(overrides: Partial<Character> = {}): Character {
|
|||||||
baseHp: 100,
|
baseHp: 100,
|
||||||
baseAttack: 6,
|
baseAttack: 6,
|
||||||
currentHp: 100,
|
currentHp: 100,
|
||||||
|
hpRegenSince: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
} as Character;
|
} as Character;
|
||||||
}
|
}
|
||||||
@@ -178,8 +180,18 @@ function createHarness(state: Partial<State> = {}) {
|
|||||||
...state,
|
...state,
|
||||||
};
|
};
|
||||||
const dataSource = new FakeDataSource(fullState);
|
const dataSource = new FakeDataSource(fullState);
|
||||||
const characterStats = new CharacterStatsService(dataSource as unknown as DataSource);
|
const characterVitals = new CharacterVitalsService({
|
||||||
const service = new EquipmentService(dataSource as unknown as DataSource, characterStats);
|
now: () => new Date('2026-08-18T09:00:00.000Z'),
|
||||||
|
});
|
||||||
|
const characterStats = new CharacterStatsService(
|
||||||
|
dataSource as unknown as DataSource,
|
||||||
|
characterVitals,
|
||||||
|
);
|
||||||
|
const service = new EquipmentService(
|
||||||
|
dataSource as unknown as DataSource,
|
||||||
|
characterStats,
|
||||||
|
characterVitals,
|
||||||
|
);
|
||||||
return { state: fullState, service };
|
return { state: fullState, service };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +420,33 @@ describe('EquipmentService', () => {
|
|||||||
'CHARACTER_IN_COMBAT',
|
'CHARACTER_IN_COMBAT',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('re-anchors HP regeneration so a later max-HP increase does not gift accumulated overflow', async () => {
|
||||||
|
const bonusHpHelm = itemDefinition({
|
||||||
|
id: 'def-bonus-hp-helm',
|
||||||
|
key: 'bonus-hp-helm',
|
||||||
|
name: 'Gepolsterter Helm',
|
||||||
|
equipmentSlot: EquipmentSlot.HEAD,
|
||||||
|
bonusHp: 20,
|
||||||
|
weaponDamage: 0,
|
||||||
|
});
|
||||||
|
const { state, service } = createHarness({
|
||||||
|
characters: [character({ currentHp: 100, hpRegenSince: null })],
|
||||||
|
itemDefinitions: [bonusHpHelm],
|
||||||
|
characterItems: [
|
||||||
|
{
|
||||||
|
id: BANDIT_HOOD_ITEM_ID,
|
||||||
|
characterId: CHARACTER_ID,
|
||||||
|
itemDefinitionId: bonusHpHelm.id,
|
||||||
|
quantity: 1,
|
||||||
|
} as CharacterItem,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.equip(CHARACTER_ID, BANDIT_HOOD_ITEM_ID);
|
||||||
|
|
||||||
|
expect(state.characters[0].currentHp).toBe(100);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getEquipment', () => {
|
describe('getEquipment', () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { CharacterStatsService } from '../characters/character-stats.service';
|
import { CharacterStatsService } from '../characters/character-stats.service';
|
||||||
|
import { CharacterVitalsService } from '../characters/character-vitals.service';
|
||||||
import { Character } from '../characters/entities/character.entity';
|
import { Character } from '../characters/entities/character.entity';
|
||||||
import { CombatStatus } from '../combat/combat-status.enum';
|
import { CombatStatus } from '../combat/combat-status.enum';
|
||||||
import { Combat } from '../combat/entities/combat.entity';
|
import { Combat } from '../combat/entities/combat.entity';
|
||||||
@@ -48,6 +49,7 @@ export class EquipmentService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
private readonly characterStats: CharacterStatsService,
|
private readonly characterStats: CharacterStatsService,
|
||||||
|
private readonly characterVitals: CharacterVitalsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getEquipment(characterId: string): Promise<EquipmentResponseDto> {
|
async getEquipment(characterId: string): Promise<EquipmentResponseDto> {
|
||||||
@@ -106,6 +108,10 @@ export class EquipmentService {
|
|||||||
throw itemLevelRequirementNotMet();
|
throw itemLevelRequirementNotMet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statsBeforeChange = await this.characterStats.calculate(character, manager);
|
||||||
|
this.characterVitals.settle(character, statsBeforeChange.maxHp);
|
||||||
|
await characters.save(character);
|
||||||
|
|
||||||
const existing = await equipmentRepo.findOne({
|
const existing = await equipmentRepo.findOne({
|
||||||
where: { characterId, slot: definition.equipmentSlot },
|
where: { characterId, slot: definition.equipmentSlot },
|
||||||
lock: { mode: 'pessimistic_write' },
|
lock: { mode: 'pessimistic_write' },
|
||||||
|
|||||||
Reference in New Issue
Block a user