579
src/data/stateFunctions.ts
Normal file
579
src/data/stateFunctions.ts
Normal file
@@ -0,0 +1,579 @@
|
||||
import {
|
||||
Character,
|
||||
FunctionCategory,
|
||||
PlayerStateMode,
|
||||
SceneDirective,
|
||||
SceneMonster,
|
||||
SkillStyle,
|
||||
StoryOption,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import {
|
||||
CAMP_TRAVEL_HOME_FUNCTION,
|
||||
NPC_CHAT_FUNCTION,
|
||||
NPC_PREVIEW_TALK_FUNCTION,
|
||||
NPC_RECRUIT_FUNCTION,
|
||||
STATE_FUNCTION_DEFINITIONS as SPLIT_STATE_FUNCTION_DEFINITIONS,
|
||||
STATE_FUNCTION_PROMPT_DESCRIPTIONS as SPLIT_STATE_FUNCTION_PROMPT_DESCRIPTIONS,
|
||||
} from './functionCatalog';
|
||||
import {
|
||||
getForwardScenePreset,
|
||||
getTravelScenePreset,
|
||||
getWorldCampScenePreset,
|
||||
} from './scenePresets';
|
||||
import stateFunctionOverridesJson from './stateFunctionOverrides.json';
|
||||
|
||||
export interface FunctionEffectConfig {
|
||||
damageMultiplier?: number;
|
||||
incomingDamageMultiplier?: number;
|
||||
healAmount?: number;
|
||||
manaRestore?: number;
|
||||
cooldownTickBonus?: number;
|
||||
turnTimeMultiplier?: number;
|
||||
skillWeights?: Partial<Record<SkillStyle, number>>;
|
||||
escapeDurationMs?: number;
|
||||
escapeDistance?: number;
|
||||
monsterLagStart?: number;
|
||||
monsterLagEnd?: number;
|
||||
sceneShift?: number;
|
||||
enterBattle?: boolean;
|
||||
}
|
||||
|
||||
export type FunctionVisualConfig = Omit<SceneDirective, 'monsterChanges'> & {
|
||||
monsterActionTemplate?: string;
|
||||
monsterAnimation?: 'idle' | 'move' | 'attack';
|
||||
monsterMoveMeters?: number;
|
||||
};
|
||||
|
||||
export interface StateFunctionDefinition {
|
||||
id: string;
|
||||
state: PlayerStateMode;
|
||||
category: FunctionCategory;
|
||||
text: string;
|
||||
description: string;
|
||||
visual: FunctionVisualConfig;
|
||||
effect: FunctionEffectConfig;
|
||||
}
|
||||
|
||||
export type StateFunctionDefinitionOverride = Partial<
|
||||
Pick<StateFunctionDefinition, 'state' | 'category' | 'text' | 'description'>
|
||||
> & {
|
||||
visual?: Partial<FunctionVisualConfig>;
|
||||
effect?: Partial<FunctionEffectConfig>;
|
||||
};
|
||||
|
||||
export type StateFunctionOverrideMap = Record<
|
||||
string,
|
||||
StateFunctionDefinitionOverride
|
||||
>;
|
||||
|
||||
export interface FunctionAvailabilityContext {
|
||||
worldType: WorldType;
|
||||
playerCharacter: Character | null;
|
||||
inBattle: boolean;
|
||||
currentSceneId?: string | null;
|
||||
currentSceneName?: string | null;
|
||||
monsters: SceneMonster[];
|
||||
playerHp: number;
|
||||
playerMaxHp: number;
|
||||
playerMana: number;
|
||||
playerMaxMana: number;
|
||||
}
|
||||
|
||||
const FUNCTION_OPTION_PRIORITIES: Record<string, number> = {
|
||||
[CAMP_TRAVEL_HOME_FUNCTION.id]: 4,
|
||||
[NPC_PREVIEW_TALK_FUNCTION.id]: 3,
|
||||
[NPC_RECRUIT_FUNCTION.id]: 3,
|
||||
[NPC_CHAT_FUNCTION.id]: 1,
|
||||
};
|
||||
|
||||
const MODEL_PRIORITY_LOCKED_OPTION_COUNT = 2;
|
||||
|
||||
export function getFunctionPromptDescription(
|
||||
functionId: string,
|
||||
fallback?: string,
|
||||
) {
|
||||
return (
|
||||
SPLIT_STATE_FUNCTION_PROMPT_DESCRIPTIONS[functionId] ??
|
||||
fallback ??
|
||||
functionId
|
||||
);
|
||||
}
|
||||
|
||||
const STATE_FUNCTION_OVERRIDES =
|
||||
stateFunctionOverridesJson as StateFunctionOverrideMap;
|
||||
const BASE_FUNCTIONS = [...SPLIT_STATE_FUNCTION_DEFINITIONS];
|
||||
|
||||
function mergeStateFunctionDefinition(
|
||||
definition: StateFunctionDefinition,
|
||||
override?: StateFunctionDefinitionOverride,
|
||||
): StateFunctionDefinition {
|
||||
if (!override) {
|
||||
return {
|
||||
...definition,
|
||||
visual: { ...definition.visual },
|
||||
effect: {
|
||||
...definition.effect,
|
||||
skillWeights: definition.effect.skillWeights
|
||||
? { ...definition.effect.skillWeights }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const mergedSkillWeights = override.effect?.skillWeights
|
||||
? {
|
||||
...(definition.effect.skillWeights ?? {}),
|
||||
...override.effect.skillWeights,
|
||||
}
|
||||
: definition.effect.skillWeights
|
||||
? { ...definition.effect.skillWeights }
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...definition,
|
||||
...override,
|
||||
visual: {
|
||||
...definition.visual,
|
||||
...(override.visual ?? {}),
|
||||
},
|
||||
effect: {
|
||||
...definition.effect,
|
||||
...(override.effect ?? {}),
|
||||
skillWeights: mergedSkillWeights,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyRuntimeFunctionAdjustments(
|
||||
definitions: StateFunctionDefinition[],
|
||||
) {
|
||||
return definitions
|
||||
.filter((definition) => definition.id !== 'idle_follow_clue')
|
||||
.map((definition) => {
|
||||
if (definition.id === 'idle_explore_forward') {
|
||||
return {
|
||||
...definition,
|
||||
text: '继续向前探索',
|
||||
description:
|
||||
'沿着当前场景继续深入,把前路真正探出来,下一刻就可能撞上新的危险或际遇。',
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.id === 'idle_call_out') {
|
||||
return {
|
||||
...definition,
|
||||
text: '主动出声试探',
|
||||
description:
|
||||
'主动朝前方喊话试探,可能把附近潜着的角色或怪物直接从远处引出来。',
|
||||
};
|
||||
}
|
||||
|
||||
return definition;
|
||||
});
|
||||
}
|
||||
|
||||
export function buildStateFunctionDefinitions(
|
||||
overrides: StateFunctionOverrideMap = STATE_FUNCTION_OVERRIDES,
|
||||
) {
|
||||
return applyRuntimeFunctionAdjustments(
|
||||
BASE_FUNCTIONS.map((definition) =>
|
||||
mergeStateFunctionDefinition(definition, overrides[definition.id]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const ALL_FUNCTIONS = buildStateFunctionDefinitions();
|
||||
|
||||
function hasAliveMonsters(monsters: SceneMonster[]) {
|
||||
return monsters.some((monster) => monster.hp > 0);
|
||||
}
|
||||
|
||||
function getPrimaryMonster(context: FunctionAvailabilityContext) {
|
||||
return (
|
||||
context.monsters.find((monster) => monster.hp > 0) ??
|
||||
context.monsters[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function getPlayerHpRatio(context: FunctionAvailabilityContext) {
|
||||
return context.playerHp / Math.max(context.playerMaxHp, 1);
|
||||
}
|
||||
|
||||
function getPlayerManaRatio(context: FunctionAvailabilityContext) {
|
||||
return context.playerMana / Math.max(context.playerMaxMana, 1);
|
||||
}
|
||||
|
||||
function getMonsterHpRatio(context: FunctionAvailabilityContext) {
|
||||
const monster = getPrimaryMonster(context);
|
||||
if (!monster) return 0;
|
||||
return monster.hp / Math.max(monster.maxHp, 1);
|
||||
}
|
||||
|
||||
function buildSuggestedActionText(
|
||||
definition: StateFunctionDefinition,
|
||||
context: FunctionAvailabilityContext,
|
||||
) {
|
||||
const monster = getPrimaryMonster(context);
|
||||
const monsterName = monster?.name ?? '前方怪物';
|
||||
const playerHpRatio = getPlayerHpRatio(context);
|
||||
const playerManaRatio = getPlayerManaRatio(context);
|
||||
const monsterHpRatio = getMonsterHpRatio(context);
|
||||
const forwardScene = getForwardScenePreset(
|
||||
context.worldType,
|
||||
context.currentSceneId,
|
||||
);
|
||||
const travelScene = getTravelScenePreset(
|
||||
context.worldType,
|
||||
context.currentSceneId,
|
||||
);
|
||||
|
||||
const sceneName = context.currentSceneName ?? '前路';
|
||||
|
||||
if (definition.id === 'idle_explore_forward') {
|
||||
if (playerHpRatio <= 0.35)
|
||||
return `按着伤口,沿着${sceneName}继续往深处摸去`;
|
||||
if (forwardScene) return `顺着${sceneName}的路势,继续朝前方深处探去`;
|
||||
return `拨开${sceneName}前的遮挡,继续朝更深处探去`;
|
||||
}
|
||||
|
||||
if (definition.id === 'idle_call_out') {
|
||||
return `冲着${sceneName}前方扬声试探,看是谁先被逼出来`;
|
||||
}
|
||||
|
||||
switch (definition.id) {
|
||||
case 'battle_finisher_window':
|
||||
if (monsterHpRatio <= 0.25) return `完成对${monsterName}的残血收割`;
|
||||
if (monsterHpRatio <= 0.45) return `抓住${monsterName}露出的破绽补上重击`;
|
||||
return `盯住${monsterName}的空当准备终结一击`;
|
||||
case 'battle_all_in_crush':
|
||||
if (monsterHpRatio <= 0.25) return `压上去收掉${monsterName}最后一口气`;
|
||||
if (playerHpRatio <= 0.35) return `顶着伤势强压${monsterName}赌一波强杀`;
|
||||
return `正面强压${monsterName}不给喘息`;
|
||||
case 'battle_guard_break':
|
||||
if (monsterHpRatio <= 0.35) return `砸开${monsterName}的架势直接斩落`;
|
||||
return `重击破开${monsterName}的招架`;
|
||||
case 'battle_probe_pressure':
|
||||
if (playerManaRatio <= 0.3)
|
||||
return `稳住节奏试探${monsterName},先省下灵力`;
|
||||
if (monsterHpRatio <= 0.3) return `稳步逼近,补掉${monsterName}残余血量`;
|
||||
return `稳扎稳打继续试探${monsterName}`;
|
||||
case 'battle_feint_step':
|
||||
if (monsterHpRatio <= 0.35) return `虚晃切进去收掉${monsterName}`;
|
||||
return `借假动作切进${monsterName}身前`;
|
||||
case 'battle_recover_breath':
|
||||
if (playerHpRatio <= 0.35) return '原地打坐恢复血量';
|
||||
if (playerManaRatio <= 0.3) return '收势调息回一口灵力';
|
||||
return '边守边调息稳住节奏';
|
||||
case 'battle_escape_breakout':
|
||||
if (playerHpRatio <= 0.35) return `撑着伤势先脱离${monsterName}的追杀`;
|
||||
return `转身拉开距离,甩开${monsterName}`;
|
||||
case 'idle_explore_forward':
|
||||
if (forwardScene) return `继续向前探路`;
|
||||
if (playerHpRatio <= 0.35) return '拖着伤势继续向前摸索';
|
||||
return '继续向前探索前路';
|
||||
case 'idle_travel_next_scene':
|
||||
return travelScene ? `前往${travelScene.name}` : '前往其他场景';
|
||||
case 'idle_rest_focus':
|
||||
if (playerHpRatio <= 0.35) return '原地打坐恢复气血';
|
||||
if (playerManaRatio <= 0.35) return '盘坐调息恢复灵力';
|
||||
return '原地调息整理状态';
|
||||
case 'idle_observe_signs':
|
||||
return '停步观察附近的风吹草动';
|
||||
case 'idle_follow_clue':
|
||||
return '顺着可疑痕迹继续靠近';
|
||||
case 'idle_call_out':
|
||||
return '朝前方主动出声试探';
|
||||
default:
|
||||
return definition.text;
|
||||
}
|
||||
}
|
||||
|
||||
function buildOptionDetailText(
|
||||
definition: StateFunctionDefinition,
|
||||
context: FunctionAvailabilityContext,
|
||||
) {
|
||||
const forwardScene = getForwardScenePreset(
|
||||
context.worldType,
|
||||
context.currentSceneId,
|
||||
);
|
||||
const travelScene = getTravelScenePreset(
|
||||
context.worldType,
|
||||
context.currentSceneId,
|
||||
);
|
||||
const sceneName = context.currentSceneName ?? '当前区域';
|
||||
|
||||
if (definition.id === 'idle_explore_forward') {
|
||||
return forwardScene
|
||||
? `沿着${sceneName}继续往前压过去,真正把前方会遇到的人影、怪物或宝藏探出来。`
|
||||
: `继续深入${sceneName}前方未探明的地带,下一刻就可能撞见新的动静。`;
|
||||
}
|
||||
|
||||
if (definition.id === 'idle_call_out') {
|
||||
return '主动打破寂静,把附近潜着的角色或怪物从屏幕外直接引到眼前。';
|
||||
}
|
||||
|
||||
switch (definition.id) {
|
||||
case 'idle_explore_forward':
|
||||
return forwardScene
|
||||
? `沿当前路径继续深入,可能会遇到角色、怪物、宝藏……`
|
||||
: '继续向前试探这片区域,可能会遇到角色、怪物、宝藏……';
|
||||
case 'idle_travel_next_scene':
|
||||
return travelScene?.description ?? '离开当前区域,前往相邻场景继续冒险。';
|
||||
case 'idle_observe_signs':
|
||||
return '先确认附近是否潜伏着人影、怪物或其他值得靠近的东西。';
|
||||
case 'idle_follow_clue':
|
||||
return '沿着声音、脚印或灵气痕迹继续摸过去,可能更快接近前方目标。';
|
||||
case 'idle_call_out':
|
||||
return '主动打破寂静,看看附近是谁或什么东西先有反应。';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getFunctionPriority(
|
||||
definition: StateFunctionDefinition,
|
||||
context: FunctionAvailabilityContext,
|
||||
) {
|
||||
const playerHpRatio = getPlayerHpRatio(context);
|
||||
const playerManaRatio = getPlayerManaRatio(context);
|
||||
const monsterHpRatio = getMonsterHpRatio(context);
|
||||
|
||||
if (definition.id === 'idle_call_out') {
|
||||
return 5;
|
||||
}
|
||||
|
||||
switch (definition.id) {
|
||||
case 'battle_recover_breath':
|
||||
return (
|
||||
(playerHpRatio <= 0.35 ? 10 : 0) + (playerManaRatio <= 0.3 ? 6 : 0)
|
||||
);
|
||||
case 'battle_finisher_window':
|
||||
return monsterHpRatio <= 0.25 ? 10 : monsterHpRatio <= 0.45 ? 6 : 1;
|
||||
case 'battle_all_in_crush':
|
||||
return monsterHpRatio <= 0.25 ? 8 : playerHpRatio <= 0.35 ? 2 : 4;
|
||||
case 'battle_guard_break':
|
||||
return monsterHpRatio <= 0.4 ? 6 : 3;
|
||||
case 'battle_probe_pressure':
|
||||
return playerManaRatio <= 0.3 ? 8 : 4;
|
||||
case 'battle_feint_step':
|
||||
return monsterHpRatio <= 0.5 ? 5 : 3;
|
||||
case 'battle_escape_breakout':
|
||||
return playerHpRatio <= 0.2 ? 9 : playerHpRatio <= 0.35 ? 5 : 1;
|
||||
case 'idle_rest_focus':
|
||||
return playerHpRatio <= 0.35 || playerManaRatio <= 0.35 ? 8 : 2;
|
||||
case 'idle_explore_forward':
|
||||
return playerHpRatio > 0.45 ? 6 : 2;
|
||||
case 'idle_travel_next_scene':
|
||||
return playerHpRatio > 0.45 ? 5 : 3;
|
||||
case 'idle_observe_signs':
|
||||
return 4;
|
||||
case 'idle_follow_clue':
|
||||
return 5;
|
||||
case 'idle_call_out':
|
||||
return 3;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesCategory(
|
||||
definition: StateFunctionDefinition,
|
||||
context: FunctionAvailabilityContext,
|
||||
) {
|
||||
switch (definition.category) {
|
||||
case 'battle':
|
||||
case 'escape':
|
||||
return context.inBattle && hasAliveMonsters(context.monsters);
|
||||
case 'idle':
|
||||
return !context.inBattle;
|
||||
case 'recovery':
|
||||
return definition.state === 'battle'
|
||||
? context.inBattle
|
||||
: !context.inBattle;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isCampSceneContext(context: FunctionAvailabilityContext) {
|
||||
return (
|
||||
getWorldCampScenePreset(context.worldType)?.id === context.currentSceneId
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlayerStateMode(inBattle: boolean): PlayerStateMode {
|
||||
return inBattle ? 'battle' : 'idle';
|
||||
}
|
||||
|
||||
export function getAllStateFunctionDefinitions(
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
export function getFunctionsForState(
|
||||
state: PlayerStateMode,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return definitions.filter((item) => item.state === state);
|
||||
}
|
||||
|
||||
export function getFunctionById(
|
||||
functionId: string,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return definitions.find((item) => item.id === functionId) ?? null;
|
||||
}
|
||||
|
||||
export function getExecutableFunctions(
|
||||
context: FunctionAvailabilityContext,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
const state = getPlayerStateMode(context.inBattle);
|
||||
return getFunctionsForState(state, definitions)
|
||||
.filter((definition) => matchesCategory(definition, context))
|
||||
.filter(
|
||||
(definition) =>
|
||||
!(
|
||||
definition.id === 'idle_explore_forward' &&
|
||||
isCampSceneContext(context)
|
||||
),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const scoreDiff =
|
||||
getFunctionPriority(b, context) - getFunctionPriority(a, context);
|
||||
if (scoreDiff !== 0) return scoreDiff;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function isFunctionExecutable(
|
||||
functionId: string,
|
||||
context: FunctionAvailabilityContext,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return getExecutableFunctions(context, definitions).some(
|
||||
(item) => item.id === functionId,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildFunctionCatalogText(
|
||||
context: FunctionAvailabilityContext,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return getExecutableFunctions(context, definitions)
|
||||
.map(
|
||||
(item) =>
|
||||
`- ${item.id}:${getFunctionPromptDescription(item.id, item.description)}`,
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function resolveFunctionOption(
|
||||
functionId: string,
|
||||
context: FunctionAvailabilityContext,
|
||||
actionText?: string,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
): StoryOption | null {
|
||||
const definition = getFunctionById(functionId, definitions);
|
||||
if (!definition || !isFunctionExecutable(functionId, context, definitions))
|
||||
return null;
|
||||
|
||||
const primaryMonster =
|
||||
context.monsters.find((monster) => monster.hp > 0) ?? context.monsters[0];
|
||||
const monsterAction =
|
||||
definition.visual.monsterActionTemplate && primaryMonster
|
||||
? definition.visual.monsterActionTemplate.replaceAll(
|
||||
'{monster}',
|
||||
primaryMonster.name,
|
||||
)
|
||||
: '前方的气氛依旧紧绷';
|
||||
const suggestedActionText = buildSuggestedActionText(definition, context);
|
||||
const shouldForceSuggestedActionText =
|
||||
definition.id === 'idle_explore_forward' ||
|
||||
definition.id === 'idle_travel_next_scene';
|
||||
const resolvedActionText = shouldForceSuggestedActionText
|
||||
? suggestedActionText
|
||||
: actionText?.trim() || suggestedActionText;
|
||||
const detailText = buildOptionDetailText(definition, context);
|
||||
|
||||
return {
|
||||
functionId: definition.id,
|
||||
actionText: resolvedActionText,
|
||||
text: resolvedActionText,
|
||||
detailText,
|
||||
priority: getStoryOptionPriority(definition.id),
|
||||
visuals: {
|
||||
playerAnimation: definition.visual.playerAnimation,
|
||||
playerMoveMeters: definition.visual.playerMoveMeters,
|
||||
playerOffsetY: definition.visual.playerOffsetY,
|
||||
playerFacing: definition.visual.playerFacing,
|
||||
scrollWorld: definition.visual.scrollWorld,
|
||||
monsterChanges:
|
||||
primaryMonster && definition.visual.monsterAnimation
|
||||
? [
|
||||
{
|
||||
id: primaryMonster.id,
|
||||
action: monsterAction,
|
||||
animation: definition.visual.monsterAnimation,
|
||||
moveMeters: definition.visual.monsterMoveMeters ?? 0,
|
||||
yOffset: 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultFunctionIdsForContext(
|
||||
context: FunctionAvailabilityContext,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return getExecutableFunctions(context, definitions)
|
||||
.slice(0, 6)
|
||||
.map((item) => item.id);
|
||||
}
|
||||
|
||||
export function getFunctionEffect(
|
||||
functionId: string,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return getFunctionById(functionId, definitions)?.effect ?? {};
|
||||
}
|
||||
|
||||
export function getFunctionSkillWeights(
|
||||
functionId: string,
|
||||
definitions: StateFunctionDefinition[] = ALL_FUNCTIONS,
|
||||
) {
|
||||
return getFunctionById(functionId, definitions)?.effect.skillWeights ?? null;
|
||||
}
|
||||
|
||||
export function getStoryOptionPriority(functionId: string) {
|
||||
return FUNCTION_OPTION_PRIORITIES[functionId] ?? 1;
|
||||
}
|
||||
|
||||
export function sortStoryOptionsByPriority(options: StoryOption[]) {
|
||||
const normalizedOptions = options.map((option, index) => ({
|
||||
option: {
|
||||
...option,
|
||||
priority: getStoryOptionPriority(option.functionId),
|
||||
},
|
||||
index,
|
||||
}));
|
||||
|
||||
const lockedOptions = normalizedOptions
|
||||
.slice(0, MODEL_PRIORITY_LOCKED_OPTION_COUNT)
|
||||
.map((item) => item.option);
|
||||
const prioritizedOptions = normalizedOptions
|
||||
.slice(MODEL_PRIORITY_LOCKED_OPTION_COUNT)
|
||||
.sort((a, b) => {
|
||||
const priorityDiff = (b.option.priority ?? 1) - (a.option.priority ?? 1);
|
||||
if (priorityDiff !== 0) return priorityDiff;
|
||||
return a.index - b.index;
|
||||
})
|
||||
.map((item) => item.option);
|
||||
|
||||
return [...lockedOptions, ...prioritizedOptions];
|
||||
}
|
||||
Reference in New Issue
Block a user