This commit is contained in:
280
src/uiAssets.ts
Normal file
280
src/uiAssets.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user