Files
Genarrative/src/data/itemDesign.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

963 lines
33 KiB
TypeScript

import {
type EquipmentSlotId,
type ItemBuildProfile,
type ItemCatalogEntry,
type ItemRarity,
type ItemStatProfile,
type ItemUseProfile,
type ItemWorldAffinity,
type ItemWorldProfile,
WorldType,
} from "../types";
import { normalizeBuildRole, normalizeBuildTags } from "./buildTags";
export type ItemCategoryLabels = {
weapon: string;
armor: string;
relic: string;
consumable: string;
material: string;
rare: string;
exclusive: string;
};
type DesignedItemMetadata = Pick<
ItemCatalogEntry,
| "name"
| "category"
| "rarity"
| "tags"
| "description"
| "worldAffinity"
| "equipmentSlotId"
| "worldProfiles"
| "statProfile"
| "useProfile"
| "buildProfile"
| "value"
>;
type MaterialTheme = {
wuxia: string;
xianxia: string;
worldAffinity: ItemWorldAffinity;
role: string;
rarity: ItemRarity;
setWuxia: string;
setXianxia: string;
tags: string[];
synergy: string[];
};
const MATERIAL_THEMES: Record<string, MaterialTheme> = {
Wooden: {
wuxia: "乌木",
xianxia: "灵木",
worldAffinity: "neutral",
role: "fieldcraft",
rarity: "common",
setWuxia: "山行木作",
setXianxia: "灵木行旅",
tags: ["探索", "制作"],
synergy: ["探索", "采集", "过渡装备"],
},
Copper: {
wuxia: "赤铜",
xianxia: "赤炼铜",
worldAffinity: "wuxia",
role: "breaker",
rarity: "common",
setWuxia: "赤铜开山",
setXianxia: "赤炼破锋",
tags: ["破甲", "爆发"],
synergy: ["破甲", "前期开荒", "刚猛流"],
},
Iron: {
wuxia: "寒铁",
xianxia: "玄铁",
worldAffinity: "wuxia",
role: "vanguard",
rarity: "uncommon",
setWuxia: "寒铁镇岳",
setXianxia: "玄铁镇山",
tags: ["先锋", "守卫"],
synergy: ["承伤", "反击", "稳扎稳打"],
},
Steel: {
wuxia: "百炼钢",
xianxia: "灵钢",
worldAffinity: "neutral",
role: "duelist",
rarity: "rare",
setWuxia: "百炼争锋",
setXianxia: "灵钢斗枢",
tags: ["决斗者", "节奏"],
synergy: ["连击", "对拼", "压制"],
},
Silver: {
wuxia: "霜银",
xianxia: "月银",
worldAffinity: "xianxia",
role: "ward",
rarity: "rare",
setWuxia: "霜银辟祟",
setXianxia: "月银镇邪",
tags: ["守卫", "灵体"],
synergy: ["克制邪祟", "回复", "护体"],
},
Gold: {
wuxia: "鎏金",
xianxia: "金霞",
worldAffinity: "neutral",
role: "fortune",
rarity: "epic",
setWuxia: "鎏金富贵",
setXianxia: "金霞天赐",
tags: ["财富", "支援"],
synergy: ["经济", "爆发", "贵重馈赠"],
},
Cobalt: {
wuxia: "苍钴",
xianxia: "苍穹钴晶",
worldAffinity: "xianxia",
role: "caster",
rarity: "epic",
setWuxia: "苍钴引雷",
setXianxia: "钴晶御雷",
tags: ["施法者", "法力"],
synergy: ["法力", "远程", "雷系构筑"],
},
Crimson: {
wuxia: "绯钢",
xianxia: "赤煞晶钢",
worldAffinity: "wuxia",
role: "berserker",
rarity: "rare",
setWuxia: "绯钢狂锋",
setXianxia: "赤煞断岳",
tags: ["狂战士", "爆发"],
synergy: ["压血爆发", "破阵", "重击"],
},
Altair: {
wuxia: "星游",
xianxia: "天狼星辉",
worldAffinity: "xianxia",
role: "assassin",
rarity: "epic",
setWuxia: "星游夜行",
setXianxia: "星辉掠影",
tags: ["刺客", "机动性"],
synergy: ["身法", "暴击", "切后"],
},
Adamantine: {
wuxia: "玄钢",
xianxia: "玄金陨铁",
worldAffinity: "neutral",
role: "fortress",
rarity: "legendary",
setWuxia: "玄钢不坏",
setXianxia: "陨铁镇界",
tags: ["堡垒", "坦克"],
synergy: ["高承伤", "套装成型", "守中反打"],
},
Angelic: {
wuxia: "天辉",
xianxia: "羽化天灵",
worldAffinity: "xianxia",
role: "paladin",
rarity: "legendary",
setWuxia: "天辉护心",
setXianxia: "羽化圣辉",
tags: ["圣骑士", "支援"],
synergy: ["护盾", "回复", "圣光构筑"],
},
Nova: {
wuxia: "星火",
xianxia: "星爆灵核",
worldAffinity: "xianxia",
role: "spellblade",
rarity: "epic",
setWuxia: "星火裂空",
setXianxia: "星爆御剑",
tags: ["法术之刃", "法力"],
synergy: ["法武双修", "中距离压制", "星辰构筑"],
},
Platinum: {
wuxia: "白金",
xianxia: "霜白灵金",
worldAffinity: "neutral",
role: "commander",
rarity: "epic",
setWuxia: "白金威仪",
setXianxia: "灵金统御",
tags: ["指挥官", "平衡"],
synergy: ["全能", "队伍增益", "中后期构筑"],
},
Fateful: {
wuxia: "命纹",
xianxia: "天命玄纹",
worldAffinity: "xianxia",
role: "fate",
rarity: "legendary",
setWuxia: "命纹转祸",
setXianxia: "天命轮转",
tags: ["命运", "实用"],
synergy: ["冷却", "机缘", "运势构筑"],
},
};
const ARMOR_PIECE_LABELS: Record<
string,
{ wuxia: string; xianxia: string; pieceName: string; slot: EquipmentSlotId }
> = {
Boots: { wuxia: "踏云靴", xianxia: "凌霄履", pieceName: "boots", slot: "armor" },
Chestplate: { wuxia: "护心甲", xianxia: "灵铠", pieceName: "chest", slot: "armor" },
Gloves: { wuxia: "护腕", xianxia: "灵纹手甲", pieceName: "gloves", slot: "armor" },
Helmet: { wuxia: "冠盔", xianxia: "灵盔", pieceName: "helm", slot: "armor" },
Leggings: { wuxia: "行岳腿甲", xianxia: "踏虚护胫", pieceName: "leggings", slot: "armor" },
Shield: { wuxia: "镇势盾", xianxia: "护界灵盾", pieceName: "shield", slot: "armor" },
Weapon: { wuxia: "战兵", xianxia: "灵兵", pieceName: "weapon", slot: "weapon" },
};
const RARITY_ORDER: ItemRarity[] = ["common", "uncommon", "rare", "epic", "legendary"];
const GENERIC_TOKEN_LABELS: Record<string, { wuxia: string; xianxia: string }> = {
scroll: { wuxia: "秘卷", xianxia: "玉简" },
ring: { wuxia: "戒", xianxia: "灵戒" },
torch: { wuxia: "火把", xianxia: "明焰灯" },
helm: { wuxia: "盔", xianxia: "灵盔" },
helmet: { wuxia: "盔", xianxia: "灵盔" },
chest: { wuxia: "胸甲", xianxia: "灵铠" },
pants: { wuxia: "护腿", xianxia: "灵裤" },
boots: { wuxia: "靴", xianxia: "云履" },
gem: { wuxia: "宝石", xianxia: "灵晶" },
crystal: { wuxia: "晶簇", xianxia: "灵晶" },
cross: { wuxia: "镇煞十字", xianxia: "镇灵法印" },
potion: { wuxia: "药剂", xianxia: "灵液" },
water: { wuxia: "清水", xianxia: "灵泉水" },
bottle: { wuxia: "药瓶", xianxia: "灵瓶" },
neck: { wuxia: "项坠", xianxia: "灵坠" },
mushroom: { wuxia: "山菌", xianxia: "灵菌" },
meat: { wuxia: "肉脯", xianxia: "灵兽肉" },
apple: { wuxia: "果实", xianxia: "灵果" },
skull: { wuxia: "颅骨", xianxia: "骨印" },
bag: { wuxia: "行囊", xianxia: "乾坤袋" },
mace: { wuxia: "钉头锤", xianxia: "镇岳杵" },
spade: { wuxia: "铲", xianxia: "灵铲" },
coin: { wuxia: "铜钱", xianxia: "灵钱" },
stone: { wuxia: "石料", xianxia: "灵石料" },
wood: { wuxia: "木料", xianxia: "灵木材" },
glowes: { wuxia: "护手", xianxia: "灵纹手套" },
gloves: { wuxia: "护手", xianxia: "灵纹手套" },
book: { wuxia: "册页", xianxia: "道卷" },
leaf: { wuxia: "叶片", xianxia: "灵叶" },
sword: { wuxia: "剑", xianxia: "灵剑" },
bow: { wuxia: "弓", xianxia: "灵弓" },
arrow: { wuxia: "箭", xianxia: "灵矢" },
shield: { wuxia: "盾", xianxia: "灵盾" },
rope: { wuxia: "绳索", xianxia: "缚灵索" },
skin: { wuxia: "兽皮", xianxia: "妖皮" },
treasure: { wuxia: "宝匣", xianxia: "秘藏灵匣" },
pick: { wuxia: "鹤嘴镐", xianxia: "开脉灵镐" },
silverbar: { wuxia: "银锭", xianxia: "月银锭" },
flower: { wuxia: "花", xianxia: "灵花" },
wand: { wuxia: "法杖", xianxia: "灵杖" },
magic: { wuxia: "秘术核心", xianxia: "灵法结晶" },
};
function clampRarity(rank: number): ItemRarity {
return RARITY_ORDER[Math.max(0, Math.min(RARITY_ORDER.length - 1, rank))] ?? "common";
}
function rarityToRank(rarity: ItemRarity) {
return RARITY_ORDER.indexOf(rarity);
}
function bumpRarity(rarity: ItemRarity, delta: number) {
return clampRarity(rarityToRank(rarity) + delta);
}
function parseVariantIndex(normalizedSourcePath: string) {
const match = normalizedSourcePath.match(/(\d+)(?=\.png$)/iu);
return match ? Number(match[1]) : 1;
}
function buildWorldProfiles(
wuxiaName: string,
xianxiaName: string,
wuxiaDescription: string,
xianxiaDescription: string,
): Partial<Record<WorldType, ItemWorldProfile>> {
return {
WUXIA: {
name: wuxiaName,
description: wuxiaDescription,
},
XIANXIA: {
name: xianxiaName,
description: xianxiaDescription,
},
};
}
function dedupe(values: string[]) {
return [...new Set(values.filter(Boolean))];
}
function buildEquipmentStats(
slot: EquipmentSlotId,
rarity: ItemRarity,
role: string,
pieceName: string,
): ItemStatProfile {
const rank = rarityToRank(rarity) + 1;
if (slot === "weapon") {
const outgoingDamageBonus = Number((0.04 + rank * 0.018).toFixed(2));
const maxManaBonus = role === "caster" || role === "spellblade" ? 8 + rank * 4 : 0;
return {
outgoingDamageBonus,
maxManaBonus,
maxHpBonus: role === "fortress" ? 8 + rank * 6 : 0,
};
}
const baseHp =
pieceName === "shield"
? 16 + rank * 10
: pieceName === "chest"
? 14 + rank * 9
: pieceName === "helm"
? 10 + rank * 6
: 8 + rank * 5;
const incomingDamageMultiplier = Number(
Math.max(0.72, 0.99 - rank * 0.03 - (role === "fortress" ? 0.04 : 0)).toFixed(2),
);
return {
maxHpBonus: baseHp,
maxManaBonus: role === "caster" || role === "paladin" ? 6 + rank * 4 : 0,
outgoingDamageBonus:
role === "duelist" || role === "berserker" || role === "assassin"
? Number((0.02 + rank * 0.01).toFixed(2))
: 0,
incomingDamageMultiplier,
};
}
function buildRelicStats(rarity: ItemRarity, role: string): ItemStatProfile {
const rank = rarityToRank(rarity) + 1;
return {
maxHpBonus: role === "ward" || role === "paladin" ? 8 + rank * 4 : 0,
maxManaBonus:
role === "caster" || role === "fate" || role === "support"
? 12 + rank * 6
: 6 + rank * 3,
outgoingDamageBonus:
role === "assassin" || role === "berserker" || role === "spellblade"
? Number((0.03 + rank * 0.012).toFixed(2))
: Number((0.01 + rank * 0.008).toFixed(2)),
incomingDamageMultiplier:
role === "ward" || role === "support"
? Number(Math.max(0.8, 0.98 - rank * 0.02).toFixed(2))
: undefined,
};
}
function buildBuildProfile(
role: string,
tags: string[],
options: {
setId?: string;
setName?: string;
pieceName?: string;
synergy?: string[];
} = {},
): ItemBuildProfile {
return {
role: normalizeBuildRole(role),
tags: normalizeBuildTags([role, ...tags]),
setId: options.setId,
setName: options.setName,
pieceName: options.pieceName,
synergy: options.synergy ?? [],
forgeRank: 0,
};
}
function buildItemBuildBuffs(sourceId: string, name: string, tags: string[], durationTurns: number) {
return [{
id: `${sourceId}-buff`,
sourceType: "item" as const,
sourceId,
name,
tags: normalizeBuildTags(tags),
durationTurns,
}];
}
function rankValue(rarity: ItemRarity, slot: EquipmentSlotId | null, useProfile: ItemUseProfile | null) {
const rank = rarityToRank(rarity) + 1;
let value = 18 + rank * 14;
if (slot === "weapon") value += 22;
if (slot === "armor") value += 18;
if (slot === "relic") value += 20;
if (useProfile?.hpRestore || useProfile?.manaRestore) value += 16;
if (useProfile?.cooldownReduction) value += 22;
return value;
}
function detectRoleFromDescriptor(descriptor: string) {
const source = descriptor.toLowerCase();
if (/(wind|gust|nimble|rogue|hawk|arrow|sword_long|spear)/u.test(source)) return "assassin";
if (/(thunder|fierce|mighty|flame|serrated|punch|booster_iron|booster_steel)/u.test(source)) return "berserker";
if (/(arcane|esoteric|mage|ethereal|lunar|time|nightvision|copy|grimoire)/u.test(source)) return "caster";
if (/(fortitude|fortress|protector|shield|hearty|adaptable|honorable|cross)/u.test(source)) return "ward";
if (/(wedding|lovely|bud|oceanic|star|marbled|rich|vibrant|vivacious)/u.test(source)) return "support";
return "balanced";
}
function buildGenericTokenName(token: string, worldType: WorldType) {
const normalized = token.toLowerCase();
const mapped = GENERIC_TOKEN_LABELS[normalized];
if (mapped) {
return worldType === WorldType.WUXIA ? mapped.wuxia : mapped.xianxia;
}
return token
.replace(/\.[^.]+$/u, "")
.replace(/_/g, " ")
.trim();
}
function buildLegacyDesign(
normalizedSourcePath: string,
name: string,
category: string,
rarity: ItemRarity,
tags: string[],
labels: ItemCategoryLabels,
): DesignedItemMetadata | null {
if (normalizedSourcePath.includes("/")) return null;
const baseToken = normalizedSourcePath
.replace(/^\d+[_-]*/u, "")
.replace(/\.png$/iu, "")
.split(/[_-]/u)
.filter(Boolean)[0] ?? name;
const wuxiaName = buildGenericTokenName(baseToken, WorldType.WUXIA);
const xianxiaName = buildGenericTokenName(baseToken, WorldType.XIANXIA);
const slot: EquipmentSlotId | null =
category === labels.weapon ? "weapon" : category === labels.armor ? "armor" : category === labels.relic ? "relic" : null;
const useProfile: ItemUseProfile | null =
category === labels.consumable || tags.includes("healing") || tags.includes("mana")
? {
hpRestore: tags.includes("healing") ? 22 : 0,
manaRestore: tags.includes("mana") ? 18 : 0,
cooldownReduction: /power|mana|magic|bandage|torch/u.test(normalizedSourcePath) ? 1 : 0,
}
: null;
const statProfile =
slot === "weapon"
? buildEquipmentStats("weapon", rarity, "balanced", "weapon")
: slot === "armor"
? buildEquipmentStats("armor", rarity, "balanced", "armor")
: slot === "relic"
? buildRelicStats(rarity, "support")
: null;
return {
name,
category,
rarity,
tags: dedupe(tags),
description: `${wuxiaName} / ${xianxiaName} 这件图标物资可在两套兼容模板中以不同风格登场,适合作为${category}基础模板继续扩展。`,
worldAffinity: "neutral",
equipmentSlotId: slot,
worldProfiles: buildWorldProfiles(
wuxiaName,
xianxiaName,
`${wuxiaName},适用于边城模板的基础${category}条目。`,
`${xianxiaName},适用于灵潮模板的基础${category}条目。`,
),
statProfile,
useProfile,
buildProfile: buildBuildProfile("starter", ["legacy", ...tags]),
value: rankValue(rarity, slot, useProfile),
};
}
function buildArmoryDesign(
normalizedSourcePath: string,
labels: ItemCategoryLabels,
): DesignedItemMetadata | null {
const match = normalizedSourcePath.match(
/Armory\/Singles\/(Armor Singles|Weapon Singles)\/([^/]+)\/([^/]+)\.png$/u,
);
if (!match) return null;
const family = match[2];
const filename = match[3];
if (!family || !filename) return null;
const theme = MATERIAL_THEMES[family];
if (!theme) return null;
const pieceMatch = filename.match(/_(Boots|Chestplate|Gloves|Helmet|Leggings|Shield|Weapon)(\d+)$/u);
if (!pieceMatch) return null;
const pieceKey = pieceMatch[1];
const variantIndex = Number(pieceMatch[2]);
if (!pieceKey || Number.isNaN(variantIndex)) return null;
const piece = ARMOR_PIECE_LABELS[pieceKey];
if (!piece) return null;
const gradeTier = variantIndex >= 25 ? 2 : variantIndex >= 13 ? 1 : 0;
const rarity: ItemRarity = bumpRarity(theme.rarity, gradeTier);
const gradeWuxia = ["初式", "精铸", "真传"][gradeTier];
const gradeXianxia = ["凡品", "灵铸", "道印"][gradeTier];
const wuxiaName = `${theme.wuxia}${piece.wuxia}${gradeWuxia}`;
const xianxiaName = `${theme.xianxia}${piece.xianxia}${gradeXianxia}`;
const slot = piece.slot;
const category = slot === "weapon" ? labels.weapon : labels.armor;
const setId = `set-armory-${family.toLowerCase()}`;
const setName = `${theme.setWuxia} / ${theme.setXianxia}`;
const statProfile =
slot === "weapon"
? buildEquipmentStats(slot, rarity, theme.role, piece.pieceName)
: buildEquipmentStats(slot, rarity, theme.role, piece.pieceName);
const tags = dedupe([
category === labels.weapon ? "weapon" : "armor",
theme.worldAffinity,
theme.role,
...theme.tags,
`set:${family.toLowerCase()}`,
`piece:${piece.pieceName}`,
]);
return {
name: theme.worldAffinity === "xianxia" ? xianxiaName : wuxiaName,
category,
rarity,
tags,
description: `${theme.setWuxia} / ${theme.setXianxia} 套装中的 ${piece.pieceName} 位。相邻编号代表同家族不同锻造阶段,适合围绕 ${theme.synergy.join("、")} 组 build。`,
worldAffinity: theme.worldAffinity,
equipmentSlotId: slot,
worldProfiles: buildWorldProfiles(
wuxiaName,
xianxiaName,
`${wuxiaName}来自${theme.setWuxia}套装,偏向${theme.synergy.join("、")}的边城模板 build。`,
`${xianxiaName}属于${theme.setXianxia}套装,围绕${theme.synergy.join("、")}构筑灵潮模板战法。`,
),
statProfile,
useProfile: null,
buildProfile: buildBuildProfile(theme.role, theme.tags, {
setId,
setName,
pieceName: piece.pieceName,
synergy: theme.synergy,
}),
value: rankValue(rarity, slot, null),
};
}
function buildJewelryDesign(
normalizedSourcePath: string,
labels: ItemCategoryLabels,
): DesignedItemMetadata | null {
const match = normalizedSourcePath.match(
/Jewelry\/(Rings|Necklaces|Bracelets)\/Singles(?:\/[^/]+)*\/([^/]+)\.png$/u,
);
if (!match) return null;
const jewelryType = match[1];
const filename = match[2];
if (!jewelryType || !filename) return null;
const descriptor = filename.replace(/^\d+_/u, "");
const role = detectRoleFromDescriptor(descriptor);
const sizeTier =
/Large|Fancy|Holder/u.test(descriptor) ? 2 : /Medium|Necklace|Bracelet/u.test(descriptor) ? 1 : 0;
const rarity: ItemRarity = bumpRarity(
sizeTier === 2 ? "rare" : sizeTier === 1 ? "uncommon" : "common",
/Arcane|Esoteric|Lunar|Relic/u.test(descriptor) ? 1 : 0,
);
const worldAffinity = role === "caster" ? "xianxia" : role === "berserker" || role === "assassin" ? "wuxia" : "neutral";
const baseWuxiaType = jewelryType === "Rings" ? "戒" : jewelryType === "Necklaces" ? "坠" : "镯";
const baseXianxiaType = jewelryType === "Rings" ? "灵戒" : jewelryType === "Necklaces" ? "灵坠" : "灵镯";
const leadingToken = descriptor.split("_").find(Boolean) ?? jewelryType;
const wuxiaName = `${buildGenericTokenName(leadingToken, WorldType.WUXIA)}${baseWuxiaType}`;
const xianxiaName = `${buildGenericTokenName(leadingToken, WorldType.XIANXIA)}${baseXianxiaType}`;
const tags = dedupe(["relic", role, jewelryType.toLowerCase(), worldAffinity]);
const setId = `set-jewelry-${role}`;
return {
name: worldAffinity === "xianxia" ? xianxiaName : wuxiaName,
category: labels.relic,
rarity,
tags,
description: `${jewelryType} 家族的 ${descriptor.replace(/_/g, " ")} 款式。围绕 ${role} build 提供核心词条,也可以与同角色定位的项链/手镯/戒指拼成饰品流派。`,
worldAffinity,
equipmentSlotId: "relic",
worldProfiles: buildWorldProfiles(
wuxiaName,
xianxiaName,
`${wuxiaName}偏向${role}向的武侠搭配,可作为饰品核心件。`,
`${xianxiaName}更适合${role}向仙侠构筑,用于补足法力、爆发或护体短板。`,
),
statProfile: buildRelicStats(rarity, role),
useProfile: null,
buildProfile: buildBuildProfile(role, tags, {
setId,
setName: `${role} 饰品系`,
pieceName: jewelryType.toLowerCase(),
synergy: ["饰品 build", "定向补短", "三件成型"],
}),
value: rankValue(rarity, "relic", null),
};
}
function buildPotionDesign(
normalizedSourcePath: string,
labels: ItemCategoryLabels,
): DesignedItemMetadata | null {
if (!/Potions\/Singles\//u.test(normalizedSourcePath)) return null;
const filename = normalizedSourcePath.split("/").pop() ?? normalizedSourcePath;
if (/Glass|Bottle_/u.test(filename) && !/Health|Mana|Pure|Essence|Soul/u.test(filename)) {
return {
name: "空药瓶",
category: labels.material,
rarity: "common",
tags: ["material", "alchemy"],
description: "炼药与装液容器,可作为配方材料或支线道具。",
worldAffinity: "neutral",
equipmentSlotId: null,
worldProfiles: buildWorldProfiles(
"药瓶",
"灵瓶",
"边城模板里常见的炼药容器。",
"灵潮模板里常用的盛装灵液器皿。",
),
statProfile: null,
useProfile: null,
buildProfile: buildBuildProfile("alchemy", ["material", "alchemy"]),
value: 14,
};
}
const index = parseVariantIndex(normalizedSourcePath);
const isHealth = /Health/u.test(filename);
const isMana = /Mana/u.test(filename);
const isPure = /Pure/u.test(filename);
const isEssence = /Essence/u.test(filename);
const isSoul = /Soul/u.test(filename);
const rarity = isSoul
? "legendary"
: isPure || isEssence
? "epic"
: index > 118
? "rare"
: index > 108
? "uncommon"
: "common";
const gradeText = isSoul ? "封魂" : isPure ? "澄澈" : isEssence ? "萃华" : rarity === "rare" ? "上品" : rarity === "uncommon" ? "精制" : "常备";
const wuxiaName = isHealth
? `${gradeText}回春药`
: isMana
? `${gradeText}养神露`
: `${gradeText}奇药`;
const xianxiaName = isHealth
? `${gradeText}补元灵液`
: isMana
? `${gradeText}聚灵露`
: `${gradeText}灵酿`;
const useProfile: ItemUseProfile = {
hpRestore: isHealth ? (isSoul ? 120 : isPure ? 82 : isEssence ? 64 : rarity === "rare" ? 44 : rarity === "uncommon" ? 28 : 18) : 0,
manaRestore: isMana ? (isSoul ? 96 : isPure ? 70 : isEssence ? 54 : rarity === "rare" ? 38 : rarity === "uncommon" ? 24 : 16) : 0,
cooldownReduction: isSoul || /Power/u.test(filename) ? 1 : 0,
buildBuffs: isHealth
? buildItemBuildBuffs(
`potion-${filename.replace(/\.png$/iu, "").toLowerCase()}`,
"续战药势",
["回复", "续战"],
isSoul ? 3 : 2,
)
: isMana
? buildItemBuildBuffs(
`potion-${filename.replace(/\.png$/iu, "").toLowerCase()}`,
"聚灵药势",
["法力", "过载"],
isSoul ? 3 : 2,
)
: [],
};
const tags = dedupe([
"alchemy",
"consumable",
isHealth ? "healing" : "",
isMana ? "mana" : "",
isSoul ? "legendary-tonic" : "",
]);
return {
name: wuxiaName,
category: labels.consumable,
rarity,
tags,
description: "同形药瓶按纯度和封装级别区分强度,越靠后的高阶药剂越适合核心战斗循环与极限保命。",
worldAffinity: isMana || isSoul ? "xianxia" : "neutral",
equipmentSlotId: null,
worldProfiles: buildWorldProfiles(
wuxiaName,
xianxiaName,
`${wuxiaName}常见于边城模板的远行行囊,用于快速续战或调息。`,
`${xianxiaName}多用于灵潮模板的据点与试炼前后,负责补元、聚灵与压缩冷却。`,
),
statProfile: null,
useProfile,
buildProfile: buildBuildProfile("alchemy", tags, {
synergy: ["续航", "爆发前准备", "战中救急"],
}),
value: rankValue(rarity, null, useProfile),
};
}
function buildGemDesign(
normalizedSourcePath: string,
labels: ItemCategoryLabels,
): DesignedItemMetadata | null {
if (!/Gems(?: II)?\/Singles\//u.test(normalizedSourcePath)) return null;
const filename = normalizedSourcePath.split("/").pop() ?? normalizedSourcePath;
const tokenMatch = filename.match(/(Ruby|Onyx|Sapphire|Morganite|Emerald|Topaz|Amethyst|Diamond|Opal)/iu);
const token = tokenMatch?.[1] ?? "Crystal";
const role =
/Ruby|Crimson/u.test(token) ? "berserker"
: /Sapphire|Amethyst|Morganite/u.test(token) ? "caster"
: /Onyx|Diamond/u.test(token) ? "ward"
: /Topaz|Opal/u.test(token) ? "assassin"
: "support";
const rarity = /Dust/u.test(filename) ? "uncommon" : /Crystal/u.test(filename) ? "epic" : "rare";
const category = /Dust/u.test(filename) ? labels.material : labels.relic;
const wuxiaName = `${buildGenericTokenName(token, WorldType.WUXIA)}${/Dust/u.test(filename) ? "碎屑" : /Crystal/u.test(filename) ? "" : ""}`;
const xianxiaName = `${buildGenericTokenName(token, WorldType.XIANXIA)}${/Dust/u.test(filename) ? "粉末" : /Crystal/u.test(filename) ? "" : ""}`;
const tags = dedupe([
category === labels.material ? "material" : "relic",
token.toLowerCase(),
role,
"socket",
]);
return {
name: category === labels.material ? wuxiaName : xianxiaName,
category,
rarity,
tags,
description: `${token} 系晶石适合做强度梯度:粉尘是材料,宝石是中阶插件,晶体是高阶核心件。`,
worldAffinity: category === labels.relic ? "xianxia" : "neutral",
equipmentSlotId: category === labels.relic ? "relic" : null,
worldProfiles: buildWorldProfiles(
wuxiaName,
xianxiaName,
`${wuxiaName}偏向边城模板里的匠造、镶嵌与兵刃锻造。`,
`${xianxiaName}更适合灵潮模板里的灵器镶嵌与灵力 build 核心堆叠。`,
),
statProfile: category === labels.relic ? buildRelicStats(rarity, role) : null,
useProfile: null,
buildProfile: buildBuildProfile(role, tags, {
setId: `gem-${token.toLowerCase()}`,
setName: `${token} 晶石谱系`,
pieceName: /Dust/u.test(filename) ? "dust" : /Crystal/u.test(filename) ? "crystal" : "gem",
synergy: ["镶嵌", "词条放大", "build 补强"],
}),
value: rankValue(rarity, category === labels.relic ? "relic" : null, null),
};
}
function buildSkillRelicDesign(
normalizedSourcePath: string,
labels: ItemCategoryLabels,
): DesignedItemMetadata | null {
if (!/Skills\/Singles\//u.test(normalizedSourcePath) && !/Librarium\/Singles\//u.test(normalizedSourcePath)) {
return null;
}
const filename = normalizedSourcePath.split("/").pop() ?? normalizedSourcePath;
const role = detectRoleFromDescriptor(filename);
const isBookLike = /Book|Grimoire|Literature/u.test(filename);
const isBooster = /Booster/u.test(filename);
const isPassive = /Passive/u.test(filename);
const isUtility = /Echolocation|Nightvision|Copy|Shout|Panic/u.test(filename);
const category = isBooster ? labels.consumable : isBookLike || isPassive || isUtility ? labels.relic : labels.rare;
const rarity = isPassive ? "epic" : isBooster ? "rare" : isBookLike ? "epic" : "rare";
const wuxiaName = isBookLike
? `·${filename.replace(/^\d+_/u, "").replace(/_/g, " ").replace(/\.png$/iu, "")}`
: `秘术符印·${filename.replace(/^\d+_/u, "").replace(/_/g, " ").replace(/\.png$/iu, "")}`;
const xianxiaName = isBookLike
? `灵诀玉简·${filename.replace(/^\d+_/u, "").replace(/_/g, " ").replace(/\.png$/iu, "")}`
: `神通法印·${filename.replace(/^\d+_/u, "").replace(/_/g, " ").replace(/\.png$/iu, "")}`;
const useProfile =
category === labels.consumable
? {
hpRestore: 0,
manaRestore: 24 + rarityToRank(rarity) * 8,
cooldownReduction: 1,
buildBuffs: buildItemBuildBuffs(
`skill-relic-${filename.replace(/\.png$/iu, "").toLowerCase()}`,
"功法激发",
[role, /Arrow|Spear/u.test(filename) ? "" : /Shield/u.test(filename) ? "" : ""],
2,
),
}
: null;
const tags = dedupe([
category === labels.consumable ? "consumable" : category === labels.relic ? "relic" : "rare",
role,
isBooster ? "cooldown" : "",
/Fire/u.test(filename) ? "fire" : "",
/Lightning/u.test(filename) ? "lightning" : "",
/Shield/u.test(filename) ? "ward" : "",
/Sword|Punch/u.test(filename) ? "burst" : "",
/Arrow|Spear/u.test(filename) ? "projectile" : "",
]);
return {
name: xianxiaName,
category,
rarity,
tags,
description: "技能图标类物品会被设计成功法、符印、强化器或秘卷,用于支撑特定流派的 build 想象。",
worldAffinity: "xianxia",
equipmentSlotId: category === labels.relic ? "relic" : null,
worldProfiles: buildWorldProfiles(
wuxiaName,
xianxiaName,
`${wuxiaName}适合在边城模板里解释为武学秘卷、战术符印或绝招凭证。`,
`${xianxiaName}可作为功法玉简、灵术法印或灵能强化器,用于构筑法修流派。`,
),
statProfile: category === labels.relic ? buildRelicStats(rarity, role) : null,
useProfile,
buildProfile: buildBuildProfile(role, tags, {
setId: `skill-${role}`,
setName: `${role} 功法谱`,
synergy: ["职业核心", "技能联动", "法术 build"],
}),
value: rankValue(rarity, category === labels.relic ? "relic" : null, useProfile),
};
}
function buildUtilityDesign(
normalizedSourcePath: string,
labels: ItemCategoryLabels,
): DesignedItemMetadata {
const filename = normalizedSourcePath.split("/").pop() ?? normalizedSourcePath;
const readable = filename.replace(/^\d+_/u, "").replace(/_/g, " ").replace(/\.png$/iu, "");
const lower = readable.toLowerCase();
const category =
/ore|ingot|dust|bone|nail|card|flag|plushie|fish|string|rope|hook|cage|flower|seed|leaf|feather|skin|wood|stone/u.test(lower)
? labels.material
: /throwable|snowballs|meat|apple|mushroom/u.test(lower)
? labels.consumable
: /rod|pickaxe|sword|bow|mace|shield/u.test(lower)
? /shield/u.test(lower)
? labels.armor
: labels.weapon
: /book|relic|amulet|charm|skull|eyepatch|hook/u.test(lower)
? labels.relic
: labels.rare;
const rarity =
/gold|angelic|sacred|relic|crystal/u.test(lower)
? "epic"
: /steel|silver|special|pirate|magic/u.test(lower)
? "rare"
: /iron|bundle|advanced|fresh/u.test(lower)
? "uncommon"
: "common";
const slot: EquipmentSlotId | null =
category === labels.weapon ? "weapon" : category === labels.armor ? "armor" : category === labels.relic ? "relic" : null;
const role = detectRoleFromDescriptor(lower);
const useProfile: ItemUseProfile | null =
category === labels.consumable
? {
hpRestore: /meat|apple|mushroom/u.test(lower) ? 16 + rarityToRank(rarity) * 8 : 0,
manaRestore: /magic|crystal|water/u.test(lower) ? 14 + rarityToRank(rarity) * 6 : 0,
cooldownReduction: /throwable|snowballs/u.test(lower) ? 1 : 0,
}
: null;
const statProfile =
slot === "weapon"
? buildEquipmentStats("weapon", rarity, role, "weapon")
: slot === "armor"
? buildEquipmentStats("armor", rarity, role, "armor")
: slot === "relic"
? buildRelicStats(rarity, role)
: null;
const wuxiaName = readable
.split(" ")
.map((token) => buildGenericTokenName(token, WorldType.WUXIA))
.join("");
const xianxiaName = readable
.split(" ")
.map((token) => buildGenericTokenName(token, WorldType.XIANXIA))
.join("");
return {
name: wuxiaName || readable,
category,
rarity,
tags: dedupe([
...(/meat|apple|mushroom/u.test(lower) ? ["healing"] : []),
...(/magic|crystal|water/u.test(lower) ? ["mana"] : []),
...(slot === "weapon" ? ["weapon"] : slot === "armor" ? ["armor"] : slot === "relic" ? ["relic"] : []),
...(category === labels.material ? ["material"] : []),
role,
]),
description: `${readable} 根据视觉和路径被自动归入 ${category} 家族,可作为 ${role} 向 build 的支撑件或素材件。`,
worldAffinity: /magic|crystal|sacred|angelic|spirit|astral/u.test(lower) ? "xianxia" : "neutral",
equipmentSlotId: slot,
worldProfiles: buildWorldProfiles(
wuxiaName || readable,
xianxiaName || readable,
`${wuxiaName || readable}更适合边城模板的在地使用语境。`,
`${xianxiaName || readable}更适合灵潮模板的灵物/法器语境。`,
),
statProfile,
useProfile,
buildProfile: buildBuildProfile(role, [category, role], {
synergy: ["素材拓展", "过渡 build", "题材补完"],
}),
value: rankValue(rarity, slot, useProfile),
};
}
export function buildDesignedItemMetadata(
normalizedSourcePath: string,
baseName: string,
baseCategory: string,
baseRarity: ItemRarity,
baseTags: string[],
labels: ItemCategoryLabels,
): DesignedItemMetadata {
const specialized =
buildLegacyDesign(normalizedSourcePath, baseName, baseCategory, baseRarity, baseTags, labels) ??
buildArmoryDesign(normalizedSourcePath, labels) ??
buildJewelryDesign(normalizedSourcePath, labels) ??
buildPotionDesign(normalizedSourcePath, labels) ??
buildGemDesign(normalizedSourcePath, labels) ??
buildSkillRelicDesign(normalizedSourcePath, labels);
if (specialized) {
return {
...specialized,
tags: dedupe([...(specialized.tags ?? []), ...baseTags]),
};
}
const fallback = buildUtilityDesign(normalizedSourcePath, labels);
return {
...fallback,
name: fallback.name || baseName,
category: fallback.category || baseCategory,
rarity: fallback.rarity || baseRarity,
tags: dedupe([...(fallback.tags ?? []), ...baseTags]),
};
}