feat: 存档自适应数量

This commit is contained in:
unanmed 2025-06-24 15:53:48 +08:00
parent 98a0e13aee
commit 2d301cf0d0
6 changed files with 261 additions and 167 deletions

View File

@ -20,6 +20,8 @@ export interface PageProps extends DefaultProps {
pages: number; pages: number;
/** 页码组件的定位 */ /** 页码组件的定位 */
loc: ElementLocator; loc: ElementLocator;
/** 当前页码 */
page?: number;
/** 页码的字体 */ /** 页码的字体 */
font?: Font; font?: Font;
/** 只有一页的时候,是否隐藏页码 */ /** 只有一页的时候,是否隐藏页码 */
@ -28,6 +30,8 @@ export interface PageProps extends DefaultProps {
export type PageEmits = { export type PageEmits = {
pageChange: (page: number) => void; pageChange: (page: number) => void;
'update:page': (page: number) => void;
}; };
export interface PageExpose { export interface PageExpose {
@ -54,8 +58,8 @@ type PageSlots = SlotsType<{
}>; }>;
const pageProps = { const pageProps = {
props: ['pages', 'loc', 'font', 'hideIfSingle'], props: ['pages', 'loc', 'page', 'font', 'hideIfSingle'],
emits: ['pageChange'] emits: ['pageChange', 'update:page']
} satisfies SetupComponentOptions< } satisfies SetupComponentOptions<
PageProps, PageProps,
PageEmits, PageEmits,
@ -85,7 +89,7 @@ export const Page = defineComponent<
keyof PageEmits, keyof PageEmits,
PageSlots PageSlots
>((props, { slots, expose, emit }) => { >((props, { slots, expose, emit }) => {
const nowPage = ref(0); const nowPage = ref(props.page ?? 0);
// 五个元素的位置 // 五个元素的位置
const leftLoc = ref<ElementLocator>([]); const leftLoc = ref<ElementLocator>([]);
@ -182,6 +186,7 @@ export const Page = defineComponent<
if (nowPage.value !== target) { if (nowPage.value !== target) {
nowPage.value = target; nowPage.value = target;
emit('pageChange', target); emit('pageChange', target);
emit('update:page', target);
} }
}; };

View File

@ -74,6 +74,6 @@ export const Thumbnail = defineComponent<ThumbnailProps>(props => {
watch(props, update); watch(props, update);
return () => ( return () => (
<sprite ref={spriteRef} loc={props.loc} render={drawThumbnail} /> <sprite noanti ref={spriteRef} loc={props.loc} render={drawThumbnail} />
); );
}, thumbnailProps); }, thumbnailProps);

View File

@ -7,22 +7,29 @@ import {
SetupComponentOptions, SetupComponentOptions,
UIComponentProps UIComponentProps
} from '@motajs/system-ui'; } from '@motajs/system-ui';
import { defineComponent, ref, computed, onMounted } from 'vue'; import {
defineComponent,
ref,
computed,
onMounted,
shallowReactive
} from 'vue';
import { Page, PageExpose } from '../components'; import { Page, PageExpose } from '../components';
import { useKey } from '../use'; import { useKey } from '../use';
import { MAP_WIDTH, MAP_HEIGHT } from '../shared'; import { MAP_WIDTH } from '../shared';
import { getSave, SaveData } from '../utils'; import { getSave, SaveData } from '../utils';
import { Thumbnail } from '../components/thumbnail'; import { Thumbnail } from '../components/thumbnail';
import { adjustGrid, IGridLayoutData } from '../utils/layout';
export interface SaveProps extends UIComponentProps, DefaultProps { export interface SaveProps extends UIComponentProps, DefaultProps {
loc: ElementLocator; loc: ElementLocator;
} }
export interface SaveBtnProps extends DefaultProps { export interface SaveItemProps extends DefaultProps {
loc: ElementLocator; loc: ElementLocator;
index: number; index: number;
isSelected: boolean; selected: boolean;
isDelete: boolean; inDelete: boolean;
data: SaveData | null; data: SaveData | null;
} }
@ -35,26 +42,45 @@ export type SaveEmits = {
exit: () => void; exit: () => void;
}; };
export type SaveBtnEmits = {
/** 读取数据 */
updateData: (isValid: boolean) => void;
};
const saveProps = { const saveProps = {
props: ['loc', 'controller', 'instance'], props: ['loc', 'controller', 'instance'],
emits: ['delete', 'emit', 'exit'] emits: ['delete', 'emit', 'exit']
} satisfies SetupComponentOptions<SaveProps, SaveEmits, keyof SaveEmits>; } satisfies SetupComponentOptions<SaveProps, SaveEmits, keyof SaveEmits>;
const saveBtnProps = { const saveBtnProps = {
props: ['loc', 'index', 'isSelected', 'isDelete', 'data'] props: ['loc', 'index', 'selected', 'inDelete', 'data']
} satisfies SetupComponentOptions<SaveBtnProps>; } satisfies SetupComponentOptions<SaveItemProps>;
export const SaveBtn = defineComponent<SaveBtnProps>(props => { export const SaveItem = defineComponent<SaveItemProps>(props => {
const w = props.loc[2] ?? 200;
const font = new Font('normal', 18); const font = new Font('normal', 18);
const statusFont = new Font('normal', 14); 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 === -1 ? '自动存档' : `存档${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(() => { const mapBlocks = computed(() => {
if (props.data === null || props.data === undefined) return void 0; if (!props.data) return [];
else { else {
const currData = props.data.data; const currData = props.data.data;
const map = core.maps.loadMap(currData.maps, currData.floorId); const map = core.maps.loadMap(currData.maps, currData.floorId);
@ -62,54 +88,42 @@ export const SaveBtn = defineComponent<SaveBtnProps>(props => {
return map.blocks; return map.blocks;
} }
}); });
const name = computed(() =>
props.index === -1 ? '自动存档' : `存档${props.index + 1}`
);
const statusText = computed(() => {
if (props.data === null || props.data === undefined) return '';
else {
const hero = props.data.data.hero;
return `${hero.hp}/${hero.atk}/${hero.def}`;
}
});
const strokeStyle = computed(() => {
if (props.isSelected) return props.isDelete ? 'red' : 'gold';
else return 'white';
});
const lineWidth = computed(() => (props.isSelected ? 2 : 1));
return () => ( return () => (
<container loc={props.loc}> <container loc={props.loc}>
<text <text
text={name.value} text={name.value}
font={font} font={font}
loc={[w / 2, 20, void 0, void 0, 0.5, 1]} loc={[w.value / 2, 20]}
anc={[0.5, 1]}
/> />
<g-rect <g-rect
loc={[lineWidth.value, 24, w - 2 * lineWidth.value, w]} loc={imgLoc.value}
fill strokeAndFill
stroke
fillStyle="gray" fillStyle="gray"
strokeStyle={strokeStyle.value} strokeStyle={strokeStyle.value}
lineWidth={lineWidth.value} lineWidth={lineWidth.value}
lineJoin="miter" lineJoin="miter"
cursor="pointer"
/> />
<Thumbnail <Thumbnail
hidden={props.data === null} hidden={!props.data}
loc={[3, 26, w - 6, w - 4]} loc={imgLoc.value}
padStyle="gray" padStyle="gray"
floorId={props.data?.data.floorId || 'MT0'} floorId={floorId.value}
map={mapBlocks.value} map={mapBlocks.value}
hero={props.data?.data.hero as HeroStatus} hero={props.data?.data.hero}
all all
noHD noHD
size={w / MAP_WIDTH} size={(w.value - 4) / MAP_WIDTH}
noevent
/> />
<text <text
text={statusText.value} text={statusText.value}
fillStyle="yellow" fillStyle="yellow"
font={statusFont} font={statusFont}
loc={[w / 2, w + 28, void 0, void 0, 0.5, 0]} loc={[w.value / 2, w.value + 28]}
anc={[0.5, 0]}
/> />
</container> </container>
); );
@ -117,62 +131,108 @@ export const SaveBtn = defineComponent<SaveBtnProps>(props => {
export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>( export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
(props, { emit }) => { (props, { emit }) => {
const row = 2; const itemSize = 150;
const column = 3; const itemHeight = itemSize + 40;
/** 除自动存档外,每一页容纳的存档数量 */ const interval = 30;
const pageCap = row * column - 1;
const font = new Font('normal', 18); const font = new Font('normal', 18);
const pageFont = new Font('normal', 14); const pageFont = new Font('normal', 14);
const isDelete = ref(false); /** 当前页上被选中的存档的posIndex */
const selected = ref(0);
const now = ref(0);
const inDelete = ref(false);
const pageRef = ref<PageExpose>(); const pageRef = ref<PageExpose>();
/** posIndex 存档在当前页的序号 范围为0到pageCap-1 */ 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) => { const getPosIndex = (index: number) => {
if (index === -1) return 0; if (index === -1) return 0;
return index - pageCap * (pageRef.value?.now() || 0) + 1; return index % (grid.value.count - 1);
};
/** index 存档的总序号 从0开始 用于数据交互 */
const getIndex = (posIndex: number, page: number) => {
return page * pageCap + posIndex - 1;
}; };
/** 存档数据的列表 */ /**
const dataList = ref<(SaveData | null)[]>( * 0
Array(pageCap + 1).fill(null) */
); const getIndex = (posIndex: number, page: number) => {
const updateDataList = (page: number) => { return page * grid.value.count + posIndex - 1;
dataList.value.forEach((_ele, i) => { };
getSave(getIndex(i, page)).then(saveValue => {
dataList.value[i] = saveValue; const updateDataList = async (page: number) => {
}); const promises: Promise<SaveData | null>[] = [];
for (let i = 0; i < grid.value.count; i++) {
const index = getIndex(i, page);
promises.push(getSave(index));
}
const data = await Promise.all(promises);
data.forEach((v, i) => {
if (v) {
saveData[i] = v;
} else {
saveData[i] = null;
}
}); });
}; };
const hasData = (posIndex: number) => {
return dataList.value[posIndex] !== null; const exist = (index: number) => {
return saveData[index] !== null;
}; };
const getData = (posIndex: number) => dataList.value[posIndex];
const deleteData = (posIndex: number) => { const deleteData = (index: number) => {
dataList.value[posIndex] = null; saveData[index] = null;
}; };
onMounted(() => { onMounted(() => {
const currPage = pageRef.value?.now() || 0; updateDataList(now.value);
updateDataList(currPage);
}); });
/** 当前页上被选中的存档的posIndex */
const pickIndex = ref(1);
const emitSave = (index: number) => { const emitSave = (index: number) => {
const posIndex = getPosIndex(index); const posIndex = getPosIndex(index);
if (isDelete.value) { if (inDelete.value) {
emit('delete', index, hasData(posIndex)); emit('delete', index, exist(posIndex));
deleteData(posIndex); deleteData(posIndex);
} else { } else {
emit('emit', index, hasData(posIndex)); emit('emit', index, exist(posIndex));
}
if (index === -1) {
selected.value = 0;
} else {
selected.value = (index % (grid.value.count - 1)) + 1;
} }
pickIndex.value = (index % pageCap) + 1;
}; };
const wheel = (ev: IWheelEvent) => { const wheel = (ev: IWheelEvent) => {
@ -185,7 +245,7 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
}; };
const toggleDelete = () => { const toggleDelete = () => {
isDelete.value = !isDelete.value; inDelete.value = !inDelete.value;
}; };
const exit = () => { const exit = () => {
@ -194,11 +254,14 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
}; };
// #region 按键实现 // #region 按键实现
const [key] = useKey(); const [key] = useKey();
key.realize('confirm', () => { key.realize('confirm', () => {
const currPage = pageRef.value?.now(); if (selected.value === 0) {
if (currPage === void 0) return; emitSave(-1);
emitSave(pageCap * currPage + pickIndex.value); } else {
emitSave((grid.value.count - 1) * now.value + selected.value);
}
}) })
.realize('exit', exit) .realize('exit', exit)
.realize('@save_exit', exit) .realize('@save_exit', exit)
@ -220,14 +283,16 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
'@save_up', '@save_up',
() => { () => {
if (!pageRef.value) return; if (!pageRef.value) return;
const now = pageRef.value.now(); const cols = grid.value.cols;
if (pickIndex.value >= row) { const count = grid.value.count;
pickIndex.value -= column; if (selected.value >= cols) {
selected.value -= cols;
} else { } else {
if (now === 0) { if (now.value === 0) {
pickIndex.value = 0; selected.value = 0;
} else { } else {
pickIndex.value += pageCap + 1 - column; const selectedCol = selected.value % cols;
selected.value = count - (cols - selectedCol);
pageRef.value?.movePage(-1); pageRef.value?.movePage(-1);
} }
} }
@ -237,10 +302,13 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
.realize( .realize(
'@save_down', '@save_down',
() => { () => {
if (pickIndex.value <= pageCap - row) { const cols = grid.value.cols;
pickIndex.value += column; const count = grid.value.count;
if (selected.value < count - cols) {
selected.value += cols;
} else { } else {
pickIndex.value += column - pageCap - 1; const selectedCol = selected.value % cols;
selected.value = selectedCol;
pageRef.value?.movePage(1); pageRef.value?.movePage(1);
} }
}, },
@ -250,12 +318,12 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
'@save_left', '@save_left',
() => { () => {
if (!pageRef.value) return; if (!pageRef.value) return;
const now = pageRef.value.now(); const count = grid.value.count;
if (pickIndex.value > 0) { if (selected.value > 0) {
pickIndex.value--; selected.value--;
} else { } else {
if (now > 0) { if (now.value > 0) {
pickIndex.value = pageCap; selected.value = count;
pageRef.value?.movePage(-1); pageRef.value?.movePage(-1);
} }
} }
@ -265,99 +333,62 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
.realize( .realize(
'@save_right', '@save_right',
() => { () => {
if (pickIndex.value < pageCap) { const count = grid.value.count;
pickIndex.value++; if (selected.value < count) {
selected.value++;
} else { } else {
pickIndex.value = 0; selected.value = 0;
pageRef.value?.movePage(1); pageRef.value?.movePage(1);
} }
}, },
{ type: 'down-repeat' } { type: 'down-repeat' }
); );
// #endregion
return () => ( return () => (
<container loc={props.loc} zIndex={10}> <container loc={props.loc} zIndex={10}>
<Page <Page
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT - 10]} ref={pageRef}
loc={[0, 0, width.value, height.value - 10]}
pages={1000} pages={1000}
font={pageFont}
v-model:page={now.value}
onWheel={wheel} onWheel={wheel}
onPageChange={updateDataList} onPageChange={updateDataList}
ref={pageRef}
font={pageFont}
> >
{(page: number) => ( {(page: number) => (
<container loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}> <container loc={contentLoc.value}>
<SaveBtn {grid.value.locs.map((v, i) => {
loc={[30, 50, 120, 170]} const count = grid.value.count;
index={-1} const rawIndex = (count - 1) * page + i;
isSelected={pickIndex.value === 0} const index = i === 0 ? -1 : rawIndex;
isDelete={isDelete.value} return (
onClick={() => emitSave(-1)} <SaveItem
cursor="pointer" loc={v}
data={getData(0)} index={index}
/> selected={selected.value === i}
<SaveBtn inDelete={inDelete.value}
loc={[180, 50, 120, 170]} data={saveData[i]}
index={page * pageCap} onClick={() => emitSave(index)}
isSelected={pickIndex.value === 1} onEnter={() => (selected.value = i)}
isDelete={isDelete.value} />
onClick={() => emitSave(page * pageCap)} );
cursor="pointer" })}
data={getData(1)}
/>
<SaveBtn
loc={[330, 50, 120, 170]}
index={page * pageCap + 1}
isSelected={pickIndex.value === 2}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 1)}
cursor="pointer"
data={getData(2)}
/>
<SaveBtn
loc={[30, 230, 120, 170]}
index={page * pageCap + 2}
isSelected={pickIndex.value === 3}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 2)}
cursor="pointer"
data={getData(3)}
/>
<SaveBtn
loc={[180, 230, 120, 170]}
index={page * pageCap + 3}
isSelected={pickIndex.value === 4}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 3)}
cursor="pointer"
data={getData(4)}
/>
<SaveBtn
loc={[330, 230, 120, 170]}
index={page * pageCap + 4}
isSelected={pickIndex.value === 5}
isDelete={isDelete.value}
onClick={() => emitSave(page * pageCap + 4)}
cursor="pointer"
data={getData(5)}
/>
</container> </container>
)} )}
</Page> </Page>
<text <text
text="删除模式" text="删除模式"
loc={deleteLoc.value}
font={font} font={font}
loc={[30, 450, void 0, void 0, 0, 0]}
zIndex={10} zIndex={10}
fillStyle={isDelete.value ? 'red' : 'white'} fillStyle={inDelete.value ? 'red' : 'white'}
onClick={toggleDelete} onClick={toggleDelete}
cursor="pointer" cursor="pointer"
/> />
<text <text
text="返回游戏" text="返回游戏"
loc={exitLoc.value}
font={font} font={font}
loc={[450, 450, void 0, void 0, 1, 0]}
zIndex={10} zIndex={10}
onClick={exit} onClick={exit}
cursor="pointer" cursor="pointer"
@ -387,9 +418,9 @@ export type SaveValidationFunction = (
* 使 * 使
* ```ts * ```ts
* const index = await selectSave(props.controller, [0, 0, 416, 416]); * const index = await selectSave(props.controller, [0, 0, 416, 416]);
* if (index === -2) { * if (index === -1) {
* // 如果用户未选择存档,而是关闭了存档。 * // 如果用户未选择存档,而是关闭了存档。
* } else if (index === -1) { * } else if (index === 0) {
* // 用户选择了自动存档。 * // 用户选择了自动存档。
* } else { * } else {
* // 用户选择了一个存档。 * // 用户选择了一个存档。
@ -462,7 +493,6 @@ export async function saveSave(
}; };
const index = await selectSave(controller, loc, validate, props); const index = await selectSave(controller, loc, validate, props);
if (index === -2) return false; if (index === -2) return false;
// 由于样板存档编号从1开始这里需要+1
core.doSL(index + 1, 'save'); core.doSL(index + 1, 'save');
return true; return true;
} }

View File

@ -0,0 +1,59 @@
import { ElementLocator } from '@motajs/render-core';
export interface IGridLayoutData {
/** 有多少列 */
readonly cols: number;
/** 有多少行 */
readonly rows: number;
/** 去除余留部分后的宽度 */
readonly width: number;
/** 去除预留部分后的高度 */
readonly height: number;
/** 元素总数量 */
readonly count: number;
/** 每个元素的定位,按照从左到右,从左上下的顺序排列,包含所有六个元素 */
readonly locs: readonly ElementLocator[];
}
/**
*
* @param width
* @param height
* @param itemWidth
* @param itemHeight
* @param intervalX
* @param intervalY
* @returns
*/
export function adjustGrid(
width: number,
height: number,
itemWidth: number,
itemHeight: number,
intervalX: number,
intervalY: number = intervalX
): IGridLayoutData {
const cols = Math.floor((width + intervalX) / (itemWidth + intervalX));
const rows = Math.floor((height + intervalY) / (itemHeight + intervalY));
const rawWidth = (itemWidth + intervalX) * cols - intervalX;
const rawHeight = (itemHeight + intervalY) * rows - intervalY;
const locs: ElementLocator[] = [];
for (let y = 0; y < rows; y++) {
const iy = (intervalY + itemHeight) * y;
for (let x = 0; x < cols; x++) {
const ix = (intervalX + itemWidth) * x;
locs.push([ix, iy, itemWidth, itemHeight, 0, 0]);
}
}
return {
cols,
rows,
count: cols * rows,
width: rawWidth,
height: rawHeight,
locs
};
}

View File

@ -25,7 +25,7 @@ export function getSave(index: number) {
const content = { const content = {
name: core.firstData.name, name: core.firstData.name,
version: core.firstData.version, version: core.firstData.version,
data: data data: data instanceof Array ? data[0] : data
}; };
res(content); res(content);
}); });

View File

@ -821,7 +821,7 @@ interface Animate {
pitch: any; pitch: any;
} }
type Save = DeepReadonly<{ type Save = {
/** /**
* id * id
*/ */
@ -866,7 +866,7 @@ type Save = DeepReadonly<{
* *
*/ */
time: number; time: number;
}>; };
/** /**
* 使 * 使