mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-06-08 00:07:59 +08:00
fear: 统计界面框架
This commit is contained in:
parent
5cd00534c0
commit
86bc383d65
191
packages-user/client-modules/src/render/components/list.tsx
Normal file
191
packages-user/client-modules/src/render/components/list.tsx
Normal file
@ -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<ListProps, ListEmits, keyof ListEmits>;
|
||||
|
||||
export const List = defineComponent<ListProps, ListEmits, keyof ListEmits>(
|
||||
(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 () => (
|
||||
<Scroll>
|
||||
{props.list.map((v, i) => {
|
||||
const [key, value] = v;
|
||||
const loc: ElementLocator = [
|
||||
0,
|
||||
lineHeight.value * i,
|
||||
props.loc[2] ?? 200,
|
||||
lineHeight.value
|
||||
];
|
||||
return (
|
||||
<container onClick={() => select(key)}>
|
||||
{selected.value === key && (
|
||||
<Selection
|
||||
loc={loc}
|
||||
color={props.color}
|
||||
border={props.border}
|
||||
winskin={props.winskin}
|
||||
alphaRange={props.alphaRange}
|
||||
/>
|
||||
)}
|
||||
<text text={value} font={props.font} />
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
);
|
||||
},
|
||||
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<ElementLocator>(() => {
|
||||
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<ElementLocator>(() => {
|
||||
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 () => (
|
||||
<container>
|
||||
<List
|
||||
{...props}
|
||||
loc={listLoc.value}
|
||||
list={props.list}
|
||||
v-model:selected={selected.value}
|
||||
onUpdate={update}
|
||||
></List>
|
||||
<container loc={contentLoc.value}>
|
||||
{slots[selected.value]?.(selected.value) ??
|
||||
slots.default?.(selected.value)}
|
||||
</container>
|
||||
{props.close && (
|
||||
<text text="关闭" cursor="pointer" font={props.font}></text>
|
||||
)}
|
||||
</container>
|
||||
);
|
||||
}, listPageProps);
|
198
packages-user/client-modules/src/render/ui/statistics.tsx
Normal file
198
packages-user/client-modules/src/render/ui/statistics.tsx
Normal file
@ -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<FloorIds, StatisticsDataOneFloor>;
|
||||
}
|
||||
|
||||
export interface StatisticsProps extends UIComponentProps, DefaultProps {
|
||||
data: StatisticsData;
|
||||
}
|
||||
|
||||
const statisticsProps = {
|
||||
props: ['data']
|
||||
} satisfies SetupComponentOptions<StatisticsProps>;
|
||||
|
||||
export const Statistics = defineComponent<StatisticsProps>(props => {
|
||||
const list: [string, string][] = [
|
||||
['total', '总览'],
|
||||
['floor', '楼层'],
|
||||
['enemy', '怪物'],
|
||||
['potion', '血瓶宝石']
|
||||
];
|
||||
|
||||
const close = () => {
|
||||
props.controller.close(props.instance);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<ListPage
|
||||
list={list}
|
||||
selected="total"
|
||||
loc={[180, 0, 480, 480]}
|
||||
close
|
||||
onClose={close}
|
||||
>
|
||||
{{
|
||||
total: () => <TotalStatistics data={props.data} />,
|
||||
floor: () => <FloorStatistics data={props.data} />,
|
||||
enemy: () => <EnemyStatistics data={props.data} />,
|
||||
potion: () => <PotionStatistics data={props.data} />
|
||||
}}
|
||||
</ListPage>
|
||||
);
|
||||
}, statisticsProps);
|
||||
|
||||
interface StatisticsPanelProps extends DefaultProps {
|
||||
data: StatisticsData;
|
||||
}
|
||||
|
||||
const TotalStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => <container></container>;
|
||||
}, statisticsProps);
|
||||
|
||||
const FloorStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => <container></container>;
|
||||
}, statisticsProps);
|
||||
|
||||
const EnemyStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => <container></container>;
|
||||
}, statisticsProps);
|
||||
|
||||
const PotionStatistics = defineComponent<StatisticsPanelProps>(props => {
|
||||
return () => <container></container>;
|
||||
}, statisticsProps);
|
||||
|
||||
function calculateStatistics(): StatisticsData {
|
||||
core.setFlag('__statistics__', true);
|
||||
const hero = core.status.hero;
|
||||
const diff: Record<string | symbol, number> = {};
|
||||
const handler: ProxyHandler<HeroStatus> = {
|
||||
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<FloorIds, StatisticsDataOneFloor>();
|
||||
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<StatisticsData>(() => {
|
||||
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);
|
Loading…
Reference in New Issue
Block a user