HumanBreak/packages-user/client-modules/src/render/ui/save.tsx

381 lines
13 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 { ElementLocator, IWheelEvent } from '@motajs/render-core';
import { DefaultProps } from '@motajs/render-vue';
import { Font } from '@motajs/render';
import {
GameUI,
IUIMountable,
SetupComponentOptions,
UIComponentProps
} from '@motajs/system-ui';
import { defineComponent, ref, computed } from 'vue';
import { Page, PageExpose } from '../components';
import { useKey } from '../use';
import { MAP_WIDTH, MAP_HEIGHT } from '../shared';
export interface SaveProps extends UIComponentProps, DefaultProps {
loc: ElementLocator;
}
export interface SaveBtnProps extends DefaultProps {
loc: ElementLocator;
index: number;
isSelected: boolean;
isDelete: boolean;
}
export type SaveEmits = {
/** 点击存档时触发 */
emit: (index: number, exist: boolean) => void;
/** 删除存档时触发 */
delete: (index: number, exist: boolean) => void;
/** 手动点击退出时触发 */
exit: () => void;
};
const saveProps = {
props: ['loc', 'controller', 'instance'],
emits: ['delete', 'emit', 'exit']
} satisfies SetupComponentOptions<SaveProps, SaveEmits, keyof SaveEmits>;
const saveBtnProps = {
props: ['loc', 'index', 'isSelected', 'isDelete']
} satisfies SetupComponentOptions<SaveBtnProps>;
export const SaveBtn = defineComponent<SaveBtnProps>(props => {
const w = props.loc[2] ?? 200;
const font = new Font('normal', 18);
const statusFont = new Font('normal', 14);
const text = computed(() =>
props.index === -1 ? '自动存档' : `存档${props.index + 1}`
);
const strokeStyle = computed(() => {
if (props.isSelected) return props.isDelete ? 'red' : 'gold';
else return 'white';
});
const lineWidth = computed(() => (props.isSelected ? 2 : 1));
return () => (
<container loc={props.loc}>
<text
text={text.value}
font={font}
loc={[w / 2, 20, void 0, void 0, 0.5, 1]}
/>
<g-rect
loc={[lineWidth.value, 24, w - 2 * lineWidth.value, w]}
fill
stroke
fillStyle="gray"
strokeStyle={strokeStyle.value}
lineWidth={lineWidth.value}
lineJoin="miter"
/>
<text
text="placeholder"
fillStyle="yellow"
font={statusFont}
loc={[w / 2, w + 28, void 0, void 0, 0.5, 0]}
/>
</container>
);
}, saveBtnProps);
export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
(props, { emit }) => {
const row = 2;
const column = 3;
/** 除自动存档外,每一页容纳的存档数量 */
const pageCap = row * column - 1;
const font = new Font('normal', 18);
const pageFont = new Font('normal', 14);
const isDelete = ref(false);
const pageRef = ref<PageExpose>();
/** 当前页上被选中的存档的序号 只会是0到5 */
const pickIndex = ref(1);
const emitSave = (index: number) => {
if (isDelete.value) emit('delete', index);
else emit('emit', index);
pickIndex.value = (index % pageCap) + 1;
};
const wheel = (ev: IWheelEvent) => {
const delta = Math.sign(ev.wheelY);
if (ev.ctrlKey) {
pageRef.value?.movePage(delta * 10);
} else {
pageRef.value?.movePage(delta);
}
};
const toggleDelete = () => {
isDelete.value = !isDelete.value;
};
const exit = () => {
emit('exit');
props.controller.close(props.instance);
};
const [key] = useKey();
key.realize('confirm', () => {
const currPage = pageRef.value?.now();
if (currPage == null) return;
emitSave(pageCap * currPage + pickIndex.value);
})
.realize('exit', exit)
.realize('@save_exit', exit)
.realize(
'@save_pageUp',
() => {
pageRef.value?.movePage(1);
},
{ type: 'down-repeat' }
)
.realize(
'@save_pageDown',
() => {
pageRef.value?.movePage(-1);
},
{ type: 'down-repeat' }
)
.realize(
'@save_up',
() => {
if (!pageRef.value) return;
const now = pageRef.value.now();
if (pickIndex.value >= row) {
pickIndex.value -= column;
} else {
if (now === 0) {
pickIndex.value = 0;
} else {
pickIndex.value += pageCap + 1 - column;
pageRef.value?.movePage(-1);
}
}
},
{ type: 'down-repeat' }
)
.realize(
'@save_down',
() => {
if (pickIndex.value <= pageCap - row)
pickIndex.value += column;
else {
pickIndex.value += column - pageCap - 1;
pageRef.value?.movePage(1);
}
},
{ type: 'down-repeat' }
)
.realize(
'@save_left',
() => {
if (!pageRef.value) return;
const now = pageRef.value.now();
if (pickIndex.value > 0) {
pickIndex.value--;
} else {
if (now > 0) {
pickIndex.value = pageCap;
pageRef.value?.movePage(-1);
}
}
},
{ type: 'down-repeat' }
)
.realize(
'@save_right',
() => {
if (pickIndex.value < pageCap) pickIndex.value++;
else {
pickIndex.value = 0;
pageRef.value?.movePage(1);
}
},
{ type: 'down-repeat' }
);
return () => (
<container loc={props.loc} zIndex={10}>
<Page
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT - 10]}
pages={1000}
onWheel={wheel}
ref={pageRef}
font={pageFont}
>
{(page: number) => (
<container loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}>
<SaveBtn
loc={[30, 50, 120, 170]}
index={-1}
isSelected={pickIndex.value === 0}
isDelete={isDelete.value}
onClick={() => emitSave(-1)}
cursor="pointer"
/>
<SaveBtn
loc={[180, 50, 120, 170]}
index={page * pageCap}
isSelected={pickIndex.value === 1}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap)}
cursor="pointer"
/>
<SaveBtn
loc={[330, 50, 120, 170]}
index={page * pageCap + 1}
isSelected={pickIndex.value === 2}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 1)}
cursor="pointer"
/>
<SaveBtn
loc={[30, 230, 120, 170]}
index={page * pageCap + 2}
isSelected={pickIndex.value === 3}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 2)}
cursor="pointer"
/>
<SaveBtn
loc={[180, 230, 120, 170]}
index={page * pageCap + 3}
isSelected={pickIndex.value === 4}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 3)}
cursor="pointer"
/>
<SaveBtn
loc={[330, 230, 120, 170]}
index={page * pageCap + 4}
isSelected={pickIndex.value === 5}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 4)}
cursor="pointer"
/>
</container>
)}
</Page>
<text
text="删除模式"
font={font}
loc={[30, 450, void 0, void 0, 0, 0]}
zIndex={10}
fillStyle={isDelete.value ? 'red' : 'white'}
onClick={toggleDelete}
cursor="pointer"
/>
<text
text="返回游戏"
font={font}
loc={[450, 450, void 0, void 0, 1, 0]}
zIndex={10}
onClick={exit}
cursor="pointer"
/>
</container>
);
},
saveProps
);
export const SaveUI = new GameUI('save', Save);
export interface SaveValidation {
readonly valid: boolean;
readonly message: string;
}
export type SaveValidationFunction = (
index: number,
exist: boolean
) => SaveValidation;
/**
* 打开存读档界面并让用户选择一个存档。如果用户手动关闭了存档界面,返回 -2否则返回用户选择的存档索引。
* 参数参考 {@link SaveProps},事件不可自定义。
*
* 使用示例:
* ```ts
* const index = await selectSave(props.controller, [0, 0, 416, 416]);
* if (index === -2) {
* // 如果用户未选择存档,而是关闭了存档。
* } else if (index === -1) {
* // 用户选择了自动存档。
* } else {
* // 用户选择了一个存档。
* }
* ```
* @param controller 在哪个控制器上打开
* @param loc 存读档界面的坐标
* @param props 传递给存读档界面的参数
* @returns 选择的存档索引
*/
export function selectSave(
controller: IUIMountable,
loc: ElementLocator,
validate?: SaveValidationFunction,
props?: SaveProps
) {
return new Promise<number>(res => {
const instance = controller.open(SaveUI, {
loc,
...props,
onEmit: (index: number, exist: boolean) => {
if (!validate) {
controller.close(instance);
res(index);
return;
}
const validation = validate(index, exist);
if (validation.valid) {
controller.close(instance);
res(index);
} else {
core.drawTip(validation.message);
}
},
onExit: () => {
res(-2);
}
});
});
}
export async function saveSave(
controller: IUIMountable,
loc: ElementLocator,
props?: SaveProps
) {
const validate = (index: number): SaveValidation => {
if (index === -1) {
return { message: '不能存档至自动存档!', valid: false };
} else {
return { message: '', valid: true };
}
};
const index = await selectSave(controller, loc, validate, props);
if (index === -2) return;
core.doSL(index, 'save');
}
export async function saveLoad(
controller: IUIMountable,
loc: ElementLocator,
props?: SaveProps
) {
const validate = (_: number, exist: boolean): SaveValidation => {
return { message: '无效的存档!', valid: exist };
};
const index = await selectSave(controller, loc, validate, props);
if (index === -2) return;
if (index === -1) {
core.doSL('autosave', 'load');
} else {
core.doSL(index, 'load');
}
}