Refine NPC interactions and runtime item generation

This commit is contained in:
2026-04-05 17:13:07 +08:00
parent c49c64896a
commit 89cecda7da
58 changed files with 4199 additions and 1562 deletions

View File

@@ -99,112 +99,113 @@ const WORLD_PRESENTATIONS: Record<ThemeMode, WorldPresentation> = {
},
machina: {
mode: 'machina',
attributeLabels: { strength: 'Power', agility: 'Dexterity', intelligence: 'Logic', spirit: 'Core' },
hpLabel: 'HP',
mpLabel: 'Energy',
maxHpLabel: 'Max HP',
maxMpLabel: 'Max Energy',
damageLabel: 'Firepower',
guardLabel: 'Shield',
rangeLabel: 'Range',
cooldownLabel: 'Recharge',
manaCostLabel: 'Energy Cost',
campSuffix: 'Mobile Outpost',
itemPrefixes: ['Iron', 'Steel', 'Pulse', 'Core', 'Nova', 'Plasma'],
itemInfixes: ['-Core','-Drive','-Link','-Grid','-Node','-Unit'],
skillPrefixes: ['Over','Ultra','Mega','Core','Pulse','Nova'],
attributeLabels: { strength: '动力', agility: '精度', intelligence: '逻辑', spirit: '核心' },
hpLabel: '耐久',
mpLabel: '能量',
maxHpLabel: '耐久上限',
maxMpLabel: '能量上限',
damageLabel: '火力',
guardLabel: '护盾',
rangeLabel: '射程',
cooldownLabel: '充能',
manaCostLabel: '能量消耗',
campSuffix: '机动前哨',
itemPrefixes: ['铁脊', '钢律', '脉冲', '核列', '新星', '等离'],
itemInfixes: ['', '', '', '', '', ''],
skillPrefixes: ['超载', '脉冲', '聚核', '磁轨', '新星', '裂火'],
skillSuffixByStyle: {
burst: ['-Burst','-Barrage','-Volley','-Salvo'],
steady: ['-Sustain','-Hold','-Guard','-Anchor'],
mobility: ['-Dash','-Boost','-Warp','-Blink'],
finisher: ['-Strike','-Cascade','-Finale','-Overload'],
projectile: ['-Round','-Bolt','-Shell','-Missile'],
burst: ['爆裂', '齐射', '连发', '倾泻'],
steady: ['稳压', '固守', '护持', '锚定'],
mobility: ['疾冲', '推进', '跃迁', '闪移'],
finisher: ['终断', '歼灭', '过载', '坠落'],
projectile: ['', '', '', ''],
},
},
tide: {
mode: 'tide',
attributeLabels: { strength: 'Strength', agility: 'Agility', intelligence: 'Intelligence', spirit: 'Spirit' },
hpLabel: 'HP',
mpLabel: 'MP',
maxHpLabel: 'Max HP',
maxMpLabel: 'Max MP',
damageLabel: 'Damage',
guardLabel: 'Guard',
rangeLabel: 'Range',
cooldownLabel: 'Cooldown',
manaCostLabel: 'Mana Cost',
campSuffix: 'Camp',
itemPrefixes: ['Wave', 'Tide', 'Ocean', 'Sea', 'Storm', 'Surf'],
itemInfixes: ['-Wave','-Tide','-Ocean','-Sea','-Storm','-Surf'],
skillPrefixes: ['Wave','Tide','Ocean','Sea','Storm','Surf'],
attributeLabels: { strength: '潮力', agility: '浪步', intelligence: '潮识', spirit: '潮魄' },
hpLabel: '潮命',
mpLabel: '潮息',
maxHpLabel: '潮命上限',
maxMpLabel: '潮息上限',
damageLabel: '潮势',
guardLabel: '潮护',
rangeLabel: '潮距',
cooldownLabel: '回潮',
manaCostLabel: '潮息消耗',
campSuffix: '潮栖营地',
itemPrefixes: ['潮纹', '海晕', '霜浪', '天澜', '潮歌', '沧流'],
itemInfixes: ['', '', '', '', '', ''],
skillPrefixes: ['', '', '', '', '', ''],
skillSuffixByStyle: {
burst: ['-Burst','-Barrage','-Volley','-Salvo'],
steady: ['-Sustain','-Hold','-Guard','-Anchor'],
mobility: ['-Dash','-Boost','-Warp','-Blink'],
finisher: ['-Strike','-Cascade','-Finale','-Overload'],
projectile: ['-Round','-Bolt','-Shell','-Missile'],
burst: ['裂潮', '怒涌', '连浪', '奔潮'],
steady: ['守潮', '潮护', '定澜', '镇流'],
mobility: ['踏浪', '游潮', '跃汐', '逐流'],
finisher: ['断潮', '覆海', '终汐', '沉落'],
projectile: ['潮矢', '水矛', '浪刃', '飞涌'],
},
},
rift: {
mode: 'rift',
attributeLabels: { strength: '界劲', agility: '裂步', intelligence: '界识', spirit: '界压' },
hpLabel: '界命',
mpLabel: '裂能',
maxHpLabel: '界命上限',
maxMpLabel: '裂能上限',
damageLabel: '界势',
guardLabel: '稳界',
rangeLabel: '界距',
cooldownLabel: '复界',
manaCostLabel: 'Rift Cost',
campSuffix: '裂界驻营',
itemPrefixes: ['裂界', '断层', '边潮', '灰域', '界桥', '前哨'],
itemInfixes: ['edge', 'void', 'span', 'seal', 'rift', 'core'],
skillPrefixes: ['rift', 'void', 'split', 'break', 'phase', 'warp'],
attributeLabels: { strength: '界劲', agility: '裂步', intelligence: '界识', spirit: '界压' },
hpLabel: '界命',
mpLabel: '裂能',
maxHpLabel: '界命上限',
maxMpLabel: '裂能上限',
damageLabel: '界势',
guardLabel: '稳界',
rangeLabel: '界距',
cooldownLabel: '复界',
manaCostLabel: '裂能消耗',
campSuffix: '裂界驻营',
itemPrefixes: ['裂界', '断层', '边潮', '灰域', '界桥', '前哨'],
itemInfixes: ['', '', '', '', '', ''],
skillPrefixes: ['', '', '', '', '', ''],
skillSuffixByStyle: {
burst: ['break', 'crash', 'shatter', 'burst'],
steady: ['guard', 'hold', 'veil', 'ward'],
mobility: ['step', 'shift', 'blink', 'drift'],
finisher: ['ending', 'drop', 'break', 'flare'],
projectile: ['spike', 'bolt', 'shard', 'wave'],
burst: ['崩断', '碎坠', '裂爆', '界崩'],
steady: ['守界', '固相', '帷障', '界卫'],
mobility: ['裂步', '转相', '闪迁', '漂移'],
finisher: ['终坠', '断灭', '裂终', '界燃'],
projectile: ['界刺', '裂矢', '碎片', '裂波'],
},
},
};
const CATEGORY_NOUNS: Record<string, string[]> = Object.fromEntries([
[CATEGORY_WEAPON, ['blade', 'axe', 'bow', 'staff', 'spear', 'shield']],
[CATEGORY_ARMOR, ['armor', 'robe', 'cloak', 'guard', 'mantle', 'bracer']],
[CATEGORY_RELIC, ['ring', 'seal', 'badge', 'gem', 'charm', 'orb']],
[CATEGORY_CONSUMABLE, ['potion', 'dust', 'draught', 'brew', 'oil', 'scroll']],
[CATEGORY_MATERIAL, ['ore', 'crystal', 'bone', 'herb', 'core', 'silk']],
[CATEGORY_RARE, ['sigil', 'relic', 'page', 'chart', 'key', 'idol']],
[CATEGORY_EXCLUSIVE, ['core', 'seal', 'master-key', 'origin-box', 'true-mark', 'world-core']],
[CATEGORY_WEAPON, ['', '', '', '', '', '']],
[CATEGORY_ARMOR, ['', '', '披风', '护具', '肩甲', '护腕']],
[CATEGORY_RELIC, ['', '', '', '', '', '']],
[CATEGORY_CONSUMABLE, ['', '', '', '', '', '']],
[CATEGORY_MATERIAL, ['', '', '', '', '', '']],
[CATEGORY_RARE, ['', '遗物', '残页', '', '', '']],
[CATEGORY_EXCLUSIVE, ['核心', '封印', '主钥', '源匣', '真印', '界核']],
]);
const DEFAULT_CATEGORY_NOUNS = ['relic', 'sigil', 'token', 'seal', 'core', 'mark'];
const DEFAULT_CATEGORY_NOUNS = ['', '', '信物', '', '', '铭片'];
const ROLE_SKILL_ROOTS: Record<string, string[]> = {
'sword-princess': ['王剑', '锋式', '裁锋', '裂锋'],
'archer-hero': ['弦诀', '远袭', '追风', '贯矢'],
'girl-hero': ['双刃', '影袭', '疾斩', '掠影'],
'punch-hero': ['拳势', '震击', '裂拳', '崩步'],
'fighter-4': ['重锋', '盾阵', '镇线', '压城'],
'sword-princess': ['王剑', '锋式', '裁锋', '裂锋'],
'archer-hero': ['弦诀', '远袭', '追风', '贯矢'],
'girl-hero': ['双刃', '影袭', '疾斩', '掠影'],
'punch-hero': ['拳势', '震击', '裂拳', '崩步'],
'fighter-4': ['重锋', '盾阵', '镇线', '压城'],
};
const SKILL_ROOT_STOP_WORDS = new Set([
'世界',
'设定',
'基调',
'目标',
'角色',
'战斗',
'风格',
'背景',
'性格',
'故事',
'世界',
'设定',
'基调',
'目标',
'角色',
'战斗',
'风格',
'背景',
'性格',
'故事',
'custom-world',
'playable-role',
]);
function hashText(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
@@ -286,7 +287,7 @@ function buildSkillRootOptions(
character: Character,
role?: Pick<CustomWorldPlayableNpc, 'title' | 'combatStyle' | 'tags'> | null,
) {
const fallbackRoots = ROLE_SKILL_ROOTS[character.id] ?? ['界式', '行诀', '裂锋', '潮印'];
const fallbackRoots = ROLE_SKILL_ROOTS[character.id] ?? ['界式', '行诀', '裂锋', '潮印'];
const derivedRoots = dedupeStrings([
...collectSkillRootFragments(role?.title ?? '', 4),
...collectSkillRootFragments(role?.combatStyle ?? '', 6),
@@ -310,9 +311,9 @@ export function getAttributeLabelsForWorld(worldType: WorldType | null | undefin
const profile = getCustomWorldProfileForDisplay(worldType, explicitProfile);
if (!profile) {
if (worldType === WorldType.XIANXIA) {
return { strength: '道躯', agility: '御行', intelligence: '神识', spirit: '灵蕴' };
return { strength: '道骨', agility: '身法', intelligence: '神识', spirit: '灵韵' };
}
return { strength: '力量', agility: '敏捷', intelligence: '智力', spirit: '精神' };
return { strength: '力量', agility: '敏捷', intelligence: '智力', spirit: '精神' };
}
return getWorldPresentation(profile).attributeLabels;
@@ -323,27 +324,27 @@ export function getResourceLabelsForWorld(worldType: WorldType | null | undefine
if (!profile) {
if (worldType === WorldType.XIANXIA) {
return {
hp: '命元',
mp: '灵蕴',
maxHp: '命元上限',
maxMp: '灵蕴上限',
damage: '术势',
guard: '护元',
range: '术距',
cooldown: '回息',
manaCost: '灵蕴消è€?',
hp: '命元',
mp: '灵韵',
maxHp: '命元上限',
maxMp: '灵韵上限',
damage: '术势',
guard: '护元',
range: '术距',
cooldown: '回息',
manaCost: '灵韵消耗',
};
}
return {
hp: 'HP',
mp: 'MP',
maxHp: '最å¤?HP',
maxMp: '最å¤?MP',
damage: 'Damage',
guard: 'Guard',
range: 'Range',
cooldown: 'Cooldown',
manaCost: 'Mana',
hp: '生命',
mp: '灵力',
maxHp: '生命上限',
maxMp: '灵力上限',
damage: '伤害',
guard: '防护',
range: '距离',
cooldown: '冷却',
manaCost: '消耗',
};
}
@@ -361,6 +362,7 @@ export function getResourceLabelsForWorld(worldType: WorldType | null | undefine
};
}
export function buildCustomCampSceneName(profile: CustomWorldProfile) {
const presentation = getWorldPresentation(profile);
return `${presentation.itemPrefixes[0]}${presentation.campSuffix}`;
@@ -383,7 +385,7 @@ export function buildThemedSkillName(
}
function getCategoryNouns(category: string) {
return CATEGORY_NOUNS[category] ?? CATEGORY_NOUNS['稀有品'];
return CATEGORY_NOUNS[category] ?? CATEGORY_NOUNS[CATEGORY_RARE];
}
function getResolvedCategoryNouns(category: string): string[] {
@@ -413,20 +415,20 @@ export function buildThemedItemDescription(
) {
const seed = hashText(`${profile.id}:${category}:${rarity}:${seedKey}`);
const hooks = [
`Suitable for the current goal "${profile.playerGoal}".`,
`Its tone closely matches the world tone "${profile.tone}".`,
'Likely to appear in one of this world\'s major conflicts.',
`It clearly ties into the expanding conflict inside this world.`,
`适合围绕“${profile.playerGoal}”继续推进。`,
`它的气质和“${profile.tone}”这条世界基调很贴近。`,
'很可能会出现在这个世界的关键冲突里。',
'能明显牵出这个世界正在扩大的主要矛盾。',
];
const rarityText = {
common: '常见',
uncommon: '进阶',
rare: 'Rare',
epic: '核心',
legendary: '关键',
common: '常见',
uncommon: '进阶',
rare: '稀有',
epic: '核心',
legendary: '关键',
}[rarity];
return `${rarityText} ${category}. ${hooks[seed % hooks.length]}`;
return `${rarityText}${category}${hooks[seed % hooks.length]}`;
}
export function inferCustomItemMechanics(
@@ -449,7 +451,7 @@ export function inferCustomItemMechanics(
legendary: 5,
}[rarity];
if (category === '武器') {
if (category === CATEGORY_WEAPON) {
return {
equipmentSlotId: 'weapon',
statProfile: {
@@ -459,7 +461,7 @@ export function inferCustomItemMechanics(
};
}
if (category === '护甲') {
if (category === CATEGORY_ARMOR) {
return {
equipmentSlotId: 'armor',
statProfile: {
@@ -470,7 +472,7 @@ export function inferCustomItemMechanics(
};
}
if (category === '??' || category === '???' || category === '????') {
if (category === CATEGORY_RELIC || category === CATEGORY_RARE || category === CATEGORY_EXCLUSIVE) {
return {
equipmentSlotId: 'relic',
statProfile: {
@@ -481,7 +483,7 @@ export function inferCustomItemMechanics(
};
}
if (category === '消耗品') {
if (category === CATEGORY_CONSUMABLE) {
const heals = tags.includes('healing') || seed % 2 === 0;
return {
useProfile: heals