diff --git a/packages-user/client-modules/src/ui/shop.tsx b/packages-user/client-modules/src/ui/shop.tsx new file mode 100644 index 0000000..84298c3 --- /dev/null +++ b/packages-user/client-modules/src/ui/shop.tsx @@ -0,0 +1,328 @@ +import { GameUI, SetupComponentOptions } from '@motajs/system'; +import { computed, defineComponent, ref } from 'vue'; +import { Font } from '@motajs/render'; +import { Scroll } from '@user/client-base'; +import { IHeroState, IShopState } from '@user/data-base'; +import { IShopSystem } from '@user/data-system'; +import { IHeroAttr } from '@user/data-common'; +import { FULL_LOC, MAIN_HEIGHT, MAIN_WIDTH } from '../shared'; +import { IUIPropsBase } from './types'; + +export interface ShopProps extends IUIPropsBase { + /** 商店状态对象 */ + shop: IShopState; + /** 勇士状态对象 */ + heroState: IHeroState; + /** 商店系统对象,负责执行购买与售出交易 */ + shopSystem: IShopSystem; +} + +const shopProps = { + props: [ + 'controller', + 'instance', + 'state', + 'shop', + 'heroState', + 'shopSystem' + ] +} satisfies SetupComponentOptions; + +export interface IShopItemEntry { + /** 道具图块数字 */ + readonly num: number; + /** 道具字符串 id */ + readonly id: string; + /** 道具名称 */ + readonly name: string; + /** 道具描述 */ + readonly text: string; + /** 商店库存 */ + readonly stock: number; + /** 玩家拥有数量 */ + readonly owned: number; + /** 道具买入价 */ + readonly buyPrice: number; + /** 道具卖出价 */ + readonly sellPrice: number; +} + +/** 顶部购买 / 售出切换栏的高度 */ +const MODE_BAR_HEIGHT = 36; +/** 左侧商店列表的宽度 */ +const LIST_WIDTH = 250; +/** 左侧商店列表的顶部纵坐标 */ +const LIST_TOP = MODE_BAR_HEIGHT + 4; +/** 左侧商店列表的高度,底部为退出按钮留出空间 */ +const LIST_HEIGHT = MAIN_HEIGHT - LIST_TOP - 16; +/** 商店列表中每一行的高度 */ +const ROW_HEIGHT = 40; +/** 行内图标的尺寸 */ +const ROW_ICON_SIZE = 28; +/** 右侧信息区域的起始横坐标 */ +const INFO_LEFT = LIST_WIDTH; +/** 右侧信息区域的宽度 */ +const INFO_WIDTH = MAIN_WIDTH - INFO_LEFT; +/** 道具名称的纵坐标 */ +const NAME_TOP = MODE_BAR_HEIGHT + 10; +/** 道具描述区域的顶部纵坐标 */ +const DESC_TOP = NAME_TOP + 26; +/** 道具描述区域的高度 */ +const DESC_HEIGHT = 116; +/** 价格信息区域每行的高度 */ +const INFO_ROW_HEIGHT = 20; +/** 价格信息区域的顶部纵坐标 */ +const PRICE_TOP = DESC_TOP + DESC_HEIGHT + 6; + +/** 标题与按钮字体 */ +const titleFont = Font.defaults({ size: 14, weight: 700 }); +/** 列表与信息字体 */ +const textFont = Font.defaults({ size: 12 }); + +export const Shop = defineComponent(props => { + const shop = props.shop; + const hero = props.heroState; + + /** 当前的购买 / 售出模式 */ + const mode = ref<'buy' | 'sell'>('buy'); + /** 当前选中的道具图块数字,-1 表示未选中 */ + const selected = ref(-1); + + /** 商店道具列表,按图块数字升序排列 */ + const entries = computed(() => { + const list: IShopItemEntry[] = []; + for (const pair of shop.iterateItems()) { + const num = pair[0]; + const data = props.state.itemStore.getData(num); + if (data === null) continue; + const id = props.state.tileStore.id(num); + const price = shop.getPrice(num); + list.push({ + num, + id: id ?? '', + name: data.name, + text: data.text, + stock: shop.getCount(num), + owned: hero.items.itemCount(num), + buyPrice: price[0], + sellPrice: price[1] + }); + } + return list.sort((a, b) => a.num - b.num); + }); + + /** 当前选中的道具条目,未选中时为 null */ + const current = computed(() => { + for (const entry of entries.value) { + if (entry.num === selected.value) return entry; + } + return null; + }); + + /** + * 选中一个道具 + * @param num 被选中道具的图块数字 + */ + const selectItem = (num: number) => { + selected.value = num; + }; + + /** + * 切换购买 / 售出模式 + * @param next 切换到的模式 + */ + const changeMode = (next: 'buy' | 'sell') => { + mode.value = next; + }; + + return () => { + const entry = current.value; + return ( + + + changeMode('buy')} + /> + changeMode('sell')} + /> + + + {entries.value.map((item, i) => { + const isSelected = item.num === selected.value; + return ( + selectItem(item.num)} + > + + + + + + ); + })} + + + + + + + + + + + + + + + + + + + ); + }; +}, shopProps); + +export const ShopUI = new GameUI('shop', Shop);