071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
291 lines
7.6 KiB
TypeScript
291 lines
7.6 KiB
TypeScript
import { generateRuntimeItemAiIntents } from '../services/runtimeItemAiDirector';
|
|
import type {
|
|
DirectedRuntimeReward,
|
|
InventoryItem,
|
|
RuntimeItemGenerationChannel,
|
|
RuntimeItemGenerationContext,
|
|
RuntimeItemKind,
|
|
RuntimeItemPermanence,
|
|
RuntimeItemPlan,
|
|
RuntimeRelationAnchor,
|
|
} from '../types';
|
|
import { compileRuntimeItem } from './runtimeItemCompiler';
|
|
import {
|
|
applyRuntimeItemNarrative,
|
|
buildRuntimeItemAiIntent,
|
|
buildRuntimeRewardStoryHint,
|
|
flattenDirectedRuntimeRewardItems,
|
|
} from './runtimeItemNarrative';
|
|
|
|
type RuntimeRewardOptions = {
|
|
seedKey: string;
|
|
variant?: string;
|
|
itemCount?: number;
|
|
fixedKinds?: RuntimeItemKind[];
|
|
fixedPermanence?: RuntimeItemPermanence[];
|
|
baseHp?: number;
|
|
baseMana?: number;
|
|
baseCurrency?: number;
|
|
storyHint?: string;
|
|
};
|
|
|
|
const GAP_TO_TAGS: Record<string, string[]> = {
|
|
survival_gap: ['守御', '护体', '回复', '续战'],
|
|
mana_gap: ['法力', '冷却', '过载'],
|
|
finisher_gap: ['爆发', '重击', '追击'],
|
|
mobility_gap: ['突进', '快袭', '风行', '游击'],
|
|
control_gap: ['控场', '符阵', '镇邪'],
|
|
};
|
|
|
|
function hashText(value: string) {
|
|
let hash = 0;
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
hash = (hash << 5) - hash + value.charCodeAt(index);
|
|
hash |= 0;
|
|
}
|
|
return Math.abs(hash);
|
|
}
|
|
|
|
function resolveGapDrivenTags(context: RuntimeItemGenerationContext) {
|
|
const gapTags = context.playerBuildGaps.flatMap(
|
|
(gap) => GAP_TO_TAGS[gap] ?? [],
|
|
);
|
|
return [...new Set([...gapTags, ...context.playerBuildTags])].slice(0, 3);
|
|
}
|
|
|
|
function resolveRelationAnchor(
|
|
context: RuntimeItemGenerationContext,
|
|
): RuntimeRelationAnchor {
|
|
if (context.encounter?.monsterPresetId) {
|
|
return {
|
|
type: 'monster',
|
|
monsterId: context.encounter.monsterPresetId,
|
|
monsterName: context.encounter.npcName,
|
|
};
|
|
}
|
|
|
|
if (context.encounterNpcName) {
|
|
return {
|
|
type: 'npc',
|
|
npcId: context.encounterNpcId ?? undefined,
|
|
npcName: context.encounterNpcName,
|
|
roleText: context.encounterContextText ?? undefined,
|
|
};
|
|
}
|
|
|
|
if (context.sceneName) {
|
|
return {
|
|
type: 'scene',
|
|
sceneId: context.sceneId ?? undefined,
|
|
sceneName: context.sceneName,
|
|
};
|
|
}
|
|
|
|
return {
|
|
type: 'landmark',
|
|
landmarkName: context.customWorldProfile?.name ?? '无名之地',
|
|
};
|
|
}
|
|
|
|
function getChannelTemplate(channel: RuntimeItemGenerationChannel) {
|
|
switch (channel) {
|
|
case 'treasure':
|
|
return {
|
|
kinds: ['relic', 'consumable', 'material'] as RuntimeItemKind[],
|
|
permanence: [
|
|
'permanent',
|
|
'timed',
|
|
'resource',
|
|
] as RuntimeItemPermanence[],
|
|
defaultCount: 2,
|
|
};
|
|
case 'npc_trade':
|
|
return {
|
|
kinds: [
|
|
'consumable',
|
|
'material',
|
|
'relic',
|
|
'equipment',
|
|
] as RuntimeItemKind[],
|
|
permanence: [
|
|
'timed',
|
|
'resource',
|
|
'permanent',
|
|
'permanent',
|
|
] as RuntimeItemPermanence[],
|
|
defaultCount: 4,
|
|
};
|
|
case 'npc_reward':
|
|
return {
|
|
kinds: ['consumable', 'relic', 'equipment'] as RuntimeItemKind[],
|
|
permanence: [
|
|
'timed',
|
|
'permanent',
|
|
'permanent',
|
|
] as RuntimeItemPermanence[],
|
|
defaultCount: 1,
|
|
};
|
|
case 'monster_drop':
|
|
return {
|
|
kinds: ['material', 'consumable', 'relic'] as RuntimeItemKind[],
|
|
permanence: [
|
|
'resource',
|
|
'timed',
|
|
'permanent',
|
|
] as RuntimeItemPermanence[],
|
|
defaultCount: 2,
|
|
};
|
|
case 'quest_reward':
|
|
return {
|
|
kinds: ['equipment', 'relic', 'consumable'] as RuntimeItemKind[],
|
|
permanence: [
|
|
'permanent',
|
|
'permanent',
|
|
'timed',
|
|
] as RuntimeItemPermanence[],
|
|
defaultCount: 2,
|
|
};
|
|
default:
|
|
return {
|
|
kinds: ['consumable', 'material', 'relic'] as RuntimeItemKind[],
|
|
permanence: [
|
|
'timed',
|
|
'resource',
|
|
'permanent',
|
|
] as RuntimeItemPermanence[],
|
|
defaultCount: 2,
|
|
};
|
|
}
|
|
}
|
|
|
|
function planRuntimeItems(
|
|
context: RuntimeItemGenerationContext,
|
|
options: RuntimeRewardOptions,
|
|
): RuntimeItemPlan[] {
|
|
const template = getChannelTemplate(context.generationChannel);
|
|
const count = Math.max(1, options.itemCount ?? template.defaultCount);
|
|
const relationAnchor = resolveRelationAnchor(context);
|
|
const targetBuildDirection = resolveGapDrivenTags(context);
|
|
const seed = hashText(
|
|
`${options.seedKey}:${options.variant ?? 'default'}:${context.generationChannel}`,
|
|
);
|
|
|
|
return Array.from({ length: count }, (_, index) => {
|
|
const kinds = options.fixedKinds?.length
|
|
? options.fixedKinds
|
|
: template.kinds;
|
|
const permanences = options.fixedPermanence?.length
|
|
? options.fixedPermanence
|
|
: template.permanence;
|
|
const itemKind = kinds[(seed + index) % kinds.length] ?? 'consumable';
|
|
const permanence =
|
|
permanences[(seed + index) % permanences.length] ?? 'timed';
|
|
|
|
return {
|
|
slot: index === 0 ? 'primary' : index === 1 ? 'secondary' : 'support',
|
|
itemKind,
|
|
permanence,
|
|
narrativeWeight: index === 0 ? 'heavy' : 'medium',
|
|
targetBuildDirection:
|
|
targetBuildDirection.length > 0 ? targetBuildDirection : ['均衡'],
|
|
relationAnchor,
|
|
} satisfies RuntimeItemPlan;
|
|
});
|
|
}
|
|
|
|
function compilePlannedItem(
|
|
context: RuntimeItemGenerationContext,
|
|
plan: RuntimeItemPlan,
|
|
seedKey: string,
|
|
intentOverride?: ReturnType<typeof buildRuntimeItemAiIntent>,
|
|
) {
|
|
const intent = intentOverride ?? buildRuntimeItemAiIntent(context, plan);
|
|
const compiled = compileRuntimeItem({
|
|
seedKey,
|
|
context,
|
|
plan,
|
|
intent,
|
|
});
|
|
|
|
return applyRuntimeItemNarrative({
|
|
item: compiled,
|
|
context,
|
|
plan,
|
|
intent,
|
|
});
|
|
}
|
|
|
|
function buildDirectedRewardFromItems(
|
|
compiledItems: InventoryItem[],
|
|
options: RuntimeRewardOptions,
|
|
): DirectedRuntimeReward {
|
|
const reward: DirectedRuntimeReward = {
|
|
primaryItem: compiledItems[0] ?? null,
|
|
supportItems: compiledItems.slice(1),
|
|
hp: options.baseHp,
|
|
mana: options.baseMana,
|
|
currency: options.baseCurrency,
|
|
storyHint: options.storyHint,
|
|
};
|
|
|
|
return {
|
|
...reward,
|
|
storyHint: buildRuntimeRewardStoryHint(reward),
|
|
};
|
|
}
|
|
|
|
export function buildDirectedRuntimeReward(
|
|
context: RuntimeItemGenerationContext,
|
|
options: RuntimeRewardOptions,
|
|
): DirectedRuntimeReward {
|
|
const plans = planRuntimeItems(context, options);
|
|
const compiledItems = plans.map((plan, index) =>
|
|
compilePlannedItem(
|
|
context,
|
|
plan,
|
|
`${options.seedKey}:${plan.slot}:${index}`,
|
|
),
|
|
);
|
|
|
|
return buildDirectedRewardFromItems(compiledItems, options);
|
|
}
|
|
|
|
export async function generateDirectedRuntimeReward(
|
|
context: RuntimeItemGenerationContext,
|
|
options: RuntimeRewardOptions,
|
|
): Promise<DirectedRuntimeReward> {
|
|
const plans = planRuntimeItems(context, options);
|
|
|
|
try {
|
|
const aiIntents = await generateRuntimeItemAiIntents({
|
|
context,
|
|
plans,
|
|
});
|
|
const compiledItems = plans.map((plan, index) =>
|
|
compilePlannedItem(
|
|
context,
|
|
plan,
|
|
`${options.seedKey}:${plan.slot}:${index}`,
|
|
aiIntents[index],
|
|
),
|
|
);
|
|
|
|
return buildDirectedRewardFromItems(compiledItems, options);
|
|
} catch (error) {
|
|
console.warn(
|
|
'[RuntimeItemDirector] falling back to deterministic item intent',
|
|
error,
|
|
);
|
|
return buildDirectedRuntimeReward(context, options);
|
|
}
|
|
}
|
|
|
|
export function buildRuntimeInventoryStock(
|
|
context: RuntimeItemGenerationContext,
|
|
options: RuntimeRewardOptions,
|
|
): InventoryItem[] {
|
|
return flattenDirectedRuntimeRewardItems(
|
|
buildDirectedRuntimeReward(context, options),
|
|
);
|
|
}
|