Split custom world generation into staged lightweight batches
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-05 22:20:30 +08:00
parent 89cecda7da
commit fcd8d727b0
57 changed files with 7646 additions and 1425 deletions

View File

@@ -1,5 +1,12 @@
import { resolveRoleCombatStats } from '../../data/attributeCombat';
import { resolveCharacterAttributeProfile } from '../../data/attributeResolver';
import { appendBuildBuffs, resolveCompanionOutgoingDamage, resolveMonsterOutgoingDamage, resolvePlayerOutgoingDamage, tickBuildBuffs } from '../../data/buildDamage';
import {
appendBuildBuffs,
resolveCompanionOutgoingDamageResult,
resolveMonsterOutgoingDamageResult,
resolvePlayerOutgoingDamageResult,
tickBuildBuffs,
} from '../../data/buildDamage';
import {
getSkillDelivery,
} from '../../data/characterCombat';
@@ -45,6 +52,7 @@ export type BattlePlanStep =
selectedSkillId: string | null;
appliedCooldowns: Record<string, number>;
damage: number;
criticalHit?: boolean;
defeated: boolean;
endsBattle: boolean;
delivery: CombatDelivery;
@@ -58,6 +66,7 @@ export type BattlePlanStep =
selectedSkillId: string | null;
appliedCooldowns: Record<string, number>;
damage: number;
criticalHit?: boolean;
defeated: boolean;
endsBattle: boolean;
delivery: CombatDelivery;
@@ -71,6 +80,7 @@ export type BattlePlanStep =
targetCompanionNpcId?: string;
targetX: number;
damage: number;
criticalHit?: boolean;
endsBattle: boolean;
selectedSkillId: string | null;
npcCharacterId: string | null;
@@ -165,7 +175,16 @@ function buildCombatTurnOrder(
actorTimings.set(getCombatActorKey('player'), {
actor: 'player',
nextAt: 0,
cadence: 1400 / Math.max((resolveCharacterAttributeProfile(playerCharacter, state.worldType, state.customWorldProfile)?.values.axis_b ?? 48) / 12, 1),
cadence: 1400 / Math.max(
resolveRoleCombatStats(
resolveCharacterAttributeProfile(
playerCharacter,
state.worldType,
state.customWorldProfile,
),
).turnSpeed,
1,
),
});
state.companions
@@ -177,7 +196,16 @@ function buildCombatTurnOrder(
actor: 'companion',
id: companion.npcId,
nextAt: 0,
cadence: 1400 / Math.max((resolveCharacterAttributeProfile(companionCharacter, state.worldType, state.customWorldProfile)?.values.axis_b ?? 48) / 12, 1),
cadence: 1400 / Math.max(
resolveRoleCombatStats(
resolveCharacterAttributeProfile(
companionCharacter,
state.worldType,
state.customWorldProfile,
),
).turnSpeed,
1,
),
});
});
@@ -321,15 +349,28 @@ export function buildBattlePlan({
resetStageMs: number;
minTurnCount: number;
}): BattlePlan {
const targetMonster = getClosestMonster(state.playerX, state.sceneMonsters);
const resolvedSceneMonsters =
state.sceneMonsters.length > 0
? state.sceneMonsters
: (state.sceneHostileNpcs ?? []);
const battleState: GameState = {
...state,
sceneMonsters: resolvedSceneMonsters,
sceneHostileNpcs: resolvedSceneMonsters,
};
const targetMonster = getClosestMonster(
battleState.playerX,
battleState.sceneMonsters,
);
if (!targetMonster) {
return {
preparedState: state,
preparedState: battleState,
turns: [],
finalState: {
...state,
...battleState,
inBattle: false,
sceneMonsters: [],
sceneHostileNpcs: [],
companions: resetCompanionCombatPresentation(state.companions),
animationState: AnimationState.IDLE,
playerActionMode: 'idle' as const,
@@ -340,9 +381,16 @@ export function buildBattlePlan({
}
const functionEffect = getFunctionEffect(option.functionId);
const isNpcSpar = state.currentNpcBattleMode === 'spar';
const isNpcSpar = battleState.currentNpcBattleMode === 'spar';
const sequenceMs = Math.round(totalSequenceMs * (functionEffect.turnTimeMultiplier ?? 1));
const turnOrder = buildCombatTurnOrder(state, character, sequenceMs, turnVisualMs, resetStageMs, minTurnCount);
const turnOrder = buildCombatTurnOrder(
battleState,
character,
sequenceMs,
turnVisualMs,
resetStageMs,
minTurnCount,
);
const normalizedOption = normalizeSkillProbabilities(option, character);
const npcBattleResources = new Map<string, {
character: Character;
@@ -350,7 +398,7 @@ export function buildBattlePlan({
cooldowns: Record<string, number>;
}>();
state.sceneMonsters.forEach(monster => {
battleState.sceneMonsters.forEach(monster => {
const npcCharacterId = monster.encounter?.characterId ?? null;
const npcCharacter = npcCharacterId ? getCharacterById(npcCharacterId) : null;
if (!npcCharacter) return;
@@ -363,9 +411,16 @@ export function buildBattlePlan({
});
let simulatedState: GameState = {
...applyRecoveryEffectToState(state, character, option.functionId),
companions: resetCompanionCombatPresentation(state.companions),
sceneMonsters: resetCombatPresentation(state.sceneMonsters, state.playerX),
...applyRecoveryEffectToState(battleState, character, option.functionId),
companions: resetCompanionCombatPresentation(battleState.companions),
sceneMonsters: resetCombatPresentation(
battleState.sceneMonsters,
battleState.playerX,
),
sceneHostileNpcs: resetCombatPresentation(
battleState.sceneMonsters,
battleState.playerX,
),
activeCombatEffects: [],
playerActionMode: 'idle' as const,
currentNpcBattleOutcome: null,
@@ -373,7 +428,7 @@ export function buildBattlePlan({
const preparedState = simulatedState;
const turns: BattlePlanStep[] = [];
for (const turn of turnOrder) {
for (const [turnIndex, turn] of turnOrder.entries()) {
const currentTarget = getClosestMonster(simulatedState.playerX, simulatedState.sceneMonsters);
if (!currentTarget) break;
@@ -398,14 +453,16 @@ export function buildBattlePlan({
...cooledDown,
[selectedSkill.id]: selectedSkill.cooldownTurns,
};
const damage = isNpcSpar
? 1
: resolvePlayerOutgoingDamage(
const damageResult = isNpcSpar
? null
: resolvePlayerOutgoingDamageResult(
simulatedState,
character,
selectedSkill.damage,
functionEffect.damageMultiplier ?? 1,
`${option.functionId}:player:${turnIndex}:${selectedSkill.id}:${currentTarget.id}`,
);
const damage = isNpcSpar ? 1 : damageResult!.damage;
const wouldEndSpar = isNpcSpar && currentTarget.hp - damage <= 1;
const resolvedMonsters = simulatedState.sceneMonsters.map(monster =>
@@ -460,6 +517,7 @@ export function buildBattlePlan({
selectedSkillId: selectedSkill.id,
appliedCooldowns,
damage,
criticalHit: damageResult?.isCritical ?? false,
defeated,
endsBattle: wouldEndSpar,
delivery,
@@ -513,15 +571,17 @@ export function buildBattlePlan({
...cooledDown,
[selectedSkill.id]: selectedSkill.cooldownTurns,
};
const damage = isNpcSpar
? 1
: resolveCompanionOutgoingDamage(
const damageResult = isNpcSpar
? null
: resolveCompanionOutgoingDamageResult(
companionCharacter,
selectedSkill.damage,
functionEffect.damageMultiplier ?? 1,
state.worldType,
state.customWorldProfile,
`${option.functionId}:companion:${turnIndex}:${companion.npcId}:${selectedSkill.id}:${targetMonster.id}`,
);
const damage = isNpcSpar ? 1 : damageResult!.damage;
const wouldEndSpar = isNpcSpar && targetMonster.hp - damage <= 1;
const resolvedMonsters = simulatedState.sceneMonsters.map(monster =>
@@ -571,6 +631,7 @@ export function buildBattlePlan({
selectedSkillId: selectedSkill.id,
appliedCooldowns,
damage,
criticalHit: damageResult?.isCritical ?? false,
defeated,
endsBattle: wouldEndSpar,
delivery,
@@ -611,15 +672,17 @@ export function buildBattlePlan({
if (selectedSkill) {
const delivery = getSkillDelivery(selectedSkill);
const strikeX = getSkillStrikeX(selectedSkill, originalMonsterX, targetX);
const damage = isNpcSpar
? 1
: resolveCompanionOutgoingDamage(
const damageResult = isNpcSpar
? null
: resolveCompanionOutgoingDamageResult(
npcCombatant.character,
selectedSkill.damage,
functionEffect.incomingDamageMultiplier ?? 1,
state.worldType,
state.customWorldProfile,
`${option.functionId}:monster-skill:${turnIndex}:${actingMonster.id}:${selectedSkill.id}:${randomTarget.kind}:${randomTarget.kind === 'companion' ? randomTarget.npcId : 'player'}`,
);
const damage = isNpcSpar ? 1 : damageResult!.damage;
const wouldEndSpar = isNpcSpar && randomTarget.kind === 'player' && simulatedState.playerHp - damage <= 1;
npcBattleResources.set(actingMonster.id, {
@@ -662,6 +725,7 @@ export function buildBattlePlan({
targetCompanionNpcId: randomTarget.kind === 'companion' ? randomTarget.npcId : undefined,
targetX,
damage,
criticalHit: damageResult?.isCritical ?? false,
endsBattle: wouldEndSpar,
selectedSkillId: selectedSkill.id,
npcCharacterId: npcCombatant.character.id,
@@ -672,15 +736,17 @@ export function buildBattlePlan({
}
const strikeX = getMeleeStrikeX(originalMonsterX, targetX);
const damage = isNpcSpar
? 1
: resolveMonsterOutgoingDamage(
const damageResult = isNpcSpar
? null
: resolveMonsterOutgoingDamageResult(
actingMonster,
9,
functionEffect.incomingDamageMultiplier ?? 1,
state.worldType,
state.customWorldProfile,
`${option.functionId}:monster:${turnIndex}:${actingMonster.id}:${randomTarget.kind}:${randomTarget.kind === 'companion' ? randomTarget.npcId : 'player'}`,
);
const damage = isNpcSpar ? 1 : damageResult!.damage;
const wouldEndSpar = isNpcSpar && randomTarget.kind === 'player' && simulatedState.playerHp - damage <= 1;
const damagedState = applyDamageToPartyTarget(simulatedState, randomTarget, damage);
@@ -714,6 +780,7 @@ export function buildBattlePlan({
targetCompanionNpcId: randomTarget.kind === 'companion' ? randomTarget.npcId : undefined,
targetX,
damage,
criticalHit: damageResult?.isCritical ?? false,
endsBattle: wouldEndSpar,
selectedSkillId: null,
npcCharacterId: null,
@@ -735,6 +802,10 @@ export function buildBattlePlan({
? false
: simulatedState.sceneMonsters.length > 0,
sceneMonsters: resetCombatPresentation(simulatedState.sceneMonsters, simulatedState.playerX),
sceneHostileNpcs: resetCombatPresentation(
simulatedState.sceneMonsters,
simulatedState.playerX,
),
},
};
}