Merge branch 'master' into worktree-encounter-status
This commit is contained in:
@@ -7,6 +7,8 @@ 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 { CombatRewardService } from '../rewards/combat-reward.service';
|
||||
import type { CombatRewardDto } from '../rewards/combat-reward.service';
|
||||
import { TravelService } from '../travel/travel.service';
|
||||
import { TravelStatus } from '../travel/travel-status.enum';
|
||||
import { CombatAction } from './combat-action.enum';
|
||||
@@ -58,6 +60,7 @@ export interface CombatDto {
|
||||
player: CombatPlayerDto;
|
||||
monster: CombatMonsterDto;
|
||||
events: CombatEventDto[];
|
||||
rewards: CombatRewardDto | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -67,6 +70,7 @@ export class CombatService {
|
||||
private readonly travelService: TravelService,
|
||||
private readonly combatEngine: CombatEngineService,
|
||||
private readonly characterCombatStats: CharacterCombatStatsService,
|
||||
private readonly combatRewards: CombatRewardService,
|
||||
) {}
|
||||
|
||||
async startCombat(
|
||||
@@ -147,7 +151,7 @@ export class CombatService {
|
||||
encounter.status = HuntEncounterStatus.IN_PROGRESS;
|
||||
await encounters.save(encounter);
|
||||
|
||||
return this.toCombatDto(combat, character.name, monster, []);
|
||||
return this.toCombatDto(combat, character.name, monster, [], null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,13 +164,14 @@ export class CombatService {
|
||||
throw combatNotFound();
|
||||
}
|
||||
|
||||
const [character, monster, events] = await Promise.all([
|
||||
const [character, monster, events, rewards] = await Promise.all([
|
||||
this.loadCharacter(combat.characterId),
|
||||
this.loadMonster(combat.monsterDefinitionId),
|
||||
this.loadEvents(combat.id),
|
||||
this.combatRewards.loadRewards(combat.id),
|
||||
]);
|
||||
|
||||
return this.toCombatDto(combat, character.name, monster, events);
|
||||
return this.toCombatDto(combat, character.name, monster, events, rewards);
|
||||
}
|
||||
|
||||
async getActiveCombat(characterId: string): Promise<CombatDto | null> {
|
||||
@@ -184,7 +189,7 @@ export class CombatService {
|
||||
this.loadEvents(combat.id),
|
||||
]);
|
||||
|
||||
return this.toCombatDto(combat, character.name, monster, events);
|
||||
return this.toCombatDto(combat, character.name, monster, events, null);
|
||||
}
|
||||
|
||||
async performAction(
|
||||
@@ -193,9 +198,18 @@ export class CombatService {
|
||||
action: CombatAction,
|
||||
): Promise<CombatDto> {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const characters = manager.getRepository(Character);
|
||||
const combats = manager.getRepository(Combat);
|
||||
const combatEvents = manager.getRepository(CombatEvent);
|
||||
|
||||
// Lock the character before the combat row here, matching the order
|
||||
// startCombat already uses (character, then combat). grantVictoryRewards
|
||||
// locks the character again later in this same transaction, which is a
|
||||
// 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 combat = await combats.findOne({
|
||||
where: { id: combatId, characterId },
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
@@ -242,6 +256,14 @@ export class CombatService {
|
||||
await combatEvents.save(entity);
|
||||
}
|
||||
|
||||
// The engine decided the outcome; rewards are resolved here, outside it
|
||||
// (spec §30). Running inside this transaction means a reward failure
|
||||
// rolls the whole round back rather than leaving a half-granted victory.
|
||||
const rewards =
|
||||
combat.status === CombatStatus.WON
|
||||
? await this.combatRewards.grantVictoryRewards(manager, combat)
|
||||
: null;
|
||||
|
||||
const [character, monster, events] = await Promise.all([
|
||||
this.loadCharacter(
|
||||
combat.characterId,
|
||||
@@ -254,7 +276,7 @@ export class CombatService {
|
||||
this.loadEvents(combat.id, combatEvents),
|
||||
]);
|
||||
|
||||
return this.toCombatDto(combat, character.name, monster, events);
|
||||
return this.toCombatDto(combat, character.name, monster, events, rewards);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -354,6 +376,7 @@ export class CombatService {
|
||||
playerName: string,
|
||||
monster: MonsterDefinition,
|
||||
events: CombatEvent[],
|
||||
rewards: CombatRewardDto | null,
|
||||
): CombatDto {
|
||||
return {
|
||||
id: combat.id,
|
||||
@@ -380,6 +403,7 @@ export class CombatService {
|
||||
target: event.target,
|
||||
amount: event.amount ?? undefined,
|
||||
})),
|
||||
rewards,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user