diff --git a/packages-user/client-modules/src/render/components/list.tsx b/packages-user/client-modules/src/render/components/list.tsx new file mode 100644 index 0000000..5fd7a21 --- /dev/null +++ b/packages-user/client-modules/src/render/components/list.tsx @@ -0,0 +1,191 @@ +import { DefaultProps } from '@motajs/render-vue'; +import Scroll from 'packages/legacy-ui/src/components/scroll.vue'; +import { computed, defineComponent, ref, SlotsType, VNode } from 'vue'; +import { Selection } from './misc'; +import { ElementLocator } from '@motajs/render-core'; +import { Font } from '@motajs/render-style'; +import { SetupComponentOptions } from '@motajs/system-ui'; + +export interface ListProps extends DefaultProps { + /** 列表内容,第一项表示 id,第二项表示显示的内容 */ + list: [string, string][]; + /** 当前选中的项 */ + selected: string; + /** 定位 */ + loc: ElementLocator; + /** 每行的高度,默认 18 */ + lineHeight?: number; + /** 字体 */ + font?: Font; + /** 使用 winskin 作为光标 */ + winskin?: ImageIds; + /** 使用指定样式作为光标背景 */ + color?: CanvasStyle; + /** 使用指定样式作为光标边框 */ + border?: CanvasStyle; + /** 选择图标的不透明度范围 */ + alphaRange?: [number, number]; +} + +export type ListEmits = { + /** + * 当用户选中某一项时触发 + * @param key 选中的项的 id + */ + update: (key: string) => void; + + 'update:selected': (value: string) => void; +}; + +const listProps = { + props: [ + 'list', + 'selected', + 'loc', + 'lineHeight', + 'font', + 'winskin', + 'color', + 'border', + 'alphaRange' + ], + emits: ['update', 'update:selected'] +} satisfies SetupComponentOptions; + +export const List = defineComponent( + (props, { emit }) => { + const selected = ref(props.list[0][0]); + const lineHeight = computed(() => props.lineHeight ?? 18); + + const select = (value: string) => { + selected.value = value; + emit('update', value); + emit('update:selected', value); + }; + + return () => ( + + {props.list.map((v, i) => { + const [key, value] = v; + const loc: ElementLocator = [ + 0, + lineHeight.value * i, + props.loc[2] ?? 200, + lineHeight.value + ]; + return ( + select(key)}> + {selected.value === key && ( + + )} + + + ); + })} + + ); + }, + listProps +); + +export interface ListPageProps extends ListProps { + /** 组件定位 */ + loc: ElementLocator; + /** 列表所占比例 */ + basis?: number; + /** 列表是否排列在右侧 */ + right?: boolean; + /** 是否显示关闭按钮 */ + close?: boolean; +} + +export type ListPageEmits = { + close: () => void; +} & ListEmits; + +export type ListPageSlots = SlotsType<{ + default: (key: string) => VNode | VNode[]; + + [x: string]: (key: string) => VNode | VNode[]; +}>; + +const listPageProps = { + props: [ + 'basis', + 'right', + 'list', + 'selected', + 'loc', + 'lineHeight', + 'font', + 'winskin', + 'color', + 'border', + 'alphaRange' + ], + emits: ['update', 'update:selected', 'close'] +} satisfies SetupComponentOptions< + ListPageProps, + ListPageEmits, + keyof ListPageEmits, + ListPageSlots +>; + +export const ListPage = defineComponent< + ListPageProps, + ListPageEmits, + keyof ListPageEmits, + ListPageSlots +>((props, { emit, slots }) => { + const selected = ref(props.selected); + + const basis = computed(() => props.basis ?? 0.3); + const width = computed(() => props.loc[2] ?? 200); + const height = computed(() => props.loc[3] ?? 200); + const listLoc = computed(() => { + const listWidth = width.value * basis.value; + if (props.right) { + return [width.value - listWidth, 0, listWidth, height.value]; + } else { + return [0, 0, listWidth, height.value]; + } + }); + const contentLoc = computed(() => { + const contentWidth = width.value * (1 - basis.value); + if (props.right) { + return [0, 0, contentWidth, height.value]; + } else { + return [width.value - contentWidth, 0, contentWidth, height.value]; + } + }); + + const update = (key: string) => { + emit('update', key); + emit('update:selected', key); + }; + + return () => ( + + + + {slots[selected.value]?.(selected.value) ?? + slots.default?.(selected.value)} + + {props.close && ( + + )} + + ); +}, listPageProps); diff --git a/packages-user/client-modules/src/render/ui/statistics.tsx b/packages-user/client-modules/src/render/ui/statistics.tsx new file mode 100644 index 0000000..9418596 --- /dev/null +++ b/packages-user/client-modules/src/render/ui/statistics.tsx @@ -0,0 +1,198 @@ +import { + GameUI, + IUIMountable, + SetupComponentOptions, + UIComponentProps +} from '@motajs/system-ui'; +import { defineComponent } from 'vue'; +import { ListPage } from '../components/list'; +import { waitbox } from '../components'; +import { DefaultProps } from '@motajs/render-vue'; +import { ItemState } from '@user/data-state'; + +interface StatisticsDataOneFloor { + enemyCount: number; + potionCount: number; + gemCount: number; + potionValue: number; + atkValue: number; + defValue: number; + mdefValue: number; +} + +interface StatisticsData { + total: StatisticsDataOneFloor; + floors: Map; +} + +export interface StatisticsProps extends UIComponentProps, DefaultProps { + data: StatisticsData; +} + +const statisticsProps = { + props: ['data'] +} satisfies SetupComponentOptions; + +export const Statistics = defineComponent(props => { + const list: [string, string][] = [ + ['total', '总览'], + ['floor', '楼层'], + ['enemy', '怪物'], + ['potion', '血瓶宝石'] + ]; + + const close = () => { + props.controller.close(props.instance); + }; + + return () => ( + + {{ + total: () => , + floor: () => , + enemy: () => , + potion: () => + }} + + ); +}, statisticsProps); + +interface StatisticsPanelProps extends DefaultProps { + data: StatisticsData; +} + +const TotalStatistics = defineComponent(props => { + return () => ; +}, statisticsProps); + +const FloorStatistics = defineComponent(props => { + return () => ; +}, statisticsProps); + +const EnemyStatistics = defineComponent(props => { + return () => ; +}, statisticsProps); + +const PotionStatistics = defineComponent(props => { + return () => ; +}, statisticsProps); + +function calculateStatistics(): StatisticsData { + core.setFlag('__statistics__', true); + const hero = core.status.hero; + const diff: Record = {}; + const handler: ProxyHandler = { + set(_target, p, newValue) { + if (typeof newValue === 'number') { + diff[p] ??= 0; + diff[p] += newValue; + } + return true; + } + }; + const proxy = new Proxy(hero, handler); + core.status.hero = proxy; + + const floors = new Map(); + core.floorIds.forEach(v => { + core.extractBlocks(v); + const statistics: StatisticsDataOneFloor = { + enemyCount: 0, + potionCount: 0, + gemCount: 0, + potionValue: 0, + atkValue: 0, + defValue: 0, + mdefValue: 0 + }; + core.status.maps[v].blocks.forEach(v => { + if (v.event.cls === 'enemys' || v.event.cls === 'enemy48') { + statistics.enemyCount++; + } else if (v.event.cls === 'items') { + const item = ItemState.items.get( + v.event.id as AllIdsOf<'items'> + ); + if (!item) return; + if (item.cls === 'items') { + try { + item.itemEffectFn?.(); + } catch { + // pass + } + if (diff.hp > 0) { + statistics.potionCount++; + statistics.potionValue += diff.hp; + } + if (diff.atk > 0 || diff.def > 0 || diff.mdef > 0) { + statistics.gemCount++; + } + if (diff.atk > 0) { + statistics.atkValue += diff.atk; + } + if (diff.def > 0) { + statistics.defValue += diff.def; + } + if (diff.mdef > 0) { + statistics.mdefValue += diff.mdef; + } + } + } + for (const key of Object.keys(diff)) { + diff[key] = 0; + } + }); + floors.set(v, statistics); + }); + + core.status.hero = hero; + window.hero = hero; + window.flags = core.status.hero.flags; + core.removeFlag('__statistics__'); + + const total = floors.values().reduce((prev, curr) => { + prev.atkValue += curr.atkValue; + prev.defValue += curr.defValue; + prev.enemyCount += curr.enemyCount; + prev.gemCount += curr.gemCount; + prev.mdefValue += curr.mdefValue; + prev.potionCount += curr.potionCount; + prev.potionValue += curr.potionValue; + return prev; + }); + + return { + total, + floors + }; +} + +/** + * 打开数据统计界面 + * @param controller 要在哪个 UI 控制器上打开 + */ +export async function openStatistics(controller: IUIMountable) { + const cal = Promise.resolve().then(() => { + return new Promise(res => { + const data = calculateStatistics(); + res(data); + }); + }); + const data = await waitbox( + controller, + [240 + 180, void 0, void 0, 240, 0.5, 0.5], + 240, + cal, + { + text: '正在统计...' + } + ); + controller.open(StatisticsUI, { data: data }); +} + +export const StatisticsUI = new GameUI('statistics', Statistics);