mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-10-31 20:32:58 +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