feat: 部分设置菜单

This commit is contained in:
unanmed 2025-03-03 23:21:57 +08:00
parent 0cc76bd475
commit 8cc0c76846
10 changed files with 844 additions and 16 deletions

View File

@ -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>
))
);

View File

@ -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."
}

View File

@ -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,

View File

@ -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);

View 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
);

View File

@ -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;

View File

@ -0,0 +1 @@
export * from './saves';

35
src/module/utils/saves.ts Normal file
View 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
View File

@ -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;
/**
*

View File

@ -46,7 +46,7 @@ interface Events extends EventData {
startGame(
hard: string,
seed?: number,
route?: string,
route?: string[],
callback?: () => void
): void;