963 lines
33 KiB
TypeScript
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]),
|
|
};
|
|
}
|