From 46cad94aff8fa7f3004930b9eee14db7033423d4 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 5 Oct 2026 19:44:33 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20UI=20=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client-base/src/components/choices.tsx | 10 +- .../client-base/src/components/input.tsx | 6 +- .../client-base/src/components/misc.tsx | 4 +- packages-user/client-base/src/types.ts | 25 +- packages-user/client-modules/src/client.ts | 19 +- .../src/render/ui/controller.tsx | 5 +- .../client-modules/src/render/ui/load.tsx | 4 +- .../client-modules/src/render/ui/save.tsx | 14 +- .../client-modules/src/render/ui/settings.tsx | 10 +- .../src/render/ui/statistics.tsx | 8 +- .../client-modules/src/render/ui/title.tsx | 4 +- .../client-modules/src/render/ui/viewmap.tsx | 8 +- .../client-modules/src/render/utils/saves.ts | 4 +- packages-user/client-modules/src/types.ts | 18 +- .../client-modules/src/ui/background.tsx | 21 + packages-user/client-modules/src/ui/index.ts | 1 + packages-user/client-modules/src/ui/load.tsx | 157 ++++++ packages-user/client-modules/src/ui/title.tsx | 484 ++++++++++++++++++ packages-user/client-modules/src/ui/types.ts | 5 + packages/system/src/ui/container.tsx | 15 +- packages/system/src/ui/controller.ts | 372 ++++++-------- packages/system/src/ui/index.ts | 1 - packages/system/src/ui/instance.ts | 50 +- packages/system/src/ui/shared.ts | 133 ----- packages/system/src/ui/types.ts | 232 ++++++++- packages/system/src/ui/ui.ts | 5 +- 26 files changed, 1164 insertions(+), 451 deletions(-) create mode 100644 packages-user/client-modules/src/ui/background.tsx create mode 100644 packages-user/client-modules/src/ui/index.ts create mode 100644 packages-user/client-modules/src/ui/load.tsx create mode 100644 packages-user/client-modules/src/ui/title.tsx create mode 100644 packages-user/client-modules/src/ui/types.ts delete mode 100644 packages/system/src/ui/shared.ts diff --git a/packages-user/client-base/src/components/choices.tsx b/packages-user/client-base/src/components/choices.tsx index 8a1c8b6..cb03a85 100644 --- a/packages-user/client-base/src/components/choices.tsx +++ b/packages-user/client-base/src/components/choices.tsx @@ -4,7 +4,7 @@ import { Background, Selection } from './misc'; import { TextContent, TextContentProps } from './textbox'; import { TextAlign } from './textboxTyper'; import { Page, PageExpose } from './page'; -import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system'; +import { GameUI, IUIController, SetupComponentOptions } from '@motajs/system'; // @ts-expect-error render/use.ts 按 D-54 留在实现层,本文件已移入系统层,按 D-68 暂以标注记录,不反向引用、不解耦 import { useKey } from '../use'; import { sleep } from 'mutate-animate'; @@ -595,7 +595,7 @@ export const Choices = defineComponent< * @param props 额外的 props,参考 {@link ConfirmBoxProps} */ export function getConfirm( - controller: IUIMountable, + controller: IUIController, text: string, loc: ElementLocator, width: number, @@ -648,7 +648,7 @@ export function getConfirm( * @param props 额外的 props,参考 {@link ChoicesProps} */ export function getChoice( - controller: IUIMountable, + controller: IUIController, choices: ChoiceItem[], loc: ElementLocator, width: number, @@ -707,7 +707,7 @@ function getChoiceRoute(defaults: number) { * @param props 额外的 props,参考 {@link ConfirmBoxProps} */ export async function routedConfirm( - controller: IUIMountable, + controller: IUIController, text: string, loc: ElementLocator, width: number, @@ -762,7 +762,7 @@ export async function routedConfirm( * @param props 额外的 props,参考 {@link ChoicesProps} */ export async function routedChoices( - controller: IUIMountable, + controller: IUIController, choices: ChoiceItem[], loc: ElementLocator, width: number, diff --git a/packages-user/client-base/src/components/input.tsx b/packages-user/client-base/src/components/input.tsx index e133de8..bc71d08 100644 --- a/packages-user/client-base/src/components/input.tsx +++ b/packages-user/client-base/src/components/input.tsx @@ -15,7 +15,7 @@ import { import { transitionedColor, useKey } from '../use'; import { linear } from 'mutate-animate'; import { Background, Selection } from './misc'; -import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system'; +import { GameUI, IUIController, SetupComponentOptions } from '@motajs/system'; import { KeyCode } from '@motajs/client-base'; export interface InputProps extends DefaultProps, Partial { @@ -544,7 +544,7 @@ export const InputBox = defineComponent< * @param props 额外的 props,参考 {@link ConfirmBoxProps} */ export function getInput( - controller: IUIMountable, + controller: IUIController, text: string, loc: ElementLocator, width: number, @@ -581,7 +581,7 @@ export function getInput( * @param props 额外的 props,参考 {@link ConfirmBoxProps} */ export async function getInputNumber( - controller: IUIMountable, + controller: IUIController, text: string, loc: ElementLocator, width: number, diff --git a/packages-user/client-base/src/components/misc.tsx b/packages-user/client-base/src/components/misc.tsx index a4f9dcf..48dfa81 100644 --- a/packages-user/client-base/src/components/misc.tsx +++ b/packages-user/client-base/src/components/misc.tsx @@ -10,7 +10,7 @@ import { Scroll, ScrollExpose, ScrollProps } from './scroll'; // @ts-expect-error render/use.ts 按 D-54 留在实现层,本文件已移入系统层,按 D-68 暂以标注记录,不反向引用、不解耦 import { transitioned } from '../use'; import { logger } from '@motajs/common'; -import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system'; +import { GameUI, IUIController, SetupComponentOptions } from '@motajs/system'; import { clamp } from 'lodash-es'; // @ts-expect-error render/renderer 按 D-54 留在实现层,本文件已移入系统层,按 D-59 暂以标注记录,不反向引用、不解耦 import { using } from '../renderer'; @@ -568,7 +568,7 @@ export const WaitBox = defineComponent< * @param props 额外的 props,参考 {@link WaitBoxProps} */ export function waitbox( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, width: number, promise: Promise, diff --git a/packages-user/client-base/src/types.ts b/packages-user/client-base/src/types.ts index 2ba5691..f57eb3c 100644 --- a/packages-user/client-base/src/types.ts +++ b/packages-user/client-base/src/types.ts @@ -1,9 +1,12 @@ -import { IBGMPlayer, IMotaAudioContext, ISoundPlayer } from '@motajs/audio'; -import { ISaveSystem } from './save'; -import { ICoreState } from '@user/data-state'; -import { ITextureManager } from './material'; -import { IRenderTreeRoot } from '@motajs/render'; import { IExcitation, IExcitationDivider } from '@motajs/animate'; +import { IRenderTreeRoot } from '@motajs/render'; +import { IRendererUsing } from '@motajs/render-vue'; +import { UIController } from '@motajs/system'; +import { IBGMPlayer, IMotaAudioContext, ISoundPlayer } from '@motajs/audio'; +import { ICoreState } from '@user/data-state'; +import { ISaveSystem } from './save'; +import { ITextureManager } from './material'; +import { IMapRenderer, IMapExtensionManager } from './map'; export interface IClientBase extends ICoreState { /** 存档系统 */ @@ -22,6 +25,18 @@ export interface IClientBase extends ICoreState { readonly rafExcitation: IExcitation; /** 用于渲染系统的激励源分频器 */ readonly excitationDivider: IExcitationDivider; + /** 主地图渲染器,主要用于渲染游戏画面中的地图 */ + readonly mainMapRenderer: IMapRenderer; + /** 副地图渲染器,主要用于渲染缩略图、浏览地图等内容 */ + readonly expandMapRenderer: IMapRenderer; + /** 主地图渲染器的拓展管理对象 */ + readonly mainMapExtension: IMapExtensionManager; + /** 渲染器使用对象,供组件内注册渲染激励与事件监听 */ + readonly using: IRendererUsing; + /** 主场景的 UI 控制器,管理游戏主场景的 UI 显示 */ + readonly sceneController: UIController; + /** 主界面的 UI 控制器,管理游戏主界面的 UI 显示 */ + readonly mainUIController: UIController; } export interface IClientBaseExtended { diff --git a/packages-user/client-modules/src/client.ts b/packages-user/client-modules/src/client.ts index af049c8..463e0e3 100644 --- a/packages-user/client-modules/src/client.ts +++ b/packages-user/client-modules/src/client.ts @@ -39,6 +39,8 @@ import { import { WebLoadStarter } from '@motajs/loader'; import { IRendererUsing, RendererUsing } from '@motajs/render-vue'; import { UIController } from '@motajs/system'; +import { createApp } from './render'; +import { LoadSceneUI } from './render/ui/load'; export class ClientCore extends CoreState implements IClientCore { // Layer 4 渲染基础层 @@ -50,14 +52,15 @@ export class ClientCore extends CoreState implements IClientCore { readonly excitationDivider: IExcitationDivider; readonly renderer: IRenderTreeRoot; readonly materials: ITextureManager; - - // Layer 5 渲染顶层 readonly using: IRendererUsing; readonly mainMapRenderer: IMapRenderer; + readonly expandMapRenderer: IMapRenderer; readonly mainMapExtension: IMapExtensionManager; readonly sceneController: UIController; readonly mainUIController: UIController; + // Layer 5 渲染顶层 + constructor(config: IClientCoreConfig) { super({ loadStarter: new WebLoadStarter(), @@ -91,6 +94,8 @@ export class ClientCore extends CoreState implements IClientCore { const rafExcitation = new RafExcitation(); const excitationDivider = new ExcitationDivider(); + this.rafExcitation = rafExcitation; + this.excitationDivider = excitationDivider; if (DEBUG_VARIATOR) { const variator = new ExcitationVariator(); @@ -105,8 +110,6 @@ export class ClientCore extends CoreState implements IClientCore { excitationDivider.setDivider(DIVIDER_DEBUG_DIVIDER); } - this.rafExcitation = rafExcitation; - this.excitationDivider = excitationDivider; this.renderer = new MotaRenderer({ canvas: '#render-main', width: MAIN_WIDTH, @@ -117,12 +120,20 @@ export class ClientCore extends CoreState implements IClientCore { this.using = new RendererUsing(this.renderer); this.mainMapRenderer = new MapRenderer(this.materials); this.mainMapExtension = new MapExtensionManager(this.mainMapRenderer); + this.expandMapRenderer = new MapRenderer(this.materials); this.sceneController = new UIController('main-scene'); this.mainUIController = new UIController('main-ui'); //#endregion this.loader.addCoreConfig('client', config.clientURL); + + this.init(); + } + + private async init() { + createApp(this.sceneController.render()).mount(this.renderer); + this.sceneController.open(LoadSceneUI, {}); } /** diff --git a/packages-user/client-modules/src/render/ui/controller.tsx b/packages-user/client-modules/src/render/ui/controller.tsx index 6c97e12..dcb1ef9 100644 --- a/packages-user/client-modules/src/render/ui/controller.tsx +++ b/packages-user/client-modules/src/render/ui/controller.tsx @@ -2,7 +2,7 @@ import { DefaultProps } from '@motajs/render-vue'; import { GameUI, SetupComponentOptions, - UIComponentProps, + IUIDefaultPropsBase, UIController } from '@motajs/system'; import { defineComponent } from 'vue'; @@ -10,7 +10,8 @@ import { MAIN_HEIGHT, MAIN_WIDTH } from '../../shared'; export const mainUIController = new UIController('main-ui'); -export interface MainBackgroundProps extends DefaultProps, UIComponentProps {} +export interface MainBackgroundProps + extends DefaultProps, IUIDefaultPropsBase {} const mainBackgroundProps = { props: ['controller', 'instance'] diff --git a/packages-user/client-modules/src/render/ui/load.tsx b/packages-user/client-modules/src/render/ui/load.tsx index a260c04..73815ca 100644 --- a/packages-user/client-modules/src/render/ui/load.tsx +++ b/packages-user/client-modules/src/render/ui/load.tsx @@ -2,7 +2,7 @@ import { DefaultProps } from '@motajs/render-vue'; import { GameUI, SetupComponentOptions, - UIComponentProps + IUIDefaultPropsBase } from '@motajs/system'; import { defineComponent } from 'vue'; import { @@ -26,7 +26,7 @@ import { sleep } from '@motajs/common'; import { GameTitleUI } from './title'; import { client } from '../../core'; -export interface ILoadProps extends UIComponentProps, DefaultProps {} +export interface ILoadProps extends IUIDefaultPropsBase, DefaultProps {} const loadSceneProps = { props: ['controller', 'instance'] diff --git a/packages-user/client-modules/src/render/ui/save.tsx b/packages-user/client-modules/src/render/ui/save.tsx index e02cf21..ce770b2 100644 --- a/packages-user/client-modules/src/render/ui/save.tsx +++ b/packages-user/client-modules/src/render/ui/save.tsx @@ -2,9 +2,9 @@ import { ElementLocator, IWheelEvent, Font } from '@motajs/render'; import { DefaultProps } from '@motajs/render-vue'; import { GameUI, - IUIMountable, + IUIController, SetupComponentOptions, - UIComponentProps + IUIDefaultPropsBase } from '@motajs/system'; import { defineComponent, @@ -37,7 +37,7 @@ export const enum SaveMode { Other } -export interface SaveProps extends UIComponentProps, DefaultProps { +export interface SaveProps extends IUIDefaultPropsBase, DefaultProps { loc: ElementLocator; mode: SaveMode; } @@ -492,7 +492,7 @@ export type SaveValidationFunction = ( * @returns 选择的存档索引 */ export function selectSave( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, mode: SaveMode, validate?: SaveValidationFunction, @@ -542,7 +542,7 @@ export function selectSave( } export async function saveSave( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, props?: SaveProps ) { @@ -566,7 +566,7 @@ export async function saveSave( } export async function saveLoad( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, props?: SaveProps ) { @@ -590,7 +590,7 @@ export async function saveLoad( } export async function saveWithExist( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, props?: SaveProps ) { diff --git a/packages-user/client-modules/src/render/ui/settings.tsx b/packages-user/client-modules/src/render/ui/settings.tsx index 3299281..74ff354 100644 --- a/packages-user/client-modules/src/render/ui/settings.tsx +++ b/packages-user/client-modules/src/render/ui/settings.tsx @@ -1,9 +1,9 @@ import { ElementLocator } from '@motajs/render'; import { GameUI, - IUIMountable, + IUIController, SetupComponentOptions, - UIComponentProps + IUIDefaultPropsBase } from '@motajs/system'; import { defineComponent } from 'vue'; import { @@ -28,7 +28,7 @@ import { useKey } from '../use'; import { client } from '../../core'; export interface MainSettingsProps - extends Partial, UIComponentProps { + extends Partial, IUIDefaultPropsBase { loc: ElementLocator; } @@ -681,7 +681,7 @@ export const ClearSaveSelectUI = new GameUI( ); export function openSettings( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, props?: MainSettingsProps ) { @@ -692,7 +692,7 @@ export function openSettings( } export function openReplay( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, props?: MainSettingsProps ) { diff --git a/packages-user/client-modules/src/render/ui/statistics.tsx b/packages-user/client-modules/src/render/ui/statistics.tsx index 801e85b..89f1ea6 100644 --- a/packages-user/client-modules/src/render/ui/statistics.tsx +++ b/packages-user/client-modules/src/render/ui/statistics.tsx @@ -1,8 +1,8 @@ import { GameUI, - IUIMountable, + IUIController, SetupComponentOptions, - UIComponentProps + IUIDefaultPropsBase } from '@motajs/system'; import { defineComponent } from 'vue'; import { waitbox, ListPage, TextContent } from '@user/client-base'; @@ -38,7 +38,7 @@ export interface StatisticsData { potionGem: StatisticsDataPotionGem; } -export interface StatisticsProps extends UIComponentProps, DefaultProps { +export interface StatisticsProps extends IUIDefaultPropsBase, DefaultProps { data: StatisticsData; } @@ -280,7 +280,7 @@ export function calculateStatistics(): StatisticsData { * 打开数据统计界面 * @param controller 要在哪个 UI 控制器上打开 */ -export async function openStatistics(controller: IUIMountable) { +export async function openStatistics(controller: IUIController) { const cal = Promise.resolve().then(() => { return new Promise(res => { const data = calculateStatistics(); diff --git a/packages-user/client-modules/src/render/ui/title.tsx b/packages-user/client-modules/src/render/ui/title.tsx index 21bc68f..e54949d 100644 --- a/packages-user/client-modules/src/render/ui/title.tsx +++ b/packages-user/client-modules/src/render/ui/title.tsx @@ -2,7 +2,7 @@ import { DefaultProps } from '@motajs/render-vue'; import { GameUI, SetupComponentOptions, - UIComponentProps + IUIDefaultPropsBase } from '@motajs/system'; import { defineComponent, nextTick, onMounted, ref } from 'vue'; import { @@ -57,7 +57,7 @@ interface ButtonOption { colorTrans: ITransitionedController; } -export interface GameTitleProps extends DefaultProps, UIComponentProps {} +export interface GameTitleProps extends DefaultProps, IUIDefaultPropsBase {} const gameTitleProps = { props: ['controller', 'instance'] diff --git a/packages-user/client-modules/src/render/ui/viewmap.tsx b/packages-user/client-modules/src/render/ui/viewmap.tsx index 8b023ea..480b2ea 100644 --- a/packages-user/client-modules/src/render/ui/viewmap.tsx +++ b/packages-user/client-modules/src/render/ui/viewmap.tsx @@ -9,9 +9,9 @@ import { import { BaseProps } from '@motajs/render-vue'; import { GameUI, - IUIMountable, + IUIController, SetupComponentOptions, - UIComponentProps + IUIDefaultPropsBase } from '@motajs/system'; import { computed, @@ -41,7 +41,7 @@ import { STATUS_BAR_WIDTH } from '../../shared'; -export interface ViewMapProps extends UIComponentProps, BaseProps { +export interface ViewMapProps extends IUIDefaultPropsBase, BaseProps { loc: ElementLocator; floorId?: FloorIds; } @@ -556,7 +556,7 @@ export const ViewMap = defineComponent(props => { export const ViewMapUI = new GameUI('view-map', ViewMap); export function openViewMap( - controller: IUIMountable, + controller: IUIController, loc: ElementLocator, props?: ViewMapProps ) { diff --git a/packages-user/client-modules/src/render/utils/saves.ts b/packages-user/client-modules/src/render/utils/saves.ts index 6486692..3037e88 100644 --- a/packages-user/client-modules/src/render/utils/saves.ts +++ b/packages-user/client-modules/src/render/utils/saves.ts @@ -1,6 +1,6 @@ import { compressToBase64, decompressFromBase64 } from 'lz-string'; import { getConfirm, waitbox } from '@user/client-base'; -import { IUIMountable } from '@motajs/system'; +import { IUIController } from '@motajs/system'; import { SyncSaveFromServerResponse } from '@motajs/client-base'; import { CENTER_LOC, POP_BOX_WIDTH } from '../../shared'; @@ -122,7 +122,7 @@ async function syncLoad(id: string, password: string) { } export async function syncFromServer( - controller: IUIMountable, + controller: IUIController, identifier: string ): Promise { if (!/^\d{6}\w{4}$/.test(identifier) && !/^\d{4}\w{3}$/.test(identifier)) { diff --git a/packages-user/client-modules/src/types.ts b/packages-user/client-modules/src/types.ts index e54d84c..4562807 100644 --- a/packages-user/client-modules/src/types.ts +++ b/packages-user/client-modules/src/types.ts @@ -1,7 +1,4 @@ import { IClientSystem } from '@user/client-system'; -import { IMapExtensionManager, IMapRenderer } from '@user/client-base'; -import { IRendererUsing } from '@motajs/render-vue'; -import { UIController } from '@motajs/system'; export interface IClientCoreConfig { /** 渲染端数据配置文件路径,相对于 `src/content` */ @@ -12,17 +9,4 @@ export interface IClientCoreConfig { readonly tilesetUnit: number; } -export interface IClientCore extends IClientSystem { - /** 主地图渲染器,主要用于渲染游戏画面中的地图 */ - readonly mainMapRenderer: IMapRenderer; - /** 副地图渲染器,主要用于渲染缩略图、浏览地图等内容 */ - // readonly expandMapRenderer: IMapRenderer; - /** 主地图渲染器的拓展管理对象 */ - readonly mainMapExtension: IMapExtensionManager; - /** 渲染器使用对象,供组件内注册渲染激励与事件监听 */ - readonly using: IRendererUsing; - /** 主场景的 UI 控制器,管理游戏主场景的 UI 显示 */ - readonly sceneController: UIController; - /** 主界面的 UI 控制器,管理游戏主界面的 UI 显示 */ - readonly mainUIController: UIController; -} +export interface IClientCore extends IClientSystem {} diff --git a/packages-user/client-modules/src/ui/background.tsx b/packages-user/client-modules/src/ui/background.tsx new file mode 100644 index 0000000..6691df4 --- /dev/null +++ b/packages-user/client-modules/src/ui/background.tsx @@ -0,0 +1,21 @@ +import { GameUI, SetupComponentOptions } from '@motajs/system'; +import { defineComponent } from 'vue'; +import { MainBackgroundProps } from '../render'; +import { MAIN_WIDTH, MAIN_HEIGHT } from '../shared'; + +const mainBackgroundProps = { + props: ['controller', 'instance'] +} satisfies SetupComponentOptions; + +export const MainBackground = defineComponent(() => { + return () => ( + + ); +}, mainBackgroundProps); + +/** 主 UI 控制器的背景 UI */ +export const MainBackgroundUI = new GameUI('main-background', MainBackground); diff --git a/packages-user/client-modules/src/ui/index.ts b/packages-user/client-modules/src/ui/index.ts new file mode 100644 index 0000000..fcb073f --- /dev/null +++ b/packages-user/client-modules/src/ui/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/packages-user/client-modules/src/ui/load.tsx b/packages-user/client-modules/src/ui/load.tsx new file mode 100644 index 0000000..ea3d0e0 --- /dev/null +++ b/packages-user/client-modules/src/ui/load.tsx @@ -0,0 +1,157 @@ +import { DefaultProps } from '@motajs/render-vue'; +import { GameUI, SetupComponentOptions } from '@motajs/system'; +import { defineComponent } from 'vue'; +import { + FULL_LOC, + LOAD_BYTE_HEIGHT, + LOAD_BYTE_LENGTH, + LOAD_BYTE_LINE_WIDTH, + LOAD_FONT_COLOR, + LOAD_LOADED_COLOR, + LOAD_TASK_CENTER_HEIGHT, + LOAD_TASK_LINE_WIDTH, + LOAD_TASK_RADIUS, + LOAD_UNLOADED_COLOR, + MAIN_WIDTH +} from '../shared'; +import { ElementLocator, Font, MotaOffscreenCanvas2D } from '@motajs/render'; +import { transitioned } from '../render'; +import { cosh, CurveMode, linear } from '@motajs/animate'; +import { clamp } from 'lodash-es'; +import { sleep } from '@motajs/common'; +import { GameTitleUI } from './title'; +import { IUIDefaultPropsBase } from './types'; + +export interface ILoadProps extends IUIDefaultPropsBase, DefaultProps {} + +const loadSceneProps = { + props: ['controller', 'instance', 'state'] +} satisfies SetupComponentOptions; + +export const LoadScene = defineComponent(props => { + const loader = props.state.loader; + const taskFont = new Font('Verdana', 24); + const byteFont = new Font('Verdana', 12); + + /** 当前加载进度 */ + const taskProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!; + const byteProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!; + const alpha = transitioned(1, 400, linear())!; + + // 两个进度条的位置 + const taskLoc: ElementLocator = [ + MAIN_WIDTH / 2, + LOAD_TASK_CENTER_HEIGHT, + LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2, + LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2, + 0.5, + 0.5 + ]; + const byteLoc: ElementLocator = [ + MAIN_WIDTH / 2, + LOAD_BYTE_HEIGHT, + LOAD_BYTE_LENGTH + LOAD_BYTE_LINE_WIDTH, + LOAD_BYTE_LINE_WIDTH * 2 + byteFont.size, + 0.5, + 0.5 + ]; + + const loadEnd = async () => { + alpha.set(0); + await sleep(400); + props.controller.closeAll(); + props.controller.open(GameTitleUI, {}); + }; + + const startLoad = async () => { + for await (const _ of loader.start()) { + taskProgress.set(loader.manager.getLoadedTasks()); + byteProgress.set(loader.manager.getLoadedByte()); + } + loadEnd(); + }; + + // 开始加载 + startLoad(); + + /** 渲染加载任务进度 */ + const renderTaskList = (canvas: MotaOffscreenCanvas2D) => { + const ctx = canvas.ctx; + ctx.lineCap = 'round'; + ctx.lineWidth = LOAD_TASK_LINE_WIDTH; + ctx.font = taskFont.string(); + const loaded = loader.manager.getLoadedTasks(); + const total = loader.manager.getAddedTasks(); + // 这里使用渐变参数,因为要有动画效果 + const progress = clamp(taskProgress.value / total, 0, 1); + const cx = taskLoc[2]! / 2; + const cy = taskLoc[3]! / 2; + ctx.beginPath(); + ctx.arc(cx, cy, LOAD_TASK_RADIUS, 0, Math.PI * 2); + ctx.strokeStyle = LOAD_UNLOADED_COLOR; + ctx.stroke(); + ctx.beginPath(); + const end = progress * Math.PI * 2 - Math.PI / 2; + ctx.arc(cx, cy, LOAD_TASK_RADIUS, -Math.PI / 2, end); + ctx.strokeStyle = LOAD_LOADED_COLOR; + ctx.stroke(); + ctx.fillStyle = LOAD_FONT_COLOR; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(`${loaded} / ${total}`, cx, cy + 3); + }; + + /** 渲染加载字节进度 */ + const renderByteList = (canvas: MotaOffscreenCanvas2D) => { + const ctx = canvas.ctx; + ctx.lineCap = 'round'; + ctx.lineWidth = LOAD_BYTE_LINE_WIDTH; + ctx.font = byteFont.string(); + const total = loader.manager.getTotalByte(); + const loaded = loader.manager.getLoadedByte(); + // 这里使用渐变参数,因为要有动画效果 + const progress = clamp(byteProgress.value / total, 0, 1); + const sx = LOAD_BYTE_LINE_WIDTH; + const sy = byteFont.size + LOAD_BYTE_LINE_WIDTH; + ctx.beginPath(); + ctx.moveTo(sx, sy); + ctx.lineTo(sx + LOAD_BYTE_LENGTH, sy); + ctx.strokeStyle = LOAD_UNLOADED_COLOR; + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(sx, sy); + ctx.lineTo(sx + progress * LOAD_BYTE_LENGTH, sy); + ctx.strokeStyle = LOAD_LOADED_COLOR; + ctx.stroke(); + ctx.textAlign = 'right'; + ctx.textBaseline = 'bottom'; + ctx.fillStyle = LOAD_FONT_COLOR; + const loadedMB = (loaded / 2 ** 20).toFixed(2); + const totalMB = (total / 2 ** 20).toFixed(2); + const percent = loader.manager.getByteRatio() * 100; + ctx.fillText( + `${loadedMB}MB / ${totalMB}MB | ${percent.toFixed(2)}%`, + byteLoc[2]! - LOAD_BYTE_LINE_WIDTH, + byteLoc[3]! - LOAD_BYTE_LINE_WIDTH * 2 + ); + }; + + return () => ( + + + + + ); +}, loadSceneProps); + +export const LoadSceneUI = new GameUI('load-scene', LoadScene); diff --git a/packages-user/client-modules/src/ui/title.tsx b/packages-user/client-modules/src/ui/title.tsx new file mode 100644 index 0000000..0ba5483 --- /dev/null +++ b/packages-user/client-modules/src/ui/title.tsx @@ -0,0 +1,484 @@ +import { DefaultProps } from '@motajs/render-vue'; +import { + GameUI, + SetupComponentOptions, + IUIDefaultPropsBase +} from '@motajs/system'; +import { defineComponent, nextTick, onMounted, ref } from 'vue'; +import { + BUTTONS_HEIGHT, + BUTTONS_WIDTH, + BUTTONS_X, + BUTTONS_Y, + HALF_HEIGHT, + HALF_WIDTH, + MAIN_HEIGHT, + MAIN_WIDTH, + TITLE_BACKGROUND_IMAGE, + TITLE_FILL, + TITLE_STROKE, + TITLE_STROKE_WIDTH, + TITLE_X, + TITLE_Y +} from '../shared'; +import { ElementLocator, Font } from '@motajs/render'; +import { + ITransitionedController, + transitioned, + transitionedColor, + useKey +} from '../render'; +import { ExitFullscreen, Fullscreen, SoundVolume } from '@user/client-base'; +import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui'; +import { saveLoad } from './save'; +import { MainSceneUI } from './main'; +import { adjustCover } from '@user/client-base'; +import { cosh, CurveMode, linear } from '@motajs/animate'; +import { sleep } from '@motajs/common'; +import { IUIDefaultPropsBase } from './types'; + +const enum TitleButton { + StartGame, + LoadGame, + Replay +} + +interface ButtonItem { + code: TitleButton; + name: string; + color: string; +} + +interface ButtonOption { + code: number; + color: string; + name: string; + hard: string; + colorTrans: ITransitionedController; +} + +export interface GameTitleProps extends DefaultProps, IUIDefaultPropsBase {} + +const gameTitleProps = { + props: ['controller', 'instance', 'state'] +} satisfies SetupComponentOptions; + +export const GameTitle = defineComponent(props => { + const materials = props.state.materials; + const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE); + + //#region 计算背景图 + const [width, height] = adjustCover( + bg?.width ?? MAIN_WIDTH, + bg?.height ?? MAIN_HEIGHT, + MAIN_WIDTH, + MAIN_HEIGHT + ); + + //#region 标题设置 + + /** 当前是否全屏 */ + const fullscreen = ref(!!document.fullscreenElement); + /** 是否开启了声音 */ + const soundOpened = ref(true); + /** 是否在选择难度界面 */ + const selectHard = ref(false); + + /** 开始界面按钮定义,你可以在这新增自己的按钮 */ + const buttonItems: ButtonItem[] = [ + { + code: TitleButton.StartGame, + color: 'rgb(40, 194, 255)', + name: '开始游戏' + }, + { + code: TitleButton.LoadGame, + color: 'rgb(0, 255, 55)', + name: '读取存档' + }, + { + code: TitleButton.Replay, + color: 'rgb(255, 251, 0)', + name: '录像回放' + } + ]; + + /** 开始界面按钮 */ + const buttons = buttonItems.map(v => { + return { + code: v.code, + color: v.color, + name: v.name, + hard: '', + colorTrans: transitionedColor( + '#fff', + 400, + cosh(2, CurveMode.EaseOut) + )!, + scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))! + }; + }); + + /** 选择难度界面按钮 */ + const hard = main.levelChoose.map(v => { + return { + code: v.hard, + color: core.arrayToRGBA(v.color!), + name: v.title, + hard: v.name, + colorTrans: transitionedColor( + '#fff', + 400, + cosh(2, CurveMode.EaseOut) + )!, + scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))! + }; + }); + // 返回按钮 + hard.push({ + code: -1, + color: '#aaa', + name: '返回', + hard: '', + colorTrans: transitionedColor('#fff', 400, cosh(2, CurveMode.EaseOut))! + }); + + /** 声音设置按钮的颜色 */ + const soundColor = transitionedColor( + '#ddd', + 400, + cosh(2, CurveMode.EaseOut) + )!; + + /** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */ + const buttonsAlpha = transitioned(1, 300, linear())!; + /** 开始界面的不透明度 */ + const mainAlpha = transitioned(0, 600, linear())!; + + const buttonFilter = ` + drop-shadow(3px 3px 5px rgba(0, 0, 0, 0.4)) + drop-shadow(0px 0px 2px rgba(255, 255, 255, 0.5)) + `; + + const titleFont = Font.defaults({ size: 72 }); + const buttonFont = Font.defaults({ size: 24, weight: 600 }); + + /** 开始界面按钮的高度 */ + const buttonHeight = (buttons.length - 1) * 40 + 60; + /** 选择难度界面按钮的高度 */ + const hardHeight = (hard.length - 1) * 40 + 60; + /** 按钮的背景框高度 */ + const rectHeight = transitioned( + buttonHeight, + 600, + cosh(2, CurveMode.EaseOut) + )!; + + //#region 按钮功能 + + /** + * 在开始界面和选择难度界面切换 + */ + const toggleHard = async () => { + if (selectHard.value) { + // 当前在难度界面,将要切换至开始界面 + enterMain(0); + rectHeight.set(buttonHeight); + } else { + // 当前在开始界面,将要切换至难度界面 + enterHard(0); + rectHeight.set(hardHeight); + } + buttonsAlpha.set(0); + await sleep(300); + selectHard.value = !selectHard.value; + buttonsAlpha.set(1); + }; + + /** + * 点击读取存档按钮 + */ + const loadGame = async () => { + const loc: ElementLocator = [0, 0, MAIN_WIDTH, MAIN_HEIGHT]; + const success = await saveLoad(props.controller, loc); + if (success) { + props.controller.close(props.instance); + props.controller.open(MainSceneUI, {}); + } + }; + + /** + * 点击录像回放按钮 + */ + const replay = () => { + core.chooseReplayFile(); + }; + + /** + * 选择难度并开始游戏 + * @param hard 选择的难度 + */ + const startGame = async (hard: string) => { + mainAlpha.set(0); + await sleep(600); + props.controller.close(props.instance); + props.controller.open(MainSceneUI, {}); + nextTick(() => { + core.startGame(hard); + }); + }; + + /** + * 点击按钮时触发 + * @param code 选中的按钮的代码 + */ + const clickButton = (code: number, index: number) => { + if (selectHard.value) { + if (index === hard.length - 1) { + // 选中了最后一个按钮 + toggleHard(); + return; + } + const item = hard[index]; + startGame(item.name); + } else { + switch (code) { + case TitleButton.StartGame: + toggleHard(); + break; + case TitleButton.LoadGame: + loadGame(); + break; + case TitleButton.Replay: + replay(); + break; + } + } + }; + + //#region 键盘操作 + + const selected = ref(0); + + const [key] = useKey(); + key.realize( + '@start_up', + () => { + selected.value--; + if (selected.value < 0) { + selected.value = 0; + } + if (selectHard.value) { + enterHard(selected.value); + } else { + enterMain(selected.value); + } + }, + { type: 'down' } + ) + .realize( + '@start_down', + () => { + selected.value++; + if (selectHard.value) { + if (selected.value > hard.length - 1) { + selected.value = hard.length - 1; + } + enterHard(selected.value); + } else { + if (selected.value > buttons.length - 1) { + selected.value = buttons.length - 1; + } + enterMain(selected.value); + } + }, + { type: 'down' } + ) + .realize('confirm', () => { + if (selectHard.value) { + clickButton(hard[selected.value].code, selected.value); + } else { + clickButton(buttons[selected.value].code, selected.value); + } + }); + + //#region 鼠标操作 + + soundOpened.value = mainSetting.getValue('audio.bgmEnabled', true); + if (soundOpened.value) { + soundColor.set('#ddd'); + } else { + soundColor.set('#d22'); + } + + const enterMain = (index: number) => { + buttons.forEach((v, i) => { + if (index !== i) { + v.colorTrans.set('#fff'); + } else { + v.colorTrans.set(v.color); + } + }); + selected.value = index; + }; + + const enterHard = (index: number) => { + hard.forEach((v, i) => { + if (index !== i) { + v.colorTrans.set('#fff'); + } else { + v.colorTrans.set(v.color); + } + }); + selected.value = index; + }; + + const toggleSound = () => { + soundOpened.value = !soundOpened.value; + mainSetting.setValue('audio.bgmEnabled', soundOpened.value); + if (soundOpened.value) { + soundColor.set('#ddd'); + } else { + soundColor.set('#d22'); + } + }; + + const toggleFullscreen = async () => { + await triggerFullscreen(!fullscreen.value); + fullscreen.value = !!document.fullscreenElement; + }; + + onMounted(() => { + enterMain(0); + mainAlpha.set(1); + }); + + return () => ( + + + + + + + + + + + + {!fullscreen.value ? ( + + ) : ( + + )} + + + ); +}, gameTitleProps); + +export const GameTitleUI = new GameUI('game-title', GameTitle); diff --git a/packages-user/client-modules/src/ui/types.ts b/packages-user/client-modules/src/ui/types.ts new file mode 100644 index 0000000..8a92889 --- /dev/null +++ b/packages-user/client-modules/src/ui/types.ts @@ -0,0 +1,5 @@ +import { IUIDefaultPropsBase } from '@motajs/system'; +import { IClientBaseExtended } from '@user/client-base'; + +export interface IUIPropsBase + extends IUIDefaultPropsBase, IClientBaseExtended {} diff --git a/packages/system/src/ui/container.tsx b/packages/system/src/ui/container.tsx index 191552d..7ce0269 100644 --- a/packages/system/src/ui/container.tsx +++ b/packages/system/src/ui/container.tsx @@ -1,22 +1,23 @@ import { defineComponent, VNode } from 'vue'; -import { IUIMountable } from './shared'; +import { IUIController } from './types'; import { SetupComponentOptions } from './types'; -export interface UIContainerProps { - controller: IUIMountable; +export interface IUIContainerProps { + /** 当前 UI 容器组件所属的控制器 */ + readonly controller: IUIController; } const containerConfig = { props: ['controller'] -} satisfies SetupComponentOptions; +} satisfies SetupComponentOptions; -export const UIContainer = defineComponent(props => { +export const UIContainer = defineComponent(props => { const data = props.controller; const back = data.backIns; return (): VNode[] => { const elements: VNode[] = []; const b = back.value; - if (b && data.showBack.value && !b.hidden) { + if (data.showBack.value && b && !b.hidden) { elements.push( (props => { key={v.key} controller={data} instance={v} - hidden={v.hidden && !v.alwaysShow} + hidden={v.hidden.value} zIndex={i * 5} > )) diff --git a/packages/system/src/ui/controller.ts b/packages/system/src/ui/controller.ts index 8f7c8ab..ab173de 100644 --- a/packages/system/src/ui/controller.ts +++ b/packages/system/src/ui/controller.ts @@ -1,158 +1,89 @@ -import { logger } from '@motajs/common'; -import EventEmitter from 'eventemitter3'; import { IGameUI, IKeepController, IUIInstance, - IUIMountable, + IUIController, UIComponent, - UIProps -} from './shared'; + IUIDefaultPropsBase, + IUICustomStrategy, + UIRawProps +} from './types'; import { UIInstance } from './instance'; import { computed, - ComputedRef, h, - reactive, ref, Ref, + shallowReactive, shallowRef, ShallowRef, VNode } from 'vue'; import { UIContainer } from './container'; -export const enum UIMode { - /** 仅显示最后一个非 alwaysShow 的 UI,在关闭时,只会关闭指定的 UI */ - LastOnly, - /** 显示所有非手动隐藏的 UI,在关闭时,只会关闭指定 UI */ - All, - /** 仅显示最后一个非 alwaysShow 的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */ - LastOnlyStack, - /** 显示所有非手动隐藏的 UI,在关闭时,在此之后的所有 UI 会全部关闭 */ - AllStack, - /** 自定义 UI 显示模式 */ - Custom -} +export class UIController< + PB extends IUIDefaultPropsBase +> implements IUIController { + readonly stack: IUIInstance[] = shallowReactive([]); + readonly backIns: ShallowRef | null>; -export interface IUICustomConfig { - /** - * 打开一个新的 UI - * @param ins 要打开的 UI 实例 - * @param stack 当前的 UI 栈,还未将 UI 实例加入栈中 - */ - open(ins: IUIInstance, stack: IUIInstance[]): void; + background: IGameUI | null = null; + active: Ref = ref(false); - /** - * 关闭一个 UI - * @param ins 要关闭的 UI 实例 - * @param stack 当前的 UI 栈,还未将 UI 实例移除 - * @param index 这个 UI 实例在 UI 栈中的索引 - */ - close(ins: IUIInstance, stack: IUIInstance[], index: number): void; + readonly showBack: Ref; - /** - * 隐藏一个 UI - * @param ins 要隐藏的 UI 实例,还未进入隐藏状态 - * @param stack 当前的 UI 栈 - * @param index 这个 UI 实例在 UI 栈中的索引 - */ - hide(ins: IUIInstance, stack: IUIInstance[], index: number): void; + /** 当前的自定义 UI 控制策略 */ + private strategy: IUICustomStrategy; - /** - * 显示一个 UI - * @param ins 要显示的 UI 实例,还未进入显示状态 - * @param stack 当前的 UI 栈 - * @param index 这个 UI 实例在 UI 栈中的索引 - */ - show(ins: IUIInstance, stack: IUIInstance[], index: number): void; - - /** - * 更新所有 UI 的显示,一般会在显示模式更改时调用 - * @param stack 当前的 UI 栈 - */ - update(stack: IUIInstance[]): void; -} - -interface UIControllerEvent { - open: [ui: IGameUI, ins: IUIInstance]; - close: [ui: IUIInstance]; -} - -export class UIController - extends EventEmitter - implements IUIMountable -{ - static controllers: Map = new Map(); - - /** 当前的 ui 栈 */ - readonly stack: IUIInstance[] = reactive([]); - /** UI 显示方式 */ - mode: UIMode = UIMode.LastOnlyStack; - /** 这个 UI 实例的背景,当这个 UI 处于显示模式时,会显示背景 */ - background?: IGameUI; - - /** 背景 UI 实例 */ - readonly backIns: ShallowRef = shallowRef(null); - /** 当前是否显示背景 UI */ - readonly showBack: ComputedRef = computed( - () => this.userShowBack.value && this.sysShowBack.value - ); - /** 当前 UI 是否激活 */ - readonly active: Ref = ref(false); - - /** 自定义显示模式下的配置信息 */ - private config?: IUICustomConfig; /** 是否维持背景 UI */ private keepBack: boolean = false; /** 用户是否显示背景 UI */ - private readonly userShowBack: Ref = ref(true); + private readonly userShowBack: Ref; /** 系统是否显示背景 UI */ - private readonly sysShowBack: Ref = ref(false); + private readonly sysShowBack: Ref; - /** - * 创建一个 ui 控制器 - * @param id 这个控制器的唯一标识符 - */ - constructor(public readonly id: string) { - super(); - if (UIController.controllers.has(id)) { - logger.warn(57, id); + constructor() { + this.backIns = shallowRef(null); + this.userShowBack = ref(true); + this.sysShowBack = ref(true); + this.showBack = computed( + () => this.userShowBack.value && this.sysShowBack.value + ); + this.strategy = new UILastOnlyStrategy(); + } + + setBackground( + background: IGameUI | null, + vBind: UIRawProps + ): IUIInstance | null { + if (!background) { + this.background = null; + this.backIns.value = null; + return null; } else { - UIController.controllers.set(id, this); + this.background = background; + const ins = new UIInstance(background, vBind, true); + this.backIns.value = ins; + return ins; } } + hideBackground() { + this.userShowBack.value = false; + } + + showBackground() { + this.userShowBack.value = true; + } + + showingBack(): boolean { + return this.showBack.value; + } + render(): VNode { return h(UIContainer, { controller: this }); } - /** - * 设置背景 UI - * @param back 这个 UI 控制器的背景 UI - */ - setBackground(back: IGameUI, vBind: UIProps) { - this.background = back; - this.backIns.value = new UIInstance(back, vBind, true); - } - - /** - * 隐藏背景 UI - */ - hideBackground() { - this.userShowBack.value = false; - } - - /** - * 显示背景 UI - */ - showBackground() { - this.userShowBack.value = true; - } - - /** - * 维持背景 UI,一般用于防闪烁,例如使用道具时可能在关闭道具栏后打开新 UI,这时就需要防闪烁 - */ keep(): IKeepController { this.keepBack = true; @@ -169,70 +100,29 @@ export class UIController }; } - open( - ui: IGameUI, - vBind: UIProps, + open( + ui: IGameUI, + vBind: UIRawProps, alwaysShow: boolean = false - ): IUIInstance { + ): IUIInstance { const ins = new UIInstance(ui, vBind, alwaysShow); - switch (this.mode) { - case UIMode.LastOnly: - case UIMode.LastOnlyStack: - this.stack.push(ins); - this.stack.forEach(v => v.hide()); - this.stack.findLast(v => !v.alwaysShow)?.show(); - break; - case UIMode.All: - case UIMode.AllStack: - this.stack.push(ins); - break; - case UIMode.Custom: - this.config?.open(ins, this.stack); - break; - } + this.strategy.open(ins, this.stack); this.sysShowBack.value = true; this.active.value = true; - this.emit('open', ui, ins); return ins; } - close(ui: IUIInstance) { + close(ui: IUIInstance) { const index = this.stack.indexOf(ui); if (index === -1) return; - switch (this.mode) { - case UIMode.LastOnly: { - this.stack.splice(index, 1); - this.stack.forEach(v => v.hide()); - this.stack.findLast(v => !v.alwaysShow)?.show(); - break; - } - case UIMode.LastOnlyStack: { - this.stack.splice(index); - this.stack.forEach(v => v.hide()); - this.stack.findLast(v => !v.alwaysShow)?.show(); - break; - } - case UIMode.All: { - this.stack.splice(index, 1); - break; - } - case UIMode.AllStack: { - this.stack.splice(index); - break; - } - case UIMode.Custom: { - this.config?.close(ui, this.stack, index); - break; - } - } + this.strategy.close(ui, this.stack, index); if (!this.keepBack && this.stack.length === 0) { this.sysShowBack.value = false; this.active.value = false; } this.keepBack = false; - this.emit('close', ui); } closeAll(ui?: IGameUI): void { @@ -245,69 +135,113 @@ export class UIController } } - hide(ins: IUIInstance): void { + hide(ins: IUIInstance): void { const index = this.stack.indexOf(ins); if (index === -1) return; - if (this.mode === UIMode.Custom) { - this.config?.hide(ins, this.stack, index); - } else { - ins.hide(); - } + this.strategy.hide(ins, this.stack, index); } - show(ins: IUIInstance): void { + show(ins: IUIInstance): void { const index = this.stack.indexOf(ins); if (index === -1) return; - if (this.mode === UIMode.Custom) { - this.config?.show(ins, this.stack, index); - } else { - ins.show(); - } + this.strategy.show(ins, this.stack, index); } - /** - * 设置为仅显示最后一个 UI - * @param stack 是否设置为栈模式,即删除一个 UI 后,其之后打开的 UI 是否也一并删除 - */ lastOnly(stack: boolean = true) { - if (stack) { - this.mode = UIMode.LastOnlyStack; - } else { - this.mode = UIMode.LastOnly; - } - this.stack.forEach(v => v.hide()); - this.stack.at(-1)?.show(); + this.showCustom(new UILastOnlyStrategy(stack)); } - /** - * 设置为显示所有 UI - * @param stack 是否设置为栈模式,即删除一个 UI 后,其之后打开的 UI 是否也一并删除 - */ showAll(stack: boolean = false) { - if (stack) { - this.mode = UIMode.AllStack; - } else { - this.mode = UIMode.All; - } - this.stack.forEach(v => v.show()); + this.showCustom(new UIShowAllStrategy(stack)); } - /** - * 使用自定义的显示模式 - * @param config 自定义显示模式的配置 - */ - showCustom(config: IUICustomConfig) { - this.mode = UIMode.Custom; - this.config = config; - config.update(this.stack); - } - - /** - * 获取一个元素上的 ui 控制器 - * @param id 要获取的 ui 控制器的唯一标识符 - */ - static getController(id: string): UIController | null { - const res = this.controllers.get(id); - return res ?? null; + showCustom(stratygy: IUICustomStrategy) { + this.strategy = stratygy; + stratygy.update(this.stack); + } +} + +class UILastOnlyStrategy implements IUICustomStrategy { + /** 是否启用栈模式 */ + readonly stack: boolean; + + constructor(stack: boolean = true) { + this.stack = stack; + } + + open( + ins: IUIInstance, + stack: IUIInstance[] + ): void { + stack.push(ins); + stack.forEach(v => v.hide()); + stack.findLast(v => !v.alwaysShow)?.show(); + } + + close( + _ins: IUIInstance, + stack: IUIInstance[], + index: number + ): void { + if (this.stack) { + stack.splice(index); + } else { + stack.splice(index, 1); + } + stack.forEach(v => v.hide()); + stack.findLast(v => !v.alwaysShow)?.show(); + } + + hide(ins: IUIInstance): void { + ins.hide(); + } + + show(ins: IUIInstance): void { + ins.show(); + } + + update(stack: IUIInstance[]): void { + stack.forEach(v => v.hide()); + stack.findLast(v => !v.alwaysShow)?.show(); + } +} + +class UIShowAllStrategy implements IUICustomStrategy { + /** 是否启用栈模式 */ + readonly stack: boolean; + + constructor(stack: boolean = true) { + this.stack = stack; + } + + open( + ins: IUIInstance, + stack: IUIInstance[] + ): void { + stack.push(ins); + } + + close( + _ins: IUIInstance, + stack: IUIInstance[], + index: number + ): void { + if (this.stack) { + stack.splice(index); + } else { + stack.splice(index, 1); + } + } + + hide(ins: IUIInstance): void { + ins.hide(); + } + + show(ins: IUIInstance): void { + ins.show(); + } + + update(stack: IUIInstance[]): void { + stack.forEach(v => v.show()); } } diff --git a/packages/system/src/ui/index.ts b/packages/system/src/ui/index.ts index c137839..47467ad 100644 --- a/packages/system/src/ui/index.ts +++ b/packages/system/src/ui/index.ts @@ -1,6 +1,5 @@ export * from './container'; export * from './controller'; export * from './instance'; -export * from './shared'; export * from './types'; export * from './ui'; diff --git a/packages/system/src/ui/instance.ts b/packages/system/src/ui/instance.ts index 92953ee..11a6ce8 100644 --- a/packages/system/src/ui/instance.ts +++ b/packages/system/src/ui/instance.ts @@ -1,40 +1,40 @@ -import { Props } from '@motajs/render'; -import { IGameUI, IUIInstance, UIComponent, UIProps } from './shared'; -import { markRaw, mergeProps } from 'vue'; +import { Reactive, reactive, Ref, ref } from 'vue'; +import { + IGameUI, + IUIDefaultPropsBase, + IUIInstance, + UIComponent, + UIRawProps +} from './types'; -export class UIInstance implements IUIInstance { - private static counter: number = 0; +/** 自增 UI 实例计数器 */ +let counter = 0; - readonly key: number = UIInstance.counter++; +export class UIInstance< + C extends UIComponent, + PB extends IUIDefaultPropsBase +> implements IUIInstance { + readonly key: number = counter++; readonly ui: IGameUI; - hidden: boolean = false; + readonly hidden: Ref = ref(false); + readonly vBind: Reactive>; + readonly alwaysShow: boolean; constructor( ui: IGameUI, - public vBind: UIProps, - public readonly alwaysShow: boolean = false + vBind: UIRawProps, + alwaysShow: boolean = false ) { - this.ui = markRaw(ui); - } - - /** - * 设置这个 UI 实例的响应式数据的值 - * @param data 要设置的值 - * @param merge 是将传入的值与原先的值合并(true),还是将当前值覆盖掉原先的值(false),默认合并 - */ - setVBind(data: Partial>, merge: boolean = true) { - if (merge) { - this.vBind = mergeProps(this.vBind, data) as UIProps; - } else { - this.vBind = data as UIProps; - } + this.ui = ui; + this.vBind = reactive(vBind); + this.alwaysShow = alwaysShow; } hide(): void { - this.hidden = true; + if (!this.alwaysShow) this.hidden.value = true; } show(): void { - this.hidden = false; + this.hidden.value = false; } } diff --git a/packages/system/src/ui/shared.ts b/packages/system/src/ui/shared.ts deleted file mode 100644 index 0d62494..0000000 --- a/packages/system/src/ui/shared.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Props } from '@motajs/render'; -import { - DefineComponent, - DefineSetupFnComponent, - Ref, - ShallowRef, - VNode -} from 'vue'; - -export type UIComponent = DefineSetupFnComponent | DefineComponent; - -export interface UIComponentProps { - controller: IUIMountable; - instance: IUIInstance; -} - -export type UIProps = Omit< - Props, - keyof UIComponentProps ->; - -export interface IGameUI { - /** 这个 UI 的名称 */ - readonly name: string; - /** 这个 UI 的组件 */ - readonly component: C; -} - -export interface IKeepController { - /** - * 安全关闭背景 UI,如果当前没有 UI 已开启,那么直接关闭,否则维持 - */ - safelyUnload(): void; - - /** - * 不论当前是否有 UI 已开启,都关闭背景 - */ - unload(): void; -} - -export interface IUIMountable { - /** 当前的 UI 栈 */ - readonly stack: IUIInstance[]; - /** 当前的背景 UI */ - readonly backIns: ShallowRef | null>; - /** 当前是否显示背景 UI */ - readonly showBack: Ref; - - /** - * 隐藏一个 UI - * @param ins 要隐藏的 UI 实例 - */ - hide(ins: IUIInstance): void; - - /** - * 显示一个 UI - * @param ins 要显示的 UI 实例 - */ - show(ins: IUIInstance): void; - - /** - * 渲染这个 UI,可以直接嵌入渲染树中 - */ - render(): VNode; - - /** - * 隐藏背景 UI - */ - hideBackground(): void; - - /** - * 显示背景 UI - */ - showBackground(): void; - - /** - * 打开一个 ui - * @param ui 要打开的 ui - * @param vBind 传递给这个 ui 的响应式数据 - * @param alwaysShow 这个 ui 是否保持开启,对于需要叠加显示的 ui 非常有用 - */ - open( - ui: IGameUI, - vBind: UIProps, - alwaysShow?: boolean - ): IUIInstance; - - /** - * 关闭一个 ui - * @param ui 要关闭的 ui 实例 - */ - close(ui: IUIInstance): void; - - /** - * 关闭所有或指定类型的所有 UI - */ - closeAll(ui?: IGameUI): void; - - /** - * 维持背景,直到下次所有 UI 都被关闭 - */ - keep(): IKeepController; -} - -export interface IUIInstance { - /** 这个 ui 实例的唯一 key,用于 vue */ - readonly key: number; - /** 这个 ui 实例的 ui 信息 */ - readonly ui: IGameUI; - /** 传递给这个 ui 实例的响应式数据 */ - readonly vBind: UIProps; - /** 当前元素是否被隐藏 */ - readonly hidden: boolean; - /** 是否永远保持开启 */ - readonly alwaysShow: boolean; - - /** - * 设置这个 UI 实例的响应式数据的值 - * @param data 要设置的值 - * @param merge 是将传入的值与原先的值合并(true),还是将当前值覆盖掉原先的值(false),默认合并 - */ - setVBind(data: Partial>, merge?: boolean): void; - - /** - * 隐藏这个 ui - */ - hide(): void; - - /** - * 显示这个 ui - */ - show(): void; -} diff --git a/packages/system/src/ui/types.ts b/packages/system/src/ui/types.ts index ed4a50c..15c94c6 100644 --- a/packages/system/src/ui/types.ts +++ b/packages/system/src/ui/types.ts @@ -1,4 +1,15 @@ -import { ComponentOptions, EmitsOptions, SlotsType } from 'vue'; +import { Props } from '@motajs/render'; +import { + ComponentOptions, + DefineComponent, + DefineSetupFnComponent, + EmitsOptions, + Reactive, + Ref, + ShallowRef, + SlotsType, + VNode +} from 'vue'; export type SetupComponentOptions< Props extends Record, @@ -10,3 +21,222 @@ export type SetupComponentOptions< emits?: E | EE[]; slots?: S; }; + +export type UIComponent = DefineSetupFnComponent | DefineComponent; + +export interface IUIDefaultPropsBase { + /** 当前打开的 UI 所属的控制器 */ + readonly controller: IUIController; + /** 当前 UI 实例对象 */ + readonly instance: IUIInstance; +} + +export interface IGameUI { + /** 这个 UI 的名称 */ + readonly name: string; + /** 这个 UI 的 vue 组件对象 */ + readonly component: C; +} + +export interface IUICustomStrategy> { + /** + * 打开一个新的 UI + * @param ins 要打开的 UI 实例 + * @param stack 当前的 UI 栈,还未将 UI 实例加入栈中 + */ + open( + ins: IUIInstance, + stack: IUIInstance[] + ): void; + + /** + * 关闭一个 UI + * @param ins 要关闭的 UI 实例 + * @param stack 当前的 UI 栈,还未将 UI 实例移除 + * @param index 这个 UI 实例在 UI 栈中的索引 + */ + close( + ins: IUIInstance, + stack: IUIInstance[], + index: number + ): void; + + /** + * 隐藏一个 UI + * @param ins 要隐藏的 UI 实例,还未进入隐藏状态 + * @param stack 当前的 UI 栈 + * @param index 这个 UI 实例在 UI 栈中的索引 + */ + hide( + ins: IUIInstance, + stack: IUIInstance[], + index: number + ): void; + + /** + * 显示一个 UI + * @param ins 要显示的 UI 实例,还未进入显示状态 + * @param stack 当前的 UI 栈 + * @param index 这个 UI 实例在 UI 栈中的索引 + */ + show( + ins: IUIInstance, + stack: IUIInstance[], + index: number + ): void; + + /** + * 更新所有 UI 的显示状态,一般会在显示模式更改时调用,用于初始化显示模式的显示状态 + * @param stack 当前的 UI 栈 + */ + update(stack: IUIInstance[]): void; +} + +export interface IKeepController { + /** + * 安全关闭背景 UI,如果当前没有 UI 已开启,那么直接关闭,否则维持 + */ + safelyUnload(): void; + + /** + * 不论当前是否有 UI 已开启,都关闭背景 + */ + unload(): void; +} + +/** + * 获取 UI 组件在排除内置基础参数后的剩余参数类型 + */ +export type UIRawProps< + C extends UIComponent, + PB extends IUIDefaultPropsBase +> = Omit, keyof PB>; + +export interface IUIController> { + /** 当前的 UI 栈 */ + readonly stack: IUIInstance[]; + /** 背景 UI 对象,当显示背景 UI 时,会显示此 UI 对应的实例 */ + readonly background: IGameUI | null; + + /** 当前的背景 UI 实例的浅层响应式对象 */ + readonly backIns: ShallowRef; + /** 当前 UI 是否已经被激活的响应式对象,即至少有一个 UI 正在显示,或处在 `keep` 环境下 */ + readonly active: Ref; + /** 当前是否显示背景的响应式对象,用于容器响应式判断是否需要显示背景 */ + readonly showBack: Ref; + + /** + * 设置控制器的背景 UI + * @param background 背景 UI + * @param vBind 当打开背景时,传递给背景的 VBind 参数 + * @returns 背景对应的 UI 实例 + */ + setBackground( + background: IGameUI | null, + vBind: UIRawProps + ): IUIInstance | null; + + /** + * 当前是否显示背景 UI + */ + showingBack(): boolean; + + /** + * 隐藏背景 UI + */ + hideBackground(): void; + + /** + * 显示背景 UI + */ + showBackground(): void; + + /** + * 隐藏一个 UI + * @param ins 要隐藏的 UI 实例 + */ + hide(ins: IUIInstance): void; + + /** + * 显示一个 UI + * @param ins 要显示的 UI 实例 + */ + show(ins: IUIInstance): void; + + /** + * 打开一个 ui + * @param ui 要打开的 ui + * @param vBind 传递给这个 ui 的响应式数据 + * @param alwaysShow 这个 ui 是否保持开启,对于需要叠加显示的 ui 非常有用 + */ + open( + ui: IGameUI, + vBind: UIRawProps, + alwaysShow?: boolean + ): IUIInstance; + + /** + * 关闭一个 ui + * @param ui 要关闭的 ui 实例 + */ + close(ui: IUIInstance): void; + + /** + * 关闭所有或指定类型的所有 UI + */ + closeAll(ui?: IGameUI): void; + + /** + * 维持背景 UI,一般用于防闪烁,例如使用道具时可能在关闭道具栏后打开新 UI,这时就需要防闪烁 + */ + keep(): IKeepController; + + /** + * 渲染这个 UI,可以直接嵌入渲染树中 + */ + render(): VNode; + + /** + * 设置为仅显示最后一个 UI + * @param stack 是否设置为栈模式,即删除一个 UI 后,其之后打开的 UI 是否也一并删除,默认为栈模式 + */ + lastOnly(stack?: boolean): void; + + /** + * 设置为显示所有 UI + * @param stack 是否设置为栈模式,即删除一个 UI 后,其之后打开的 UI 是否也一并删除,默认不是栈模式 + */ + showAll(stack?: boolean): void; + + /** + * 使用自定义的显示模式 + * @param strategy 自定义显示模式的配置 + */ + showCustom(strategy: IUICustomStrategy): void; +} + +export interface IUIInstance< + C extends UIComponent = UIComponent, + PB extends IUIDefaultPropsBase = IUIDefaultPropsBase +> { + /** 这个 ui 实例的唯一 key,用于 vue */ + readonly key: number; + /** 这个 ui 实例的 ui 信息 */ + readonly ui: IGameUI; + /** 传递给这个 ui 实例的响应式数据 */ + readonly vBind: Reactive>; + /** 当前元素是否被隐藏 */ + readonly hidden: Ref; + /** 是否永远保持开启 */ + readonly alwaysShow: boolean; + + /** + * 隐藏这个 ui + */ + hide(): void; + + /** + * 显示这个 ui + */ + show(): void; +} diff --git a/packages/system/src/ui/ui.ts b/packages/system/src/ui/ui.ts index 3f28345..04b98c0 100644 --- a/packages/system/src/ui/ui.ts +++ b/packages/system/src/ui/ui.ts @@ -1,6 +1,9 @@ -import { IGameUI, UIComponent } from './shared'; +import { IGameUI, UIComponent } from './types'; export class GameUI implements IGameUI { + // TODO: 这个静态成员是否可以考虑删除? + + /** 当前的所有已声明 UI,用于全局性获取 */ static list: Map> = new Map(); constructor(