Files
Genarrative/src/data/runtimeItemCompiler.ts
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

331 lines
8.5 KiB
TypeScript

import type {
InventoryItem,
ItemBuildProfile,
ItemRarity,
ItemStatProfile,
ItemUseProfile,
RuntimeItemAiIntent,
RuntimeItemCompileBudget,
RuntimeItemGenerationContext,
RuntimeItemPlan,
TimedBuildBuff,
} from '../types';
import { normalizeBuildRole, normalizeBuildTags } from './buildTags';
const RARITY_ORDER: ItemRarity[] = [
'common',
'uncommon',
'rare',
'epic',
'legendary',
];
function clampIndex(index: number) {
return Math.max(0, Math.min(RARITY_ORDER.length - 1, index));
}
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 buildStructuralTags(plan: RuntimeItemPlan) {
switch (plan.itemKind) {
case 'equipment':
return ['weapon'];
case 'relic':
return ['relic'];
case 'material':
return ['material'];
case 'consumable':
return ['consumable'];
case 'quest':
return ['relic'];
default:
return [];
}
}
function resolveEquipmentSlotId(plan: RuntimeItemPlan) {
if (plan.itemKind === 'equipment') {
if (
plan.targetBuildDirection.includes('守御') ||
plan.targetBuildDirection.includes('护体')
) {
return 'armor' as const;
}
return 'weapon' as const;
}
if (plan.itemKind === 'relic' || plan.itemKind === 'quest') {
return 'relic' as const;
}
return null;
}
function resolveCategory(plan: RuntimeItemPlan) {
switch (plan.itemKind) {
case 'equipment':
return resolveEquipmentSlotId(plan) === 'armor' ? '护甲' : '武器';
case 'consumable':
return '消耗品';
case 'material':
return '材料';
case 'quest':
return '专属物';
default:
return '稀有品';
}
}
function buildRuntimeBudget(
context: RuntimeItemGenerationContext,
plan: RuntimeItemPlan,
seedKey: string,
): RuntimeItemCompileBudget {
const seed = hashText(
`${context.generationChannel}:${seedKey}:${plan.slot}:${plan.itemKind}`,
);
const channelBaseIndex =
context.generationChannel === 'quest_reward'
? 2
: context.generationChannel === 'treasure'
? 1
: context.generationChannel === 'monster_drop'
? 0
: 1;
const slotBonus = plan.slot === 'primary' ? 1 : 0;
const permanenceBonus = plan.permanence === 'permanent' ? 1 : 0;
const rarityIndex = clampIndex(
channelBaseIndex + slotBonus + permanenceBonus + (seed % 2),
);
const rarity = RARITY_ORDER[rarityIndex] ?? 'common';
return {
rarity,
buildTagLimit:
rarity === 'legendary'
? 2
: rarity === 'epic' || rarity === 'rare'
? 2
: 1,
timedBuffTagLimit:
rarity === 'legendary'
? 3
: rarity === 'epic' || rarity === 'rare'
? 2
: 1,
timedBuffDuration: rarity === 'legendary' ? 3 : rarity === 'epic' ? 3 : 2,
statBudgetTier: rarityIndex + 1,
};
}
function buildTimedBuffs(
itemId: string,
intent: RuntimeItemAiIntent,
budget: RuntimeItemCompileBudget,
): TimedBuildBuff[] {
const tags = normalizeBuildTags(
intent.desiredBuildTags,
budget.timedBuffTagLimit,
);
if (tags.length <= 0) return [];
return [
{
id: `${itemId}:buff`,
sourceType: 'item',
sourceId: itemId,
name: `${tags[0]}增益`,
tags,
durationTurns: budget.timedBuffDuration,
},
];
}
function buildUseProfile(
plan: RuntimeItemPlan,
intent: RuntimeItemAiIntent,
budget: RuntimeItemCompileBudget,
itemId: string,
): ItemUseProfile | null {
if (plan.itemKind !== 'consumable' && plan.permanence === 'permanent')
return null;
const useProfile: ItemUseProfile = {};
if (intent.desiredFunctionalBias.includes('heal')) {
useProfile.hpRestore = 16 + budget.statBudgetTier * 10;
}
if (intent.desiredFunctionalBias.includes('mana')) {
useProfile.manaRestore = 14 + budget.statBudgetTier * 8;
}
if (intent.desiredFunctionalBias.includes('cooldown')) {
useProfile.cooldownReduction =
budget.rarity === 'legendary' || budget.rarity === 'epic' ? 1 : 0;
}
if (plan.permanence === 'timed' || plan.itemKind === 'consumable') {
useProfile.buildBuffs = buildTimedBuffs(itemId, intent, budget);
}
if (
(useProfile.hpRestore ?? 0) <= 0 &&
(useProfile.manaRestore ?? 0) <= 0 &&
(useProfile.cooldownReduction ?? 0) <= 0 &&
(useProfile.buildBuffs?.length ?? 0) <= 0
) {
return null;
}
return useProfile;
}
function buildStatProfile(
plan: RuntimeItemPlan,
intent: RuntimeItemAiIntent,
budget: RuntimeItemCompileBudget,
): ItemStatProfile | null {
if (plan.itemKind === 'material') return null;
const statProfile: ItemStatProfile = {};
const tier = budget.statBudgetTier;
if (
plan.itemKind === 'equipment' ||
plan.itemKind === 'relic' ||
plan.itemKind === 'quest'
) {
if (
intent.desiredFunctionalBias.includes('guard') ||
plan.targetBuildDirection.includes('守御')
) {
statProfile.maxHpBonus =
6 * tier + (plan.itemKind === 'equipment' ? 10 : 4);
statProfile.incomingDamageMultiplier = Number(
Math.max(0.84, 1 - tier * 0.03).toFixed(2),
);
}
if (
intent.desiredFunctionalBias.includes('mana') ||
plan.targetBuildDirection.includes('法力')
) {
statProfile.maxManaBonus =
8 * tier + (plan.itemKind === 'relic' ? 10 : 0);
}
if (
intent.desiredFunctionalBias.includes('damage') ||
plan.targetBuildDirection.length > 0
) {
statProfile.outgoingDamageBonus = Number((0.03 * tier).toFixed(2));
}
}
if (Object.keys(statProfile).length <= 0) return null;
return statProfile;
}
function buildProfile(
plan: RuntimeItemPlan,
intent: RuntimeItemAiIntent,
budget: RuntimeItemCompileBudget,
): ItemBuildProfile | null {
if (
plan.permanence !== 'permanent' ||
(plan.itemKind !== 'equipment' &&
plan.itemKind !== 'relic' &&
plan.itemKind !== 'quest')
) {
return null;
}
const tags = normalizeBuildTags(
intent.desiredBuildTags,
budget.buildTagLimit,
);
if (tags.length <= 0) return null;
return {
role: normalizeBuildRole(tags[0]),
tags,
synergy: normalizeBuildTags(
[...plan.targetBuildDirection, ...intent.desiredBuildTags],
3,
),
craftTags: tags,
forgeRank: 0,
};
}
function buildValue(budget: RuntimeItemCompileBudget, plan: RuntimeItemPlan) {
const baseValue = {
common: 18,
uncommon: 32,
rare: 52,
epic: 80,
legendary: 118,
}[budget.rarity];
if (plan.itemKind === 'material') return baseValue - 8;
if (plan.itemKind === 'consumable') return baseValue - 4;
if (plan.itemKind === 'quest') return baseValue + 12;
return baseValue;
}
function buildItemTags(
plan: RuntimeItemPlan,
intent: RuntimeItemAiIntent,
budget: RuntimeItemCompileBudget,
) {
const tags = [
...buildStructuralTags(plan),
...normalizeBuildTags(intent.desiredBuildTags, budget.buildTagLimit + 1),
];
if (intent.desiredFunctionalBias.includes('heal')) tags.push('healing');
if (intent.desiredFunctionalBias.includes('mana')) tags.push('mana');
if (resolveEquipmentSlotId(plan) === 'armor') tags.push('armor');
if (resolveEquipmentSlotId(plan) === 'weapon') tags.push('weapon');
if (resolveEquipmentSlotId(plan) === 'relic') tags.push('relic');
return [...new Set(tags.filter(Boolean))];
}
export function compileRuntimeItem(params: {
seedKey: string;
context: RuntimeItemGenerationContext;
plan: RuntimeItemPlan;
intent: RuntimeItemAiIntent;
}) {
const { seedKey, context, plan, intent } = params;
const budget = buildRuntimeBudget(context, plan, seedKey);
const itemId = `runtime:${context.generationChannel}:${hashText(seedKey).toString(36)}`;
const runtimeMetadata = {
origin: 'ai_compiled' as const,
generationChannel: context.generationChannel,
seedKey,
relationAnchor: plan.relationAnchor,
sourceReason: intent.reasonToAppear,
recentEventHook: context.recentActions[0],
};
return {
id: itemId,
category: resolveCategory(plan),
name: intent.shortNameSeed || '未命名秘物',
quantity: 1,
rarity: budget.rarity,
tags: buildItemTags(plan, intent, budget),
equipmentSlotId: resolveEquipmentSlotId(plan),
statProfile: buildStatProfile(plan, intent, budget),
useProfile: buildUseProfile(plan, intent, budget, itemId),
buildProfile: buildProfile(plan, intent, budget),
value: buildValue(budget, plan),
runtimeMetadata,
} satisfies InventoryItem;
}