1
This commit is contained in:
@@ -423,4 +423,49 @@ describe('buildBattlePlan', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers fight_defeat over fight_victory when the round ends with player death after local battle settlement', () => {
|
||||
const state = {
|
||||
...createBaseState(),
|
||||
currentBattleNpcId: 'npc-opponent',
|
||||
currentNpcBattleMode: 'fight' as const,
|
||||
playerHp: 6,
|
||||
playerMaxHp: 30,
|
||||
sceneHostileNpcs: [
|
||||
{
|
||||
id: 'npc-opponent',
|
||||
name: '山道客',
|
||||
action: '提刀逼近',
|
||||
description: '测试敌人',
|
||||
animation: 'idle' as const,
|
||||
xMeters: 3,
|
||||
yOffset: 0,
|
||||
facing: 'left' as const,
|
||||
attackRange: 1,
|
||||
speed: 1,
|
||||
hp: 8,
|
||||
maxHp: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const plan = buildBattlePlan({
|
||||
state,
|
||||
option: {
|
||||
...createBattleOption(),
|
||||
functionId: 'battle_all_in_crush',
|
||||
},
|
||||
character: createTestCharacter(),
|
||||
totalSequenceMs: 900,
|
||||
turnVisualMs: 820,
|
||||
resetStageMs: 260,
|
||||
minTurnCount: 1,
|
||||
});
|
||||
|
||||
expect(plan.turns.map((turn) => turn.actor)).toEqual(['player', 'monster']);
|
||||
expect(plan.finalState.playerHp).toBe(0);
|
||||
expect(plan.finalState.inBattle).toBe(false);
|
||||
expect(plan.finalState.currentNpcBattleOutcome).toBe('fight_defeat');
|
||||
expect(plan.finalState.sceneHostileNpcs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,6 +87,22 @@ export type BattlePlan = {
|
||||
finalState: GameState;
|
||||
};
|
||||
|
||||
function resolveFightBattleOutcome(state: GameState): GameState['currentNpcBattleOutcome'] {
|
||||
if (state.currentNpcBattleMode === 'spar') {
|
||||
return state.currentNpcBattleOutcome;
|
||||
}
|
||||
if (state.playerHp <= 0) {
|
||||
return state.currentBattleNpcId ? 'fight_defeat' : state.currentNpcBattleOutcome;
|
||||
}
|
||||
if (
|
||||
state.currentBattleNpcId &&
|
||||
state.sceneHostileNpcs.every((monster) => monster.hp <= 0)
|
||||
) {
|
||||
return 'fight_victory';
|
||||
}
|
||||
return state.currentNpcBattleOutcome;
|
||||
}
|
||||
|
||||
function createEmptyCooldowns(character: Character) {
|
||||
return Object.fromEntries(character.skills.map((skill) => [skill.id, 0]));
|
||||
}
|
||||
@@ -543,11 +559,29 @@ export function buildBattlePlan({
|
||||
const preparedState = simulatedState;
|
||||
const turns: BattlePlanStep[] = [];
|
||||
const turnOrder = buildRoundTurnOrder(simulatedState, character);
|
||||
const pendingMonsterTurnIds = new Set(
|
||||
turnOrder
|
||||
.filter(
|
||||
(turnActor): turnActor is Extract<BattleTurnActor, {actor: 'monster'}> =>
|
||||
turnActor.actor === 'monster',
|
||||
)
|
||||
.map((turnActor) => turnActor.monsterId),
|
||||
);
|
||||
|
||||
for (const turnActor of turnOrder) {
|
||||
if (!simulatedState.inBattle || simulatedState.playerHp <= 0) {
|
||||
if (
|
||||
simulatedState.playerHp <= 0 ||
|
||||
simulatedState.currentNpcBattleOutcome === 'spar_complete' ||
|
||||
simulatedState.currentNpcBattleOutcome === 'fight_defeat'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
if (!simulatedState.inBattle && pendingMonsterTurnIds.size === 0) {
|
||||
break;
|
||||
}
|
||||
if (turnActor.actor === 'monster') {
|
||||
pendingMonsterTurnIds.delete(turnActor.monsterId);
|
||||
}
|
||||
|
||||
if (
|
||||
turnActor.actor === 'player' &&
|
||||
@@ -725,20 +759,14 @@ export function buildBattlePlan({
|
||||
}
|
||||
: monster,
|
||||
);
|
||||
const playerDefeated =
|
||||
const targetDefeated =
|
||||
!isNpcSpar &&
|
||||
resolvedMonsters.some(
|
||||
(monster) => monster.id === currentTarget.id && monster.hp <= 0,
|
||||
);
|
||||
const remainingMonsters = playerDefeated
|
||||
? resolvedMonsters.filter(
|
||||
(monster) =>
|
||||
!(monster.id === currentTarget.id && monster.hp <= 0),
|
||||
)
|
||||
: resolvedMonsters;
|
||||
const nextTarget = getClosestHostileNpc(
|
||||
originalPlayerX,
|
||||
remainingMonsters,
|
||||
resolvedMonsters.filter((monster) => monster.hp > 0),
|
||||
);
|
||||
|
||||
simulatedState = {
|
||||
@@ -757,7 +785,7 @@ export function buildBattlePlan({
|
||||
simulatedState.playerMana - selectedSkill.manaCost,
|
||||
),
|
||||
playerSkillCooldowns: appliedCooldowns,
|
||||
sceneHostileNpcs: remainingMonsters.map((monster) => ({
|
||||
sceneHostileNpcs: resolvedMonsters.map((monster) => ({
|
||||
...monster,
|
||||
characterAnimation: undefined,
|
||||
combatMode: undefined,
|
||||
@@ -766,14 +794,17 @@ export function buildBattlePlan({
|
||||
inBattle:
|
||||
isNpcSpar
|
||||
? !wouldEndSpar
|
||||
: remainingMonsters.length > 0 && simulatedState.playerHp > 0,
|
||||
: (resolvedMonsters.some((monster) => monster.hp > 0) ||
|
||||
pendingMonsterTurnIds.size > 0) &&
|
||||
simulatedState.playerHp > 0,
|
||||
currentNpcBattleOutcome: wouldEndSpar
|
||||
? 'spar_complete'
|
||||
: !isNpcSpar &&
|
||||
remainingMonsters.length === 0 &&
|
||||
simulatedState.currentBattleNpcId
|
||||
? 'fight_victory'
|
||||
: simulatedState.currentNpcBattleOutcome,
|
||||
: pendingMonsterTurnIds.size > 0
|
||||
? simulatedState.currentNpcBattleOutcome
|
||||
: resolveFightBattleOutcome({
|
||||
...simulatedState,
|
||||
sceneHostileNpcs: resolvedMonsters,
|
||||
}),
|
||||
};
|
||||
|
||||
turns.push({
|
||||
@@ -787,7 +818,7 @@ export function buildBattlePlan({
|
||||
appliedCooldowns,
|
||||
damage: playerDamage,
|
||||
criticalHit: playerCriticalHit,
|
||||
defeated: playerDefeated,
|
||||
defeated: targetDefeated,
|
||||
endsBattle: wouldEndSpar,
|
||||
delivery: playerDelivery,
|
||||
playerHpAfterAction: simulatedState.playerHp,
|
||||
@@ -870,16 +901,9 @@ export function buildBattlePlan({
|
||||
const defeated = resolvedMonsters.some(
|
||||
(monster) => monster.id === currentTarget.id && monster.hp <= 0,
|
||||
);
|
||||
const remainingMonsters = defeated
|
||||
? resolvedMonsters.filter(
|
||||
(monster) =>
|
||||
!(monster.id === currentTarget.id && monster.hp <= 0),
|
||||
)
|
||||
: resolvedMonsters;
|
||||
|
||||
simulatedState = {
|
||||
...simulatedState,
|
||||
sceneHostileNpcs: remainingMonsters.map((monster) => ({
|
||||
sceneHostileNpcs: resolvedMonsters.map((monster) => ({
|
||||
...monster,
|
||||
characterAnimation: undefined,
|
||||
combatMode: undefined,
|
||||
@@ -894,11 +918,16 @@ export function buildBattlePlan({
|
||||
}),
|
||||
),
|
||||
inBattle:
|
||||
remainingMonsters.length > 0 && simulatedState.playerHp > 0,
|
||||
(resolvedMonsters.some((monster) => monster.hp > 0) ||
|
||||
pendingMonsterTurnIds.size > 0) &&
|
||||
simulatedState.playerHp > 0,
|
||||
currentNpcBattleOutcome:
|
||||
remainingMonsters.length === 0 && simulatedState.currentBattleNpcId
|
||||
? 'fight_victory'
|
||||
: simulatedState.currentNpcBattleOutcome,
|
||||
pendingMonsterTurnIds.size > 0
|
||||
? simulatedState.currentNpcBattleOutcome
|
||||
: resolveFightBattleOutcome({
|
||||
...simulatedState,
|
||||
sceneHostileNpcs: resolvedMonsters,
|
||||
}),
|
||||
};
|
||||
|
||||
turns.push({
|
||||
@@ -923,7 +952,8 @@ export function buildBattlePlan({
|
||||
}
|
||||
|
||||
const actingMonster = simulatedState.sceneHostileNpcs.find(
|
||||
(monster) => monster.id === turnActor.monsterId && monster.hp > 0,
|
||||
(monster) =>
|
||||
monster.id === turnActor.monsterId,
|
||||
);
|
||||
if (!actingMonster) {
|
||||
continue;
|
||||
@@ -1035,10 +1065,18 @@ export function buildBattlePlan({
|
||||
inBattle:
|
||||
isNpcSpar
|
||||
? !wouldEndSpar
|
||||
: nextPlayerHp > 0 && simulatedState.sceneHostileNpcs.length > 0,
|
||||
: nextPlayerHp > 0 &&
|
||||
(simulatedState.sceneHostileNpcs.some((monster) => monster.hp > 0) ||
|
||||
pendingMonsterTurnIds.size > 0),
|
||||
currentNpcBattleOutcome: wouldEndSpar
|
||||
? 'spar_complete'
|
||||
: simulatedState.currentNpcBattleOutcome,
|
||||
: pendingMonsterTurnIds.size > 0
|
||||
? simulatedState.currentNpcBattleOutcome
|
||||
: resolveFightBattleOutcome({
|
||||
...simulatedState,
|
||||
...damagedState,
|
||||
playerHp: nextPlayerHp,
|
||||
}),
|
||||
};
|
||||
|
||||
turns.push({
|
||||
@@ -1115,10 +1153,18 @@ export function buildBattlePlan({
|
||||
inBattle:
|
||||
isNpcSpar
|
||||
? !wouldEndSpar
|
||||
: nextPlayerHp > 0 && simulatedState.sceneHostileNpcs.length > 0,
|
||||
: nextPlayerHp > 0 &&
|
||||
(simulatedState.sceneHostileNpcs.some((monster) => monster.hp > 0) ||
|
||||
pendingMonsterTurnIds.size > 0),
|
||||
currentNpcBattleOutcome: wouldEndSpar
|
||||
? 'spar_complete'
|
||||
: simulatedState.currentNpcBattleOutcome,
|
||||
: pendingMonsterTurnIds.size > 0
|
||||
? simulatedState.currentNpcBattleOutcome
|
||||
: resolveFightBattleOutcome({
|
||||
...simulatedState,
|
||||
...damagedState,
|
||||
playerHp: nextPlayerHp,
|
||||
}),
|
||||
};
|
||||
|
||||
turns.push({
|
||||
@@ -1144,8 +1190,8 @@ export function buildBattlePlan({
|
||||
return {
|
||||
preparedState,
|
||||
turns,
|
||||
finalState: {
|
||||
...simulatedState,
|
||||
finalState: {
|
||||
...simulatedState,
|
||||
companions: resetCompanionCombatPresentation(simulatedState.companions),
|
||||
animationState: AnimationState.IDLE,
|
||||
playerActionMode: 'idle' as const,
|
||||
@@ -1155,11 +1201,15 @@ export function buildBattlePlan({
|
||||
simulatedState.currentNpcBattleOutcome === 'spar_complete' ||
|
||||
simulatedState.playerHp <= 0
|
||||
? false
|
||||
: simulatedState.sceneHostileNpcs.length > 0,
|
||||
: simulatedState.sceneHostileNpcs.some((monster) => monster.hp > 0),
|
||||
sceneHostileNpcs: resetCombatPresentation(
|
||||
simulatedState.sceneHostileNpcs,
|
||||
simulatedState.sceneHostileNpcs.filter((monster) => monster.hp > 0),
|
||||
simulatedState.playerX,
|
||||
),
|
||||
currentNpcBattleOutcome: resolveFightBattleOutcome({
|
||||
...simulatedState,
|
||||
sceneHostileNpcs: simulatedState.sceneHostileNpcs.filter((monster) => monster.hp > 0),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user