This commit is contained in:
242
src/data/runtimeItemDirector.ts
Normal file
242
src/data/runtimeItemDirector.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user