This commit is contained in:
Bastian Wagner
2026-08-22 16:41:47 +02:00
parent dfa62fd152
commit 081c9f83f9
137 changed files with 11594 additions and 1302 deletions

View File

@@ -14,7 +14,11 @@ import { TravelService } from '../travel/travel.service';
import { TravelStatus } from '../travel/travel-status.enum';
import { CombatAction } from './combat-action.enum';
import { CombatEngineService } from './combat-engine.service';
import { CombatEngineState, CombatIntent } from './combat-engine.types';
import {
ActiveStatusEffect,
CombatEngineState,
CombatIntent,
} from './combat-engine.types';
import {
characterNotFound,
characterTooWounded,
@@ -30,18 +34,30 @@ import {
} from './combat.errors';
import { CombatStatus } from './combat-status.enum';
import { CombatEvent } from './entities/combat-event.entity';
import { Combat, CombatPlayerState } from './entities/combat.entity';
import {
Combat,
CombatMonsterState,
CombatPlayerState,
} from './entities/combat.entity';
import { StatusEffectType } from './status-effect.enum';
// Playable Slice 0.6 spec §3: fixed at 2 for V1, not yet backed by the
// persistent consumable inventory.
const STARTING_POTION_COUNT = 2;
export interface CombatStatusEffectDto {
type: StatusEffectType;
remainingRounds: number;
damagePerRound: number;
}
export interface CombatPlayerDto {
name: string;
maxHp: number;
currentHp: number;
potionsRemaining: number;
potionsMax: number;
statusEffects: CombatStatusEffectDto[];
}
export interface CombatMonsterDto {
@@ -61,6 +77,7 @@ export interface CombatEventDto {
source: string;
target: string;
amount?: number;
statusEffect?: StatusEffectType;
}
export interface CombatDto {
@@ -163,7 +180,11 @@ export class CombatService {
armor: playerStats.armor,
potionsRemaining: STARTING_POTION_COUNT,
},
monsterState: { attack: monster.attack, armor: monster.armor },
monsterState: {
attack: monster.attack,
armor: monster.armor,
abilities: monster.abilities ?? {},
},
completedAt: null,
});
await combats.save(combat);
@@ -223,11 +244,10 @@ export class CombatService {
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.
// startCombat already uses (character, then combat). Keeping both code
// paths on the same order avoids a lock-order inversion that could
// deadlock two concurrent requests against the same character. Do not
// reorder this.
const character = await this.lockCharacter(characters, characterId);
const combat = await combats.findOne({
@@ -256,7 +276,7 @@ export class CombatService {
combat.playerCurrentHp = result.state.player.currentHp;
combat.monsterCurrentHp = result.state.monster.currentHp;
combat.playerState = result.state.player.stats as CombatPlayerState;
combat.monsterState = result.state.monster.stats;
combat.monsterState = result.state.monster.stats as CombatMonsterState;
if (combat.status !== CombatStatus.ACTIVE) {
combat.completedAt = new Date();
this.characterVitals.resume(character, combat.playerCurrentHp);
@@ -284,6 +304,7 @@ export class CombatService {
source: event.source,
target: event.target,
amount: event.amount ?? null,
statusEffect: event.statusEffect ?? null,
});
await combatEvents.save(entity);
}
@@ -308,7 +329,13 @@ export class CombatService {
this.loadEvents(combat.id, combatEvents),
]);
return this.toCombatDto(combat, reloadedCharacter.name, monster, events, rewards);
return this.toCombatDto(
combat,
reloadedCharacter.name,
monster,
events,
rewards,
);
});
}
@@ -420,6 +447,9 @@ export class CombatService {
currentHp: combat.playerCurrentHp,
potionsRemaining: combat.playerState.potionsRemaining,
potionsMax: STARTING_POTION_COUNT,
statusEffects: this.toStatusEffectDtos(
combat.playerState.statusEffects,
),
},
monster: {
key: monster.key,
@@ -437,8 +467,19 @@ export class CombatService {
source: event.source,
target: event.target,
amount: event.amount ?? undefined,
statusEffect: event.statusEffect ?? undefined,
})),
rewards,
};
}
private toStatusEffectDtos(
effects: ActiveStatusEffect[] | undefined,
): CombatStatusEffectDto[] {
return (effects ?? []).map((effect) => ({
type: effect.type,
remainingRounds: effect.remainingRounds,
damagePerRound: effect.damagePerRound,
}));
}
}