Files
Genarrative/src/uiAssets.ts
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

308 lines
10 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);
}