281 lines
9.6 KiB
TypeScript
281 lines
9.6 KiB
TypeScript
import type {CSSProperties} from 'react';
|
||
|
||
import type {InventoryItem} from './types';
|
||
|
||
export type NineSliceTexture = {
|
||
src: string;
|
||
slice: {
|
||
top: number;
|
||
right: number;
|
||
bottom: number;
|
||
left: number;
|
||
};
|
||
padding?: {
|
||
x: number;
|
||
y: number;
|
||
};
|
||
repeat?: 'stretch' | 'repeat' | 'round';
|
||
baseColor?: string;
|
||
};
|
||
|
||
export function getNineSliceStyle(
|
||
texture: NineSliceTexture,
|
||
overrides?: Partial<{
|
||
paddingX: number;
|
||
paddingY: number;
|
||
scale: number;
|
||
repeat: NineSliceTexture['repeat'];
|
||
baseColor: string;
|
||
}>,
|
||
): CSSProperties {
|
||
const style = {
|
||
'--frame-src': `url("${texture.src}")`,
|
||
'--slice-top': `${texture.slice.top}`,
|
||
'--slice-right': `${texture.slice.right}`,
|
||
'--slice-bottom': `${texture.slice.bottom}`,
|
||
'--slice-left': `${texture.slice.left}`,
|
||
'--frame-pad-x': `${overrides?.paddingX ?? texture.padding?.x ?? 14}`,
|
||
'--frame-pad-y': `${overrides?.paddingY ?? texture.padding?.y ?? 12}`,
|
||
'--frame-repeat': overrides?.repeat ?? texture.repeat ?? 'round',
|
||
} as CSSProperties & Record<string, string>;
|
||
|
||
const baseColor = overrides?.baseColor ?? texture.baseColor;
|
||
if (baseColor) style['--frame-base'] = baseColor;
|
||
if (typeof overrides?.scale === 'number') style['--frame-scale'] = `${overrides.scale}`;
|
||
|
||
return style;
|
||
}
|
||
|
||
export const CUSTOM_WORLD_THEME_ICONS = {
|
||
martial: '/Icons/38_sword.png',
|
||
arcane: '/Icons/72_magic.png',
|
||
} as const;
|
||
|
||
export const BRAND_ASSETS = {
|
||
kuranGamesLogo: '/branding/kuran-games-logo.svg',
|
||
} as const;
|
||
|
||
export const UI_CHROME = {
|
||
appBackground: '/UI/Background_fill.png',
|
||
// 图源 125×28:上下 slice 之和必须 < 28,否则中间行高度为 0,border-image fill 失效(见 UI_CODING_STANDARD.md)
|
||
characterCardFrame: {
|
||
src: '/UI/pick_hero_frame.png',
|
||
slice: { top: 18, right: 18, bottom: 18, left: 18 },
|
||
padding: { x: 16, y: 14 },
|
||
repeat: 'round',
|
||
},
|
||
tabActive: {
|
||
src: '/UI/Shop_tab_picked.png',
|
||
slice: { top: 16, right: 16, bottom: 16, left: 16 },
|
||
padding: { x: 10, y: 10 },
|
||
repeat: 'round',
|
||
},
|
||
tabInactive: {
|
||
src: '/UI/Shop_tab.png',
|
||
slice: { top: 16, right: 16, bottom: 16, left: 16 },
|
||
padding: { x: 10, y: 10 },
|
||
repeat: 'round',
|
||
},
|
||
panel: {
|
||
src: '/UI/Frame_bg_big_2.png',
|
||
slice: { top: 20, right: 20, bottom: 20, left: 20 },
|
||
padding: { x: 18, y: 16 },
|
||
repeat: 'round',
|
||
},
|
||
storyPanel: {
|
||
src: '/UI/Dialogue_frame.png',
|
||
slice: { top: 18, right: 18, bottom: 18, left: 18 },
|
||
padding: { x: 18, y: 16 },
|
||
repeat: 'round',
|
||
},
|
||
inventoryPanel: {
|
||
src: '/UI/Inventory_bg.png',
|
||
slice: { top: 18, right: 18, bottom: 18, left: 18 },
|
||
padding: { x: 18, y: 16 },
|
||
repeat: 'round',
|
||
},
|
||
statsPanel: {
|
||
src: '/UI/Stats_bar.png',
|
||
slice: { top: 16, right: 16, bottom: 16, left: 16 },
|
||
padding: { x: 16, y: 14 },
|
||
repeat: 'round',
|
||
},
|
||
choiceButton: {
|
||
src: '/UI/Options_bar.png',
|
||
slice: { top: 14, right: 14, bottom: 14, left: 14 },
|
||
padding: { x: 16, y: 12 },
|
||
repeat: 'round',
|
||
},
|
||
/** 地图弹窗外壳:切片只含浅灰描边+角饰,勿把大块深色中心划进 border(否则会吃掉内容区) */
|
||
modalPanel: {
|
||
src: '/UI/Frame_square_512.png',
|
||
slice: { top: 6, right: 6, bottom: 6, left: 6 },
|
||
padding: { x: 14, y: 10 },
|
||
repeat: 'stretch',
|
||
},
|
||
/** 仅用于地图弹窗内信息板(256 图为 512 的半尺寸,切片同比) */
|
||
infoPanel: {
|
||
src: '/UI/Frame_square_256.png',
|
||
slice: { top: 3, right: 3, bottom: 3, left: 3 },
|
||
padding: { x: 9, y: 6 },
|
||
repeat: 'stretch',
|
||
},
|
||
/** 画布顶部可点击地点名 */
|
||
mapDiagramPanel: {
|
||
src: '/UI/Map_frame.png',
|
||
slice: { top: 12, right: 12, bottom: 12, left: 12 },
|
||
padding: { x: 18, y: 16 },
|
||
repeat: 'stretch',
|
||
},
|
||
sceneTitle: {
|
||
src: '/UI/special_button.png',
|
||
slice: { top: 9, right: 14, bottom: 9, left: 14 },
|
||
padding: { x: 16, y: 5 },
|
||
repeat: 'stretch',
|
||
},
|
||
/** 地图弹窗内节点卡片(方框) */
|
||
mapRoomCell: {
|
||
src: '/UI/Frame_square_256.png',
|
||
slice: { top: 3, right: 3, bottom: 3, left: 3 },
|
||
padding: { x: 8, y: 4 },
|
||
repeat: 'stretch',
|
||
},
|
||
} as const;
|
||
|
||
export const TAB_ICONS = {
|
||
character: {
|
||
active: '/Icons/42_shield.png',
|
||
inactive: '/Icons/42_shield.png',
|
||
},
|
||
adventure: {
|
||
active: '/UI/1_weapon.png',
|
||
inactive: '/UI/1_weapon_d.png',
|
||
},
|
||
inventory: {
|
||
active: '/Icons/28_bag.png',
|
||
inactive: '/Icons/28_bag.png',
|
||
},
|
||
} as const;
|
||
|
||
export const CHROME_ICONS = {
|
||
optionArrow: '/UI/11_right_arrow.png',
|
||
map: '/UI/Map_icon_action.png',
|
||
settings: "/Icons/Admurin's Pixel Items/Admurin's Pixel Items/General/Singles/499_Iron_Gear.png",
|
||
close: '/UI/1_exit_s.png',
|
||
refreshOptions: "/Icons/Admurin's Pixel Items/Admurin's Pixel Items/Miscellaneous/Singles/206_Dice.png",
|
||
} as const;
|
||
|
||
const EQUIPMENT_SLOT_ICONS: Record<string, string> = {
|
||
武器: '/UI/Icon_Eq_Weapon.png',
|
||
护甲: '/UI/Icon_Eq_Chest.png',
|
||
饰品: '/UI/Icon_Eq_ring.png',
|
||
};
|
||
|
||
const INVENTORY_CATEGORY_ICONS: Record<string, string> = {
|
||
武器: '/UI/Icon_Eq_Weapon.png',
|
||
护甲: '/UI/Icon_Eq_Chest.png',
|
||
饰品: '/UI/Icon_Eq_ring.png',
|
||
消耗品: '/Icons/12_potion.png',
|
||
稀有品: '/Icons/68_relic.png',
|
||
专属品: '/Icons/47_treasure.png',
|
||
专属物: '/Icons/47_treasure.png',
|
||
专属物品: '/Icons/47_treasure.png',
|
||
材料: '/Icons/45_crystal.png',
|
||
};
|
||
|
||
const ITEM_SEMANTIC_ICON_RULES: Array<{pattern: RegExp; icon: string}> = [
|
||
{pattern: /残页|书页|卷轴|卷|册|档案|秘典|图录|script|scroll|book/u, icon: '/Icons/01_Scroll.png'},
|
||
{pattern: /符印|符|印记|印章|印|sigil|seal|rune/u, icon: '/Icons/69_magic.png'},
|
||
{pattern: /戒|环|ring/u, icon: '/Icons/15_Silver_ring.png'},
|
||
{pattern: /坠|佩|链|neck|amulet/u, icon: '/Icons/13_neck.png'},
|
||
{pattern: /火|焰|烬|embers?|torch/u, icon: '/Icons/03_Torch.png'},
|
||
{pattern: /盔|冠|helm|helmet/u, icon: '/Icons/04_helm.png'},
|
||
{pattern: /甲|铠|壳|胸|护甲|armor|chest/u, icon: '/Icons/05_chest.png'},
|
||
{pattern: /裤|胫|腿|pants/u, icon: '/Icons/06_pants.png'},
|
||
{pattern: /靴|履|boots/u, icon: '/Icons/07_boots.png'},
|
||
{pattern: /药|露|浆|液|泉|瓶|bottle|potion|water/u, icon: '/Icons/12_potion.png'},
|
||
{pattern: /蘑|菇|菌/u, icon: '/Icons/24_Mushroom.png'},
|
||
{pattern: /肉|meat/u, icon: '/Icons/25_Meat.png'},
|
||
{pattern: /果|apple/u, icon: '/Icons/26_apple.png'},
|
||
{pattern: /骨|骸|skull/u, icon: '/Icons/27_Skull.png'},
|
||
{pattern: /囊|包|袋|bag|pouch/u, icon: '/Icons/29_bag.png'},
|
||
{pattern: /锤|mace|hammer/u, icon: '/Icons/30_mace.png'},
|
||
{pattern: /铲|spade/u, icon: '/Icons/31_spade.png'},
|
||
{pattern: /钱|币|coin/u, icon: '/Icons/32_coin.png'},
|
||
{pattern: /石|岩|stone/u, icon: '/Icons/33_stone.png'},
|
||
{pattern: /木|枝|wood/u, icon: '/Icons/34_wood.png'},
|
||
{pattern: /手套|glove|glowes/u, icon: '/Icons/35_glowes.png'},
|
||
{pattern: /花|flower/u, icon: '/Icons/55_flower.png'},
|
||
{pattern: /叶|leaf/u, icon: '/Icons/37_leaf.png'},
|
||
{pattern: /杖|wand|staff/u, icon: '/Icons/66_wand.png'},
|
||
{pattern: /弓|bow/u, icon: '/Icons/40_bow.png'},
|
||
{pattern: /箭|翎|羽|arrow/u, icon: '/Icons/41_arrow.png'},
|
||
{pattern: /盾|shield/u, icon: '/Icons/42_shield.png'},
|
||
{pattern: /绳|索|藤|须|rope|tendril/u, icon: '/Icons/44_rope.png'},
|
||
{pattern: /皮|膜|hide|skin|pelt/u, icon: '/Icons/46_skin.png'},
|
||
{pattern: /钥|匣|函|宝|treasure|relic|artifact|信物/u, icon: '/Icons/47_treasure.png'},
|
||
{pattern: /镐|pick|矿/u, icon: '/Icons/49_pick.png'},
|
||
{pattern: /银|silver|bar/u, icon: '/Icons/54_silverbar.png'},
|
||
{pattern: /晶|核|珠|瞳|眼|玉|lens|core|crystal|gem|pearl/u, icon: '/Icons/45_crystal.png'},
|
||
{pattern: /魔|灵|魂|mana|magic|essence/u, icon: '/Icons/69_magic.png'},
|
||
{pattern: /绷带|bandage/u, icon: '/Icons/67_bandage.png'},
|
||
{pattern: /剑|刃|牙|blade|fang|sword/u, icon: '/Icons/38_sword.png'},
|
||
];
|
||
|
||
function buildInventoryLookupText(
|
||
item: Pick<InventoryItem, 'name' | 'category' | 'tags' | 'equipmentSlotId'>,
|
||
) {
|
||
return [
|
||
item.name,
|
||
item.category,
|
||
item.equipmentSlotId ?? '',
|
||
...(item.tags ?? []),
|
||
]
|
||
.join(' ')
|
||
.trim()
|
||
.toLowerCase();
|
||
}
|
||
|
||
export function getEquipmentSlotIcon(slot: string) {
|
||
return EQUIPMENT_SLOT_ICONS[slot] ?? '/UI/Icon_Frame.png';
|
||
}
|
||
|
||
export function getInventoryCategoryIcon(category: string) {
|
||
return INVENTORY_CATEGORY_ICONS[category] ?? '/Icons/28_bag.png';
|
||
}
|
||
|
||
export function getInventoryItemVisualSrc(
|
||
item: Pick<InventoryItem, 'iconSrc' | 'name' | 'category' | 'tags' | 'equipmentSlotId'>,
|
||
) {
|
||
if (item.iconSrc) return item.iconSrc;
|
||
|
||
const lookupText = buildInventoryLookupText(item);
|
||
|
||
if (item.equipmentSlotId === 'weapon') {
|
||
if (/弓|bow/u.test(lookupText)) return '/Icons/40_bow.png';
|
||
if (/杖|wand|staff|符/u.test(lookupText)) return '/Icons/66_wand.png';
|
||
if (/锤|mace|hammer/u.test(lookupText)) return '/Icons/30_mace.png';
|
||
return '/Icons/38_sword.png';
|
||
}
|
||
|
||
if (item.equipmentSlotId === 'armor') {
|
||
if (/盾|shield/u.test(lookupText)) return '/Icons/42_shield.png';
|
||
return '/Icons/05_chest.png';
|
||
}
|
||
|
||
if (item.equipmentSlotId === 'relic') {
|
||
if (/戒|环|ring/u.test(lookupText)) return '/Icons/15_Silver_ring.png';
|
||
if (/坠|佩|neck|amulet/u.test(lookupText)) return '/Icons/13_neck.png';
|
||
}
|
||
|
||
const semanticMatch = ITEM_SEMANTIC_ICON_RULES.find(rule => rule.pattern.test(lookupText));
|
||
if (semanticMatch) return semanticMatch.icon;
|
||
|
||
if (item.tags.includes('mana')) return '/Icons/69_magic.png';
|
||
if (item.tags.includes('healing')) return '/Icons/12_potion.png';
|
||
if (item.tags.includes('material')) return '/Icons/45_crystal.png';
|
||
if (item.tags.includes('relic')) return '/Icons/68_relic.png';
|
||
|
||
return getInventoryCategoryIcon(item.category);
|
||
}
|