feat(api): mirror HP onto the character each round and resume regen at combat end
This commit is contained in:
@@ -579,6 +579,41 @@ describe('CombatService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('mirrors the player HP onto the character each round while the fight continues', async () => {
|
||||
const { dataSource, service, combatId } = await startedCombat();
|
||||
|
||||
await service.performAction(CHARACTER_ID, combatId, CombatAction.ATTACK);
|
||||
|
||||
expect(dataSource.state.characters[0].currentHp).toBe(95);
|
||||
expect(dataSource.state.characters[0].hpRegenSince).toBeNull();
|
||||
});
|
||||
|
||||
it('restarts regeneration on the character once the fight is won', async () => {
|
||||
const state = createState({ monsters: [monster({ maxHp: 10 })] });
|
||||
const { dataSource, service, combatId } = await startedCombat(state);
|
||||
|
||||
await service.performAction(CHARACTER_ID, combatId, CombatAction.ATTACK);
|
||||
|
||||
expect(dataSource.state.characters[0].currentHp).toBe(
|
||||
dataSource.state.combats[0].playerCurrentHp,
|
||||
);
|
||||
expect(dataSource.state.characters[0].hpRegenSince).toEqual(
|
||||
new Date('2026-08-18T09:00:00.000Z'),
|
||||
);
|
||||
});
|
||||
|
||||
it('restarts regeneration from 0 HP once the fight is lost', async () => {
|
||||
const state = createState({ characters: [character({ baseHp: 1, currentHp: 1 })] });
|
||||
const { dataSource, service, combatId } = await startedCombat(state);
|
||||
|
||||
await service.performAction(CHARACTER_ID, combatId, CombatAction.ATTACK);
|
||||
|
||||
expect(dataSource.state.characters[0].currentHp).toBe(0);
|
||||
expect(dataSource.state.characters[0].hpRegenSince).toEqual(
|
||||
new Date('2026-08-18T09:00:00.000Z'),
|
||||
);
|
||||
});
|
||||
|
||||
it('ends the combat as WON, stops persisting new rounds, and rejects further actions', async () => {
|
||||
const state = createState({ monsters: [monster({ maxHp: 10 })] });
|
||||
const { dataSource, service, combatId } = await startedCombat(state);
|
||||
@@ -650,11 +685,19 @@ describe('CombatService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('lets a lost encounter be fought again as a fresh combat', async () => {
|
||||
it('lets a lost encounter be fought again once the character has recovered HP', async () => {
|
||||
const state = createState({ characters: [character({ baseHp: 1, currentHp: 1 })] });
|
||||
const { dataSource, service, combatId } = await startedCombat(state);
|
||||
await service.performAction(CHARACTER_ID, combatId, CombatAction.ATTACK);
|
||||
|
||||
// The loss now mirrors 0 HP onto the character (this task); simulate
|
||||
// that regeneration has since restored it before retrying. This test
|
||||
// is about the encounter itself being retryable once the character
|
||||
// can fight again, not about regen math (covered by
|
||||
// CharacterVitalsService's own tests).
|
||||
dataSource.state.characters[0].currentHp = 1;
|
||||
dataSource.state.characters[0].hpRegenSince = null;
|
||||
|
||||
const retry = await service.startCombat(CHARACTER_ID, ENCOUNTER_ID);
|
||||
|
||||
expect(retry.id).not.toBe(combatId);
|
||||
|
||||
@@ -225,7 +225,7 @@ export class CombatService {
|
||||
// no-op re-lock — but locking it first here keeps both code paths
|
||||
// consistent and avoids a lock-order inversion that could deadlock two
|
||||
// concurrent requests against the same character. Do not reorder this.
|
||||
await this.lockCharacter(characters, characterId);
|
||||
const character = await this.lockCharacter(characters, characterId);
|
||||
|
||||
const combat = await combats.findOne({
|
||||
where: { id: combatId, characterId },
|
||||
@@ -253,12 +253,16 @@ export class CombatService {
|
||||
combat.monsterState = result.state.monster.stats;
|
||||
if (combat.status !== CombatStatus.ACTIVE) {
|
||||
combat.completedAt = new Date();
|
||||
this.characterVitals.resume(character, combat.playerCurrentHp);
|
||||
await this.settleEncounter(
|
||||
manager.getRepository(HuntEncounter),
|
||||
combat.huntEncounterId,
|
||||
combat.status,
|
||||
);
|
||||
} else {
|
||||
this.characterVitals.pause(character, combat.playerCurrentHp);
|
||||
}
|
||||
await characters.save(character);
|
||||
await combats.save(combat);
|
||||
|
||||
const startingSequence = await combatEvents.count({
|
||||
@@ -286,7 +290,7 @@ export class CombatService {
|
||||
? await this.combatRewards.grantVictoryRewards(manager, combat)
|
||||
: null;
|
||||
|
||||
const [character, monster, events] = await Promise.all([
|
||||
const [reloadedCharacter, monster, events] = await Promise.all([
|
||||
this.loadCharacter(
|
||||
combat.characterId,
|
||||
manager.getRepository(Character),
|
||||
@@ -298,7 +302,7 @@ export class CombatService {
|
||||
this.loadEvents(combat.id, combatEvents),
|
||||
]);
|
||||
|
||||
return this.toCombatDto(combat, character.name, monster, events, rewards);
|
||||
return this.toCombatDto(combat, reloadedCharacter.name, monster, events, rewards);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user