feat(hunting): show cleared encounters and resume interrupted fights
The hunt screen kept whatever roll was last in memory, so a player coming back from a fight saw every encounter as fresh. Encounters now carry their own status, which the combat module advances as fights start and end. - hunt_encounters.status replaces consumed_at, which only recorded that a fight had begun and could not distinguish a win from a loss - a lost fight hands the encounter back as AVAILABLE, so it can be retried; the unique index tying one combat to one encounter goes with it - GET /hunts/active serves the resumable hunt, which the hunt page adopts on entry rather than trusting its in-memory roll - defeated encounters are crossed out and lose their hover and attack action - a fresh page load rejoins a combat the server still holds open Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { CharacterCombatStatsService } from '../characters/character-combat-stat
|
||||
import { Character } from '../characters/entities/character.entity';
|
||||
import { Hunt } from '../hunting/entities/hunt.entity';
|
||||
import { HuntEncounter } from '../hunting/entities/hunt-encounter.entity';
|
||||
import { HuntEncounterStatus } from '../hunting/hunt-encounter-status.enum';
|
||||
import { HuntStatus } from '../hunting/hunt-status.enum';
|
||||
import { MonsterDefinition } from '../monsters/entities/monster-definition.entity';
|
||||
import { TravelService } from '../travel/travel.service';
|
||||
@@ -93,7 +94,7 @@ export class CombatService {
|
||||
if (!encounter) {
|
||||
throw huntEncounterNotFound();
|
||||
}
|
||||
if (encounter.consumedAt) {
|
||||
if (encounter.status !== HuntEncounterStatus.AVAILABLE) {
|
||||
throw huntEncounterAlreadyConsumed();
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ export class CombatService {
|
||||
});
|
||||
await combats.save(combat);
|
||||
|
||||
encounter.consumedAt = new Date();
|
||||
encounter.status = HuntEncounterStatus.IN_PROGRESS;
|
||||
await encounters.save(encounter);
|
||||
|
||||
return this.toCombatDto(combat, character.name, monster, []);
|
||||
@@ -216,6 +217,11 @@ export class CombatService {
|
||||
combat.monsterCurrentHp = result.state.monster.currentHp;
|
||||
if (combat.status !== CombatStatus.ACTIVE) {
|
||||
combat.completedAt = new Date();
|
||||
await this.settleEncounter(
|
||||
manager.getRepository(HuntEncounter),
|
||||
combat.huntEncounterId,
|
||||
combat.status,
|
||||
);
|
||||
}
|
||||
await combats.save(combat);
|
||||
|
||||
@@ -252,6 +258,28 @@ export class CombatService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the fight's outcome on the encounter that spawned it. A win
|
||||
* retires the encounter; a loss hands it back so the player can try again.
|
||||
*/
|
||||
private async settleEncounter(
|
||||
encounters: Repository<HuntEncounter>,
|
||||
encounterId: string,
|
||||
outcome: CombatStatus,
|
||||
): Promise<void> {
|
||||
const encounter = await encounters.findOneBy({ id: encounterId });
|
||||
if (!encounter) {
|
||||
// combats.hunt_encounter_id is a RESTRICT FK; guaranteed to exist.
|
||||
throw combatStateInvalid();
|
||||
}
|
||||
|
||||
encounter.status =
|
||||
outcome === CombatStatus.WON
|
||||
? HuntEncounterStatus.DEFEATED
|
||||
: HuntEncounterStatus.AVAILABLE;
|
||||
await encounters.save(encounter);
|
||||
}
|
||||
|
||||
private async lockCharacter(
|
||||
characters: Repository<Character>,
|
||||
characterId: string,
|
||||
|
||||
Reference in New Issue
Block a user