Files
Genarrative/src/uiAssets.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

281 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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否则中间行高度为 0border-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);
}