Files
Genarrative/src/uiAssets.ts
T
wuxiangwanzi fcd9470dcf 加入客服反馈和社区二维码
增加桌面端全站客服反馈悬浮入口。

悬停入口时展示 QQ 和微信社区二维码浮窗。

补充入口拖拽边界、点击跳转和移动端隐藏测试。

同步全站客服悬浮入口接入文档。
2026-06-23 21:02:13 +08:00

285 lines
9.9 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',
taonierProductIp: '/branding/taonier-product-ip.png',
taonierFeedbackEntry: '/branding/taonier-feedback-entry.png',
taonierFeedbackQq: '/branding/taonier-feedback-qq.png',
taonierFeedbackWechat: '/branding/taonier-feedback-wechat.png',
} 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);
}