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, onMounted, shallowReactive } from 'vue'; import { getConfirm, Page, PageExpose } from '../components'; import { useKey } from '../use'; import { MAP_WIDTH } from '../shared'; import { getSave, SaveData } from '../utils'; import { Thumbnail } from '../components/thumbnail'; import { adjustGrid, IGridLayoutData } from '../utils/layout'; export const enum SaveMode { Save, Load, Other } export interface SaveProps extends UIComponentProps, DefaultProps { loc: ElementLocator; mode: SaveMode; } export interface SaveItemProps extends DefaultProps { loc: ElementLocator; index: number; selected: boolean; inDelete: boolean; data: SaveData | null; } export type SaveEmits = { /** 点击存档时触发 */ emit: (index: number, exist: boolean) => void; /** 删除存档时触发 */ delete: (index: number, exist: boolean) => void; /** 手动点击退出时触发 */ exit: () => void; }; const saveProps = { props: ['loc', 'controller', 'instance', 'mode'], emits: ['delete', 'emit', 'exit'] } satisfies SetupComponentOptions; const saveBtnProps = { props: ['loc', 'index', 'selected', 'inDelete', 'data'] } satisfies SetupComponentOptions; export const SaveItem = defineComponent(props => { const font = new Font('normal', 18); const statusFont = new Font('normal', 14); const w = computed(() => props.loc[2] ?? 200); const lineWidth = computed(() => (props.selected ? 4 : 2)); const imgLoc = computed(() => { const size = w.value - 4; return [2, 24, size, size]; }); const name = computed(() => { return props.index === 0 ? '自动存档' : `存档${props.index}`; }); const statusText = computed(() => { if (!props.data) return ''; else { const hero = props.data.data.hero; return `${hero.hp}/${hero.atk}/${hero.def}`; } }); const strokeStyle = computed(() => { if (props.selected) return props.inDelete ? 'red' : 'gold'; else return 'white'; }); const floorId = computed(() => props.data?.data.floorId ?? 'empty'); const mapBlocks = computed(() => { if (!props.data) return []; else { const currData = props.data.data; const map = core.maps.loadMap(currData.maps, currData.floorId); core.extractBlocksForUI(map, currData.hero.flags); // 这一步会向map写入blocks return map.blocks; } }); return () => ( ); }, saveBtnProps); export const Save = defineComponent( (props, { emit }) => { const itemSize = 150; const itemHeight = itemSize + 40; const interval = 30; const font = new Font('normal', 18); const pageFont = new Font('normal', 14); /** 当前页上被选中的存档的posIndex */ const selected = ref(0); const now = ref(0); const inDelete = ref(false); const pageRef = ref(); const saveData: Record = shallowReactive({}); const width = computed(() => props.loc[2] ?? 200); const height = computed(() => props.loc[3] ?? 200); const grid = computed(() => adjustGrid( width.value, height.value - 30, itemSize, itemHeight, interval ) ); const contentLoc = computed(() => { const cx = width.value / 2; const cy = (height.value - 30) / 2; return [cx, cy, grid.value.width, grid.value.height, 0.5, 0.5]; }); const deleteLoc = computed(() => { const pad = (width.value - grid.value.width) / 2; return [pad, height.value - 13, void 0, void 0, 0, 1]; }); const exitLoc = computed(() => { const pad = (width.value - grid.value.width) / 2; const right = width.value - pad; return [right, height.value - 13, void 0, void 0, 1, 1]; }); /** * 获取存档在当前页的序号,范围为 0 到 pageCap-1。 */ const getPosIndex = (index: number) => { if (index === -1) return 0; return (index % (grid.value.count - 1)) + 1; }; /** * 获取存档的总序号,从 0 开始,用于数据交互。 */ const getIndex = (posIndex: number, page: number) => { return page * (grid.value.count - 1) + posIndex - 1; }; const updateDataList = async (page: number) => { const promises: Promise[] = [getSave(0)]; for (let i = 1; i < grid.value.count; i++) { const index = getIndex(i, page); promises.push(getSave(index + 1)); } const before = now.value; const data = await Promise.all(promises); if (before !== now.value) return; data.forEach((v, i) => { if (v) { saveData[i] = v; } else { saveData[i] = null; } }); }; const exist = (index: number) => { return saveData[index] !== null; }; const deleteData = (index: number) => { saveData[index] = null; }; onMounted(() => { const startIndex = getPosIndex(core.saves.saveIndex); selected.value = startIndex; pageRef.value?.changePage( Math.floor(core.saves.saveIndex / (grid.value.count - 1)) ); updateDataList(now.value); }); const emitSave = async (index: number) => { const posIndex = getPosIndex(index); if (inDelete.value) { const confirm = await getConfirm( props.controller, `确认要删除存档 ${index + 1}?`, [420, 240, void 0, void 0, 0.5, 0.5], 240, { winskin: 'winskin2.png' } ); if (confirm) { emit('delete', index, exist(posIndex)); deleteData(posIndex); } } else { emit('emit', index, exist(posIndex)); } if (index === -1) { selected.value = 0; } else { selected.value = (index % (grid.value.count - 1)) + 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 = () => { inDelete.value = !inDelete.value; }; const exit = () => { emit('exit'); props.controller.close(props.instance); }; // #region 按键实现 const [key] = useKey(); key.realize('confirm', () => { if (selected.value === 0) { emitSave(-1); } else { emitSave( (grid.value.count - 1) * now.value + selected.value - 1 ); } }) .realize('exit', exit) .realize('save', () => { if (props.mode === SaveMode.Save) exit(); }) .realize('load', () => { if (props.mode === SaveMode.Load) 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 cols = grid.value.cols; const count = grid.value.count; if (selected.value >= cols) { selected.value -= cols; } else { if (now.value === 0) { selected.value = 0; } else { const selectedCol = selected.value % cols; selected.value = count - (cols - selectedCol); pageRef.value?.movePage(-1); } } }, { type: 'down-repeat' } ) .realize( '@save_down', () => { const cols = grid.value.cols; const count = grid.value.count; if (selected.value < count - cols) { selected.value += cols; } else { const selectedCol = selected.value % cols; selected.value = selectedCol; pageRef.value?.movePage(1); } }, { type: 'down-repeat' } ) .realize( '@save_left', () => { if (!pageRef.value) return; const count = grid.value.count; if (selected.value > 0) { selected.value--; } else { if (now.value > 0) { selected.value = count; pageRef.value?.movePage(-1); } } }, { type: 'down-repeat' } ) .realize( '@save_right', () => { const count = grid.value.count; if (selected.value < count) { selected.value++; } else { selected.value = 0; pageRef.value?.movePage(1); } }, { type: 'down-repeat' } ); return () => ( {(page: number) => ( {grid.value.locs.map((v, i) => { const count = grid.value.count; const rawIndex = (count - 1) * page + i; const index = i === 0 ? 0 : rawIndex; return ( emitSave(index - 1)} onEnter={() => (selected.value = i)} /> ); })} )} ); }, 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 === -1) { * // 如果用户未选择存档,而是关闭了存档。 * } else if (index === 0) { * // 用户选择了自动存档。 * } else { * // 用户选择了一个存档。 * } * ``` * @param controller 在哪个控制器上打开 * @param loc 存读档界面的坐标 * @param props 传递给存读档界面的参数 * @returns 选择的存档索引 */ export function selectSave( controller: IUIMountable, loc: ElementLocator, mode: SaveMode, validate?: SaveValidationFunction, props?: SaveProps ) { const validateDelete = (index: number, exist: boolean): SaveValidation => { if (index === -1) { return { message: '不能删除自动存档!', valid: false }; } else { return { message: '无法删除该存档!', valid: exist }; } }; return new Promise(res => { const instance = controller.open(SaveUI, { loc, ...props, mode, 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); } }, onDelete: (index: number, exist: boolean) => { if (!validate) return; const validation = validateDelete(index, exist); if (validation.valid) { core.removeSave(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, SaveMode.Save, validate, props ); if (index === -2) return false; core.saves.saveIndex = index; core.doSL(index + 1, 'save'); return true; } 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, SaveMode.Load, validate, props ); if (index === -2) return false; if (index === -1) { core.doSL('autoSave', 'load'); } else { core.doSL(index + 1, 'load'); } return true; }