mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-07-19 03:41:47 +08:00
562 lines
18 KiB
TypeScript
562 lines
18 KiB
TypeScript
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<SaveProps, SaveEmits, keyof SaveEmits>;
|
||
|
||
const saveBtnProps = {
|
||
props: ['loc', 'index', 'selected', 'inDelete', 'data']
|
||
} satisfies SetupComponentOptions<SaveItemProps>;
|
||
|
||
export const SaveItem = defineComponent<SaveItemProps>(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<ElementLocator>(() => {
|
||
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 () => (
|
||
<container loc={props.loc}>
|
||
<text
|
||
text={name.value}
|
||
font={font}
|
||
loc={[w.value / 2, 20]}
|
||
anc={[0.5, 1]}
|
||
/>
|
||
<g-rect
|
||
loc={imgLoc.value}
|
||
strokeAndFill
|
||
fillStyle="gray"
|
||
strokeStyle={strokeStyle.value}
|
||
lineWidth={lineWidth.value}
|
||
lineJoin="miter"
|
||
cursor="pointer"
|
||
/>
|
||
<Thumbnail
|
||
hidden={!props.data}
|
||
loc={imgLoc.value}
|
||
padStyle="gray"
|
||
floorId={floorId.value}
|
||
map={mapBlocks.value}
|
||
hero={props.data?.data.hero}
|
||
all
|
||
noHD
|
||
size={(w.value - 4) / MAP_WIDTH}
|
||
noevent
|
||
/>
|
||
<text
|
||
text={statusText.value}
|
||
fillStyle="yellow"
|
||
font={statusFont}
|
||
loc={[w.value / 2, w.value + 28]}
|
||
anc={[0.5, 0]}
|
||
/>
|
||
</container>
|
||
);
|
||
}, saveBtnProps);
|
||
|
||
export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
|
||
(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<PageExpose>();
|
||
|
||
const saveData: Record<number, SaveData | null> = shallowReactive({});
|
||
|
||
const width = computed(() => props.loc[2] ?? 200);
|
||
const height = computed(() => props.loc[3] ?? 200);
|
||
|
||
const grid = computed<IGridLayoutData>(() =>
|
||
adjustGrid(
|
||
width.value,
|
||
height.value - 30,
|
||
itemSize,
|
||
itemHeight,
|
||
interval
|
||
)
|
||
);
|
||
|
||
const contentLoc = computed<ElementLocator>(() => {
|
||
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<ElementLocator>(() => {
|
||
const pad = (width.value - grid.value.width) / 2;
|
||
return [pad, height.value - 13, void 0, void 0, 0, 1];
|
||
});
|
||
|
||
const exitLoc = computed<ElementLocator>(() => {
|
||
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<SaveData | null>[] = [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 () => (
|
||
<container loc={props.loc}>
|
||
<Page
|
||
ref={pageRef}
|
||
loc={[0, 0, width.value, height.value - 10]}
|
||
pages={1000}
|
||
font={pageFont}
|
||
v-model:page={now.value}
|
||
onWheel={wheel}
|
||
onPageChange={updateDataList}
|
||
>
|
||
{(page: number) => (
|
||
<container loc={contentLoc.value}>
|
||
{grid.value.locs.map((v, i) => {
|
||
const count = grid.value.count;
|
||
const rawIndex = (count - 1) * page + i;
|
||
const index = i === 0 ? 0 : rawIndex;
|
||
return (
|
||
<SaveItem
|
||
loc={v}
|
||
index={index}
|
||
selected={selected.value === i}
|
||
inDelete={inDelete.value}
|
||
data={saveData[i]}
|
||
onClick={() => emitSave(index - 1)}
|
||
onEnter={() => (selected.value = i)}
|
||
/>
|
||
);
|
||
})}
|
||
</container>
|
||
)}
|
||
</Page>
|
||
<text
|
||
text="删除模式"
|
||
loc={deleteLoc.value}
|
||
font={font}
|
||
zIndex={10}
|
||
fillStyle={inDelete.value ? 'red' : 'white'}
|
||
onClick={toggleDelete}
|
||
cursor="pointer"
|
||
/>
|
||
<text
|
||
text="返回游戏"
|
||
loc={exitLoc.value}
|
||
font={font}
|
||
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 === -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<number>(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;
|
||
}
|