init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

280
src/uiAssets.ts Normal file
View 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否则中间行高度为 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);
}