mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-11 15:47:06 +08:00
feat: 部分设置菜单
This commit is contained in:
parent
0cc76bd475
commit
8cc0c76846
@ -23,7 +23,7 @@ export const UIContainer = defineComponent<UIContainerProps>(props => {
|
||||
controller={data}
|
||||
instance={b}
|
||||
key={b.key}
|
||||
hidden={b.hidden}
|
||||
hidden={b.hidden && !b.alwaysShow}
|
||||
></b.ui.component>
|
||||
);
|
||||
}
|
||||
@ -34,7 +34,7 @@ export const UIContainer = defineComponent<UIContainerProps>(props => {
|
||||
key={v.key}
|
||||
controller={data}
|
||||
instance={v}
|
||||
hidden={v.hidden}
|
||||
hidden={v.hidden && !v.alwaysShow}
|
||||
></v.ui.component>
|
||||
))
|
||||
);
|
||||
|
@ -94,6 +94,7 @@
|
||||
"60": "Repeated Tip id: '$1'.",
|
||||
"61": "Unexpected recursive call of $1.update in render function. Please ensure you have to do this, if you do, ignore this warn.",
|
||||
"62": "Recursive fallback fonts in '$1'.",
|
||||
"63": "Uncaught promise error in waiting box component. Error reason: $1",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
|
||||
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ const confirmBoxProps = {
|
||||
>;
|
||||
|
||||
/**
|
||||
* 确认框组件,与 2.x 的 drawConfirm 类似,可以键盘操作,
|
||||
* 确认框组件,与 2.x 的 drawConfirm 类似,可以键盘操作,单次调用参考 {@link getConfirm}。
|
||||
* 参数参考 {@link ConfirmBoxProps},事件参考 {@link ConfirmBoxEmits},用例如下:
|
||||
* ```tsx
|
||||
* const onYes = () => console.log('yes');
|
||||
@ -243,7 +243,7 @@ const choicesProps = {
|
||||
>;
|
||||
|
||||
/**
|
||||
* 选项框组件,用于在多个选项中选择一个,例如样板的系统设置就由它实现。
|
||||
* 选项框组件,用于在多个选项中选择一个,例如样板的系统设置就由它实现。单次调用参考 {@link getChoice}。
|
||||
* 参数参考 {@link ChoicesProps},事件参考 {@link ChoicesEmits}。用例如下:
|
||||
* ```tsx
|
||||
* <Choices
|
||||
@ -530,12 +530,28 @@ export const Choices = defineComponent<
|
||||
}, choicesProps);
|
||||
|
||||
/**
|
||||
* 弹出一个确认框,然后将确认结果返回
|
||||
* 弹出一个确认框,然后将确认结果返回,例如给玩家弹出一个确认框,并获取玩家是否确认:
|
||||
* ```ts
|
||||
* const confirm = await getConfirm(
|
||||
* // 在哪个 UI 控制器上打开,对于一般 UI 组件来说,直接填写 props.controller 即可
|
||||
* props.controller,
|
||||
* // 确认内容
|
||||
* '确认要 xxx 吗?',
|
||||
* // 确认框的位置,宽度由下一个参数指定,高度参数由组件内部计算得出,指定无效
|
||||
* [240, 240, void 0, void 0, 0.5, 0.5],
|
||||
* // 宽度设为 240
|
||||
* 240,
|
||||
* // 可以给选择框传入其他的 props,例如指定字体,此项可选
|
||||
* { font: new Font('Verdana', 20) }
|
||||
* );
|
||||
* // 之后,就可以直接判断 confirm 来执行不同的操作了
|
||||
* if (confirm) { ... }
|
||||
* ```
|
||||
* @param controller UI 控制器
|
||||
* @param text 确认文本内容
|
||||
* @param loc 确认框的位置
|
||||
* @param width 确认框的宽度
|
||||
* @param props 额外的 props
|
||||
* @param props 额外的 props,参考 {@link ConfirmBoxProps}
|
||||
*/
|
||||
export function getConfirm(
|
||||
controller: IUIMountable,
|
||||
@ -567,12 +583,28 @@ export function getConfirm(
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出一个选择框,然后将选择结果返回
|
||||
* 弹出一个选择框,然后将选择结果返回,例如给玩家弹出一个选择框,并获取玩家选择了哪个:
|
||||
* ```ts
|
||||
* const choice = await getChoice(
|
||||
* // 在哪个 UI 控制器上打开,对于一般 UI 组件来说,直接填写 props.controller 即可
|
||||
* props.controller,
|
||||
* // 选项内容,参考 Choices 的注释
|
||||
* [[0, '选项1'], [1, '选项2'], [2, '选项3']],
|
||||
* // 选择框的位置,宽度由下一个参数指定,高度参数由组件内部计算得出,指定无效
|
||||
* [240, 240, void 0, void 0, 0.5, 0.5],
|
||||
* // 宽度设为 240
|
||||
* 240,
|
||||
* // 可以给选择框传入其他的 props,例如指定标题,此项可选
|
||||
* { title: '选项标题' }
|
||||
* );
|
||||
* // 之后,就可以直接判断 choice 来执行不同的操作了
|
||||
* if (choice === 0) { ... }
|
||||
* ```
|
||||
* @param controller UI 控制器
|
||||
* @param choices 选择框的选项
|
||||
* @param loc 选择框的位置
|
||||
* @param width 选择框的宽度
|
||||
* @param props 额外的 props
|
||||
* @param props 额外的 props,参考 {@link ChoicesProps}
|
||||
*/
|
||||
export function getChoice<T extends ChoiceKey = ChoiceKey>(
|
||||
controller: IUIMountable,
|
||||
|
@ -5,13 +5,15 @@ import {
|
||||
PathProps,
|
||||
Sprite
|
||||
} from '@/core/render';
|
||||
import { computed, defineComponent, ref, watch } from 'vue';
|
||||
import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
|
||||
import { SetupComponentOptions } from './types';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { TextboxProps, TextContent } from './textbox';
|
||||
import { TextboxProps, TextContent, TextContentProps } from './textbox';
|
||||
import { Scroll, ScrollExpose, ScrollProps } from './scroll';
|
||||
import { transitioned } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { GameUI, IUIMountable } from '@/core/system';
|
||||
|
||||
interface ProgressProps extends DefaultProps {
|
||||
/** 进度条的位置 */
|
||||
@ -401,3 +403,181 @@ export const Background = defineComponent<BackgroundProps>(props => {
|
||||
/>
|
||||
);
|
||||
}, backgroundProps);
|
||||
|
||||
export interface WaitBoxProps<T>
|
||||
extends Partial<BackgroundProps>,
|
||||
Partial<TextContentProps> {
|
||||
loc: ElementLocator;
|
||||
width: number;
|
||||
promise?: Promise<T>;
|
||||
text?: string;
|
||||
pad?: number;
|
||||
}
|
||||
|
||||
export type WaitBoxEmits<T> = {
|
||||
resolve: (data: T) => void;
|
||||
};
|
||||
|
||||
export interface WaitBoxExpose<T> {
|
||||
/**
|
||||
* 手动将此组件兑现,注意调用时如果传入的 Promise 还没有兑现,
|
||||
* 当 Promise 兑现后将不会再次触发 resolve 事件,即 resolve 事件只会被触发一次
|
||||
* @param data 兑现值
|
||||
*/
|
||||
resolve(data: T): void;
|
||||
}
|
||||
|
||||
const waitBoxProps = {
|
||||
props: ['promise', 'loc', 'winskin', 'color', 'border'],
|
||||
emits: ['resolve']
|
||||
} satisfies SetupComponentOptions<
|
||||
WaitBoxProps<unknown>,
|
||||
WaitBoxEmits<unknown>,
|
||||
keyof WaitBoxEmits<unknown>
|
||||
>;
|
||||
|
||||
/**
|
||||
* 等待框,可以等待某个异步操作 (Promise),操作完毕后触发兑现事件,单次调用参考 {@link waitbox}。
|
||||
* 参数参考 {@link WaitBoxProps},事件参考 {@link WaitBoxEmits},函数接口参考 {@link WaitBoxExpose}。用例如下:
|
||||
* ```tsx
|
||||
* // 创建一个等待 1000ms 的 Promise,兑现值是等待完毕时的当前时间刻
|
||||
* const promise = new Promise(res => window.setTimeout(() => res(Date.now()), 1000));
|
||||
*
|
||||
* <WaitBox
|
||||
* // 传入要等待的 Promise
|
||||
* promise={promise}
|
||||
* // 等待框的位置,宽度由 width 参数指定,高度由内部计算得来,不需要手动指定,即使手动指定也无效
|
||||
* loc={[240, 240, void 0, void 0, 0.5, 0.5]}
|
||||
* // 等待框的宽度
|
||||
* width={240}
|
||||
* // 完全继承 Background 的参数,因此可以直接指定背景样式
|
||||
* winskin="winskin2.png"
|
||||
* // 完全继承 TextContent 的参数,因此可以直接指定字体
|
||||
* font={new Font('Verdana', 28)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const WaitBox = defineComponent<
|
||||
WaitBoxProps<unknown>,
|
||||
WaitBoxEmits<unknown>,
|
||||
keyof WaitBoxEmits<unknown>
|
||||
>(
|
||||
<T,>(
|
||||
props: WaitBoxProps<T>,
|
||||
{ emit, expose, attrs }: SetupContext<WaitBoxEmits<T>>
|
||||
) => {
|
||||
const contentHeight = ref(200);
|
||||
|
||||
const text = computed(() => props.text ?? '请等待 ...');
|
||||
const pad = computed(() => props.pad ?? 24);
|
||||
const containerLoc = computed<ElementLocator>(() => {
|
||||
const [x = 0, y = 0, , , ax = 0, ay = 0] = props.loc;
|
||||
return [x, y, props.width, contentHeight.value, ax, ay];
|
||||
});
|
||||
const backLoc = computed<ElementLocator>(() => {
|
||||
return [1, 1, props.width - 2, contentHeight.value - 2];
|
||||
});
|
||||
const contentLoc = computed<ElementLocator>(() => {
|
||||
return [
|
||||
pad.value,
|
||||
pad.value,
|
||||
props.width - pad.value * 2,
|
||||
contentHeight.value - pad.value * 2
|
||||
];
|
||||
});
|
||||
|
||||
let resolved: boolean = false;
|
||||
|
||||
props.promise?.then(
|
||||
value => {
|
||||
resolve(value);
|
||||
},
|
||||
reason => {
|
||||
logger.warn(63, reason);
|
||||
}
|
||||
);
|
||||
|
||||
const resolve = (data: T) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
emit('resolve', data);
|
||||
};
|
||||
|
||||
const onContentHeight = (height: number) => {
|
||||
contentHeight.value = height + pad.value * 2;
|
||||
};
|
||||
|
||||
expose<WaitBoxExpose<T>>({ resolve });
|
||||
|
||||
return () => (
|
||||
<container loc={containerLoc.value}>
|
||||
<Background
|
||||
loc={backLoc.value}
|
||||
zIndex={0}
|
||||
winskin={props.winskin}
|
||||
color={props.color}
|
||||
border={props.border}
|
||||
/>
|
||||
<TextContent
|
||||
{...attrs}
|
||||
autoHeight
|
||||
text={text.value}
|
||||
loc={contentLoc.value}
|
||||
width={props.width - pad.value * 2}
|
||||
zIndex={5}
|
||||
onUpdateHeight={onContentHeight}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
},
|
||||
waitBoxProps
|
||||
);
|
||||
|
||||
/**
|
||||
* 打开一个等待框,等待传入的 Promise 兑现后,关闭等待框,并将兑现值返回。
|
||||
* 示例,等待 1000ms:
|
||||
* ```ts
|
||||
* // 创建一个等待 1000ms 的 Promise,兑现值是等待完毕时的当前时间刻
|
||||
* const promise = new Promise(res => window.setTimeout(() => res(Date.now()), 1000));
|
||||
* const value = await waitbox(
|
||||
* // 在哪个 UI 控制器上打开,对于一般 UI 组件来说,直接填写 props.controller 即可
|
||||
* props.controller,
|
||||
* // 确认框的位置,宽度由下一个参数指定,高度参数由组件内部计算得出,指定无效
|
||||
* [240, 240, void 0, void 0, 0.5, 0.5],
|
||||
* // 确认框的宽度
|
||||
* 240,
|
||||
* // 要等待的 Promise
|
||||
* promise,
|
||||
* // 额外的 props,例如填写等待文本,此项可选,参考 WaitBoxProps
|
||||
* { text: '请等待 1000ms' }
|
||||
* );
|
||||
* console.log(value); // 输出时间刻
|
||||
* ```
|
||||
* @param controller UI 控制器
|
||||
* @param loc 等待框的位置
|
||||
* @param width 等待框的宽度
|
||||
* @param promise 要等待的 Promise
|
||||
* @param props 额外的 props,参考 {@link WaitBoxProps}
|
||||
*/
|
||||
export function waitbox<T>(
|
||||
controller: IUIMountable,
|
||||
loc: ElementLocator,
|
||||
width: number,
|
||||
promise: Promise<T>,
|
||||
props?: Partial<WaitBoxProps<T>>
|
||||
): Promise<T> {
|
||||
return new Promise<T>(res => {
|
||||
const instance = controller.open(WaitBoxUI, {
|
||||
...(props ?? {}),
|
||||
loc,
|
||||
width,
|
||||
promise,
|
||||
onResolve: data => {
|
||||
controller.close(instance);
|
||||
res(data as T);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const WaitBoxUI = new GameUI('wait-box', WaitBox);
|
||||
|
579
src/module/render/ui/settings.tsx
Normal file
579
src/module/render/ui/settings.tsx
Normal file
@ -0,0 +1,579 @@
|
||||
import { ElementLocator } from '@/core/render';
|
||||
import { GameUI, UIComponentProps } from '@/core/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
ChoiceItem,
|
||||
ChoiceKey,
|
||||
Choices,
|
||||
ChoicesProps,
|
||||
getConfirm,
|
||||
SetupComponentOptions,
|
||||
waitbox
|
||||
} from '../components';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { generateKeyboardEvent } from '@/core/main/custom/keyboard';
|
||||
import { getVitualKeyOnce } from '@/plugin/utils';
|
||||
import { getAllSavesData, getSaveData } from '@/module/utils';
|
||||
|
||||
export interface SettingsProps extends Partial<ChoicesProps>, UIComponentProps {
|
||||
loc: ElementLocator;
|
||||
}
|
||||
|
||||
const settingsProps = {
|
||||
props: ['loc', 'controller', 'instance']
|
||||
} satisfies SetupComponentOptions<SettingsProps>;
|
||||
|
||||
const enum MainChoice {
|
||||
SystemSetting,
|
||||
VirtualKey,
|
||||
ViewMap,
|
||||
/** @see {@link ReplaySettings} */
|
||||
Replay,
|
||||
/** @see {@link SyncSave} */
|
||||
SyncSave,
|
||||
/** @see {@link GameInfo} */
|
||||
GameInfo,
|
||||
Restart,
|
||||
Back
|
||||
}
|
||||
|
||||
export const MainSettings = defineComponent<SettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[MainChoice.SystemSetting, '系统设置'],
|
||||
[MainChoice.VirtualKey, '虚拟键盘'],
|
||||
[MainChoice.ViewMap, '浏览地图'],
|
||||
[MainChoice.Replay, '录像回放'],
|
||||
[MainChoice.SyncSave, '同步存档'],
|
||||
[MainChoice.GameInfo, '游戏信息'],
|
||||
[MainChoice.Restart, '返回标题'],
|
||||
[MainChoice.Back, '返回游戏']
|
||||
];
|
||||
|
||||
const choose = (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case MainChoice.SystemSetting: {
|
||||
mainUi.open('settings');
|
||||
break;
|
||||
}
|
||||
case MainChoice.VirtualKey: {
|
||||
getVitualKeyOnce().then(value => {
|
||||
gameKey.emitKey(
|
||||
value.key,
|
||||
value.assist,
|
||||
'up',
|
||||
generateKeyboardEvent(value.key, value.assist)
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case MainChoice.ViewMap: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case MainChoice.Replay: {
|
||||
props.controller.open(ReplaySettingsUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case MainChoice.SyncSave: {
|
||||
props.controller.open(SyncSaveUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case MainChoice.GameInfo: {
|
||||
props.controller.open(GameInfoUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case MainChoice.Restart: {
|
||||
props.controller.closeAll();
|
||||
core.restart();
|
||||
break;
|
||||
}
|
||||
case MainChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
choices={choices}
|
||||
width={240}
|
||||
onChoose={choose}
|
||||
/>
|
||||
);
|
||||
}, settingsProps);
|
||||
|
||||
const enum ReplayChoice {
|
||||
Start,
|
||||
StartFromSave,
|
||||
ResumeReplay,
|
||||
ReplayRest,
|
||||
ChooseReplay,
|
||||
Download,
|
||||
Back
|
||||
}
|
||||
|
||||
export const ReplaySettings = defineComponent<SettingsProps>(props => {
|
||||
const choice: ChoiceItem[] = [
|
||||
[ReplayChoice.Start, '从头回放录像'],
|
||||
[ReplayChoice.StartFromSave, '从存档开始回放'],
|
||||
[ReplayChoice.ResumeReplay, '接续播放剩余录像'],
|
||||
[ReplayChoice.ReplayRest, '播放存档剩余录像'],
|
||||
[ReplayChoice.ChooseReplay, '选择录像文件'],
|
||||
[ReplayChoice.Download, '下载当前录像'],
|
||||
[ReplayChoice.Back, '返回游戏']
|
||||
];
|
||||
|
||||
const choose = (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case ReplayChoice.Start: {
|
||||
props.controller.closeAll();
|
||||
core.ui.closePanel();
|
||||
const route = core.status.route.slice();
|
||||
const seed = core.getFlag<number>('__seed__');
|
||||
core.startGame(core.status.hard, seed, route);
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.StartFromSave: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.ResumeReplay: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.ReplayRest: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.ChooseReplay: {
|
||||
props.controller.closeAll();
|
||||
core.chooseReplayFile();
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.Download: {
|
||||
core.download(
|
||||
core.firstData.name + '_' + core.formatDate2() + '.h5route',
|
||||
// @ts-expect-error 暂时无法推导
|
||||
LZString.compressToBase64(
|
||||
JSON.stringify({
|
||||
name: core.firstData.name,
|
||||
hard: core.status.hard,
|
||||
seed: core.getFlag('__seed__'),
|
||||
route: core.encodeRoute(core.status.route)
|
||||
})
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ReplayChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
choices={choice}
|
||||
width={240}
|
||||
onChoose={choose}
|
||||
/>
|
||||
);
|
||||
}, settingsProps);
|
||||
|
||||
const enum GameInfoChoice {
|
||||
Statistics,
|
||||
Project,
|
||||
Tower,
|
||||
Help,
|
||||
Download,
|
||||
Back
|
||||
}
|
||||
|
||||
export const GameInfo = defineComponent<SettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[GameInfoChoice.Statistics, '数据统计'],
|
||||
[GameInfoChoice.Project, '查看工程'],
|
||||
[GameInfoChoice.Tower, '游戏主页'],
|
||||
[GameInfoChoice.Help, '操作帮助'],
|
||||
[GameInfoChoice.Download, '下载离线版本'],
|
||||
[GameInfoChoice.Back, '返回主菜单']
|
||||
];
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case GameInfoChoice.Statistics: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Project: {
|
||||
if (core.platform.isPC) window.open('editor.html', '_blank');
|
||||
else {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'即将离开本游戏,跳转至工程页面,确认跳转?',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
window.location.href = 'editor-mobile.html';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Tower: {
|
||||
const name = core.firstData.name;
|
||||
const href = `/tower/?name=${name}`;
|
||||
if (core.platform.isPC) {
|
||||
window.open(href, '_blank');
|
||||
} else {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'即将离开本游戏,跳转至评论页面,确认跳转?',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Download: {
|
||||
const name = core.firstData.name;
|
||||
const href = `/games/${name}/${name}.zip`;
|
||||
if (core.platform.isPC) window.open(href);
|
||||
else window.location.href = href;
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Help: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case GameInfoChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
choices={choices}
|
||||
width={240}
|
||||
onChoose={choose}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const enum SyncSaveChoice {
|
||||
// ----- 主菜单
|
||||
ToServer,
|
||||
FromServer,
|
||||
ToLocal,
|
||||
FromLocal,
|
||||
ClearLocal,
|
||||
Back,
|
||||
// ----- 子菜单
|
||||
AllSaves,
|
||||
NowSave
|
||||
}
|
||||
|
||||
export const SyncSave = defineComponent<SettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.ToServer, '同步存档至服务器'],
|
||||
[SyncSaveChoice.FromServer, '从服务器加载存档'],
|
||||
[SyncSaveChoice.ToLocal, '存档至本地文件'],
|
||||
[SyncSaveChoice.FromLocal, '存本地文件读档'],
|
||||
[SyncSaveChoice.ClearLocal, '清空本地存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const choose = (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.ToServer: {
|
||||
props.controller.open(SyncSaveSelectUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.FromServer: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.ToLocal: {
|
||||
props.controller.open(DownloadSaveSelectUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.FromLocal: {
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.ClearLocal: {
|
||||
props.controller.open(ClearSaveSelectUI, { loc: props.loc });
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={240}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const SyncSaveSelect = defineComponent<SettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.AllSaves, '同步全部存档'],
|
||||
[SyncSaveChoice.NowSave, '同步当前存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.AllSaves: {
|
||||
core.playSound('confirm.opus');
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'你确定要同步全部存档么?这可能在存档较多的时候比较慢。',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
core.syncSave('all');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.NowSave: {
|
||||
core.playSound('confirm.opus');
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'确定要同步当前存档吗?',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
core.syncSave();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={240}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const DownloadSaveSelect = defineComponent<SettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.AllSaves, '下载全部存档'],
|
||||
[SyncSaveChoice.NowSave, '下载当前存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.AllSaves: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'确认要下载所有存档吗?',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
const data = await waitbox(
|
||||
props.controller,
|
||||
props.loc,
|
||||
240,
|
||||
getAllSavesData(),
|
||||
{ text: '请等待处理完毕' }
|
||||
);
|
||||
core.download(
|
||||
`${core.firstData.name}_${core.formatDate2(new Date())}.h5save`,
|
||||
data
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.NowSave: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'确认要下载当前存档吗?',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
const data = await getSaveData(core.saves.saveIndex);
|
||||
core.download(
|
||||
`${core.firstData.name}_${core.formatDate2(new Date())}.h5save`,
|
||||
data
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={240}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const ClearSaveSelect = defineComponent<SettingsProps>(props => {
|
||||
const choices: ChoiceItem[] = [
|
||||
[SyncSaveChoice.AllSaves, '清空全部塔存档'],
|
||||
[SyncSaveChoice.NowSave, '清空当前塔存档'],
|
||||
[SyncSaveChoice.Back, '返回上一级']
|
||||
];
|
||||
|
||||
const choose = async (key: ChoiceKey) => {
|
||||
switch (key) {
|
||||
case SyncSaveChoice.AllSaves: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'你确定要清除【全部游戏】的所有本地存档?此行为不可逆!!!',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
await waitbox(
|
||||
props.controller,
|
||||
props.loc,
|
||||
240,
|
||||
new Promise<void>(res => {
|
||||
core.clearLocalForage(() => {
|
||||
core.saves.ids = {};
|
||||
core.saves.autosave.data = null;
|
||||
core.saves.autosave.updated = false;
|
||||
core.saves.autosave.now = 0;
|
||||
// @ts-expect-error 沙比样板
|
||||
core.saves.cache = {};
|
||||
core.saves.saveIndex = 1;
|
||||
core.saves.favorite = [];
|
||||
core.saves.favoriteName = {};
|
||||
// @ts-expect-error 沙比样板
|
||||
core.control._updateFavoriteSaves();
|
||||
core.removeLocalStorage('saveIndex');
|
||||
res();
|
||||
});
|
||||
}),
|
||||
{ text: '正在情况,请稍后...' }
|
||||
);
|
||||
await getConfirm(
|
||||
props.controller,
|
||||
'所有塔的存档已经全部清空',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.NowSave: {
|
||||
const confirm = await getConfirm(
|
||||
props.controller,
|
||||
'你确定要清除【当前游戏】的所有本地存档?此行为不可逆!!!',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
if (confirm) {
|
||||
await waitbox(
|
||||
props.controller,
|
||||
props.loc,
|
||||
240,
|
||||
new Promise<void>(res => {
|
||||
Object.keys(core.saves.ids).forEach(function (v) {
|
||||
core.removeLocalForage('save' + v);
|
||||
});
|
||||
core.removeLocalForage('autoSave', () => {
|
||||
core.saves.ids = {};
|
||||
core.saves.autosave.data = null;
|
||||
core.saves.autosave.updated = false;
|
||||
core.saves.autosave.now = 0;
|
||||
core.ui.closePanel();
|
||||
core.saves.saveIndex = 1;
|
||||
core.saves.favorite = [];
|
||||
core.saves.favoriteName = {};
|
||||
// @ts-expect-error 沙比样板
|
||||
core.control._updateFavoriteSaves();
|
||||
core.removeLocalStorage('saveIndex');
|
||||
res();
|
||||
});
|
||||
}),
|
||||
{ text: '正在情况,请稍后...' }
|
||||
);
|
||||
await getConfirm(
|
||||
props.controller,
|
||||
'当前塔的存档已被清空',
|
||||
props.loc,
|
||||
240
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncSaveChoice.Back: {
|
||||
props.controller.close(props.instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Choices
|
||||
loc={props.loc}
|
||||
width={240}
|
||||
choices={choices}
|
||||
onChoose={choose}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/** @see {@link MainSettings} */
|
||||
export const MainSettingsUI = new GameUI('main-settings', MainSettings);
|
||||
/** @see {@link ReplaySettings} */
|
||||
export const ReplaySettingsUI = new GameUI('replay-settings', ReplaySettings);
|
||||
/** @see {@link GameInfo} */
|
||||
export const GameInfoUI = new GameUI('game-info', GameInfo);
|
||||
/** @see {@link SyncSave} */
|
||||
export const SyncSaveUI = new GameUI('sync-save', SyncSave);
|
||||
/** @see {@link SyncSaveSelect} */
|
||||
export const SyncSaveSelectUI = new GameUI('sync-save-select', SyncSaveSelect);
|
||||
/** @see {@link DownloadSaveSelect} */
|
||||
export const DownloadSaveSelectUI = new GameUI(
|
||||
'download-save-select',
|
||||
DownloadSaveSelect
|
||||
);
|
||||
/** @see {@link ClearSaveSelect} */
|
||||
export const ClearSaveSelectUI = new GameUI(
|
||||
'clear-save-select',
|
||||
ClearSaveSelect
|
||||
);
|
@ -1,4 +1,4 @@
|
||||
import { GameUI } from '@/core/system';
|
||||
import { GameUI, UIComponentProps } from '@/core/system';
|
||||
import { computed, defineComponent, ref, watch } from 'vue';
|
||||
import { SetupComponentOptions, TextContent } from '../components';
|
||||
import { DefaultProps, ElementLocator, Sprite, Font } from '@/core/render';
|
||||
@ -34,7 +34,7 @@ export interface ILeftHeroStatus {
|
||||
magicDef: number;
|
||||
}
|
||||
|
||||
interface StatusBarProps<T> extends DefaultProps {
|
||||
interface StatusBarProps<T> extends DefaultProps, UIComponentProps {
|
||||
loc: ElementLocator;
|
||||
status: T;
|
||||
hidden: boolean;
|
||||
|
1
src/module/utils/index.ts
Normal file
1
src/module/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './saves';
|
35
src/module/utils/saves.ts
Normal file
35
src/module/utils/saves.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export function getAllSavesData() {
|
||||
return new Promise<string>(res => {
|
||||
core.getAllSaves(saves => {
|
||||
if (!saves) {
|
||||
res('');
|
||||
return;
|
||||
}
|
||||
const content = {
|
||||
name: core.firstData.name,
|
||||
version: core.firstData.version,
|
||||
data: saves
|
||||
};
|
||||
// @ts-expect-error 暂时无法推导
|
||||
res(LZString.compressToBase64(JSON.stringify(content)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getSaveData(index: number) {
|
||||
return new Promise<string>(res => {
|
||||
core.getSave(index, data => {
|
||||
if (!data) {
|
||||
res('');
|
||||
return;
|
||||
}
|
||||
const content = {
|
||||
name: core.firstData.name,
|
||||
version: core.firstData.version,
|
||||
data: data
|
||||
};
|
||||
// @ts-expect-error 暂时无法推导
|
||||
res(LZString.compressToBase64(JSON.stringify(content)));
|
||||
});
|
||||
});
|
||||
}
|
6
src/types/core.d.ts
vendored
6
src/types/core.d.ts
vendored
@ -657,7 +657,7 @@ interface CoreSave {
|
||||
/**
|
||||
* 自动存档信息
|
||||
*/
|
||||
autosave: Readonly<Autosave>;
|
||||
autosave: Autosave;
|
||||
|
||||
/**
|
||||
* 收藏的存档
|
||||
@ -679,7 +679,7 @@ interface Autosave {
|
||||
/**
|
||||
* 当前存档信息
|
||||
*/
|
||||
data?: Save[];
|
||||
data?: Save[] | null;
|
||||
|
||||
/**
|
||||
* 自动存档位的最大值
|
||||
@ -982,7 +982,7 @@ interface Core extends Pick<Main, CoreDataFromMain> {
|
||||
/**
|
||||
* 存档信息
|
||||
*/
|
||||
readonly saves: Readonly<CoreSave>;
|
||||
readonly saves: CoreSave;
|
||||
|
||||
/**
|
||||
* 全局数值信息
|
||||
|
2
src/types/event.d.ts
vendored
2
src/types/event.d.ts
vendored
@ -46,7 +46,7 @@ interface Events extends EventData {
|
||||
startGame(
|
||||
hard: string,
|
||||
seed?: number,
|
||||
route?: string,
|
||||
route?: string[],
|
||||
callback?: () => void
|
||||
): void;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user