mirror of
https://github.com/motajs/template.git
synced 2026-10-17 11:40:16 +08:00
refactor: UI 系统
This commit is contained in:
parent
3e22e3e14a
commit
46cad94aff
@ -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<T extends ChoiceKey = ChoiceKey>(
|
||||
controller: IUIMountable,
|
||||
controller: IUIController,
|
||||
choices: ChoiceItem<T>[],
|
||||
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<T extends ChoiceKey>(
|
||||
controller: IUIMountable,
|
||||
controller: IUIController,
|
||||
choices: ChoiceItem<T>[],
|
||||
loc: ElementLocator,
|
||||
width: number,
|
||||
|
||||
@ -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<TextContentProps> {
|
||||
@ -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,
|
||||
|
||||
@ -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<T>(
|
||||
controller: IUIMountable,
|
||||
controller: IUIController,
|
||||
loc: ElementLocator,
|
||||
width: number,
|
||||
promise: Promise<T>,
|
||||
|
||||
@ -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<number>;
|
||||
/** 用于渲染系统的激励源分频器 */
|
||||
readonly excitationDivider: IExcitationDivider<number>;
|
||||
/** 主地图渲染器,主要用于渲染游戏画面中的地图 */
|
||||
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 {
|
||||
|
||||
@ -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<number>;
|
||||
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<number>();
|
||||
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, {});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
@ -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<ChoicesProps>, UIComponentProps {
|
||||
extends Partial<ChoicesProps>, 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
|
||||
) {
|
||||
|
||||
@ -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<StatisticsData>(() => {
|
||||
return new Promise(res => {
|
||||
const data = calculateStatistics();
|
||||
|
||||
@ -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<string>;
|
||||
}
|
||||
|
||||
export interface GameTitleProps extends DefaultProps, UIComponentProps {}
|
||||
export interface GameTitleProps extends DefaultProps, IUIDefaultPropsBase {}
|
||||
|
||||
const gameTitleProps = {
|
||||
props: ['controller', 'instance']
|
||||
|
||||
@ -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<ViewMapProps>(props => {
|
||||
export const ViewMapUI = new GameUI('view-map', ViewMap);
|
||||
|
||||
export function openViewMap(
|
||||
controller: IUIMountable,
|
||||
controller: IUIController,
|
||||
loc: ElementLocator,
|
||||
props?: ViewMapProps
|
||||
) {
|
||||
|
||||
@ -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<void> {
|
||||
if (!/^\d{6}\w{4}$/.test(identifier) && !/^\d{4}\w{3}$/.test(identifier)) {
|
||||
|
||||
@ -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 {}
|
||||
|
||||
21
packages-user/client-modules/src/ui/background.tsx
Normal file
21
packages-user/client-modules/src/ui/background.tsx
Normal file
@ -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<MainBackgroundProps>;
|
||||
|
||||
export const MainBackground = defineComponent<MainBackgroundProps>(() => {
|
||||
return () => (
|
||||
<g-rect
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
fill
|
||||
fillStyle="rgba(0, 0, 0, 0.8)"
|
||||
/>
|
||||
);
|
||||
}, mainBackgroundProps);
|
||||
|
||||
/** 主 UI 控制器的背景 UI */
|
||||
export const MainBackgroundUI = new GameUI('main-background', MainBackground);
|
||||
1
packages-user/client-modules/src/ui/index.ts
Normal file
1
packages-user/client-modules/src/ui/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './types';
|
||||
157
packages-user/client-modules/src/ui/load.tsx
Normal file
157
packages-user/client-modules/src/ui/load.tsx
Normal file
@ -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<ILoadProps>;
|
||||
|
||||
export const LoadScene = defineComponent<ILoadProps>(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 () => (
|
||||
<container loc={FULL_LOC} alpha={alpha.ref.value}>
|
||||
<custom
|
||||
loc={taskLoc}
|
||||
render={renderTaskList}
|
||||
bindings={[taskProgress.ref]}
|
||||
nocache
|
||||
/>
|
||||
<custom
|
||||
loc={byteLoc}
|
||||
render={renderByteList}
|
||||
bindings={[byteProgress.ref]}
|
||||
nocache
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
}, loadSceneProps);
|
||||
|
||||
export const LoadSceneUI = new GameUI('load-scene', LoadScene);
|
||||
484
packages-user/client-modules/src/ui/title.tsx
Normal file
484
packages-user/client-modules/src/ui/title.tsx
Normal file
@ -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<string>;
|
||||
}
|
||||
|
||||
export interface GameTitleProps extends DefaultProps, IUIDefaultPropsBase {}
|
||||
|
||||
const gameTitleProps = {
|
||||
props: ['controller', 'instance', 'state']
|
||||
} satisfies SetupComponentOptions<GameTitleProps>;
|
||||
|
||||
export const GameTitle = defineComponent<GameTitleProps>(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<ButtonOption>(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<ButtonOption>(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 () => (
|
||||
<container
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
alpha={mainAlpha.ref.value}
|
||||
>
|
||||
<image
|
||||
image={bg}
|
||||
loc={[HALF_WIDTH, HALF_HEIGHT, width, height]}
|
||||
anc={[0.5, 0.5]}
|
||||
zIndex={0}
|
||||
/>
|
||||
<text
|
||||
text={core.firstData.title}
|
||||
loc={[TITLE_X, TITLE_Y]}
|
||||
anc={[0.5, 0.5]}
|
||||
fillStyle={TITLE_FILL}
|
||||
strokeStyle={TITLE_STROKE}
|
||||
font={titleFont}
|
||||
strokeWidth={TITLE_STROKE_WIDTH}
|
||||
/>
|
||||
<container
|
||||
zIndex={15}
|
||||
loc={[BUTTONS_X, BUTTONS_Y, BUTTONS_WIDTH, BUTTONS_HEIGHT]}
|
||||
anc={[0.5, 0]}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[2, BUTTONS_HEIGHT / 2]}
|
||||
width={BUTTONS_WIDTH - 4}
|
||||
height={rectHeight.ref.value - 4}
|
||||
anc={[0, 0.5]}
|
||||
circle={[16]}
|
||||
fill
|
||||
stroke
|
||||
fillStyle="rgba(50, 54, 159, 0.7)"
|
||||
strokeStyle="rgba(255, 204, 0, 1)"
|
||||
lineWidth={3}
|
||||
zIndex={0}
|
||||
/>
|
||||
<container
|
||||
hidden={selectHard.value}
|
||||
loc={[0, BUTTONS_HEIGHT / 2, BUTTONS_WIDTH, buttonHeight]}
|
||||
anc={[0, 0.5]}
|
||||
alpha={buttonsAlpha.ref.value}
|
||||
zIndex={5}
|
||||
>
|
||||
{buttons.map((v, i) => {
|
||||
const x = BUTTONS_WIDTH / 2;
|
||||
const y = 30 + i * 40;
|
||||
return (
|
||||
<text
|
||||
text={v.name}
|
||||
font={buttonFont}
|
||||
loc={[x, y, void 0, void 0, 0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
filter={buttonFilter}
|
||||
fillStyle={v.colorTrans.ref.value}
|
||||
onEnter={() => enterMain(i)}
|
||||
onClick={() => clickButton(v.code, i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</container>
|
||||
<container
|
||||
hidden={!selectHard.value}
|
||||
loc={[0, BUTTONS_HEIGHT / 2, BUTTONS_WIDTH, hardHeight]}
|
||||
anc={[0, 0.5]}
|
||||
alpha={buttonsAlpha.ref.value}
|
||||
>
|
||||
{hard.map((v, i) => {
|
||||
const x = BUTTONS_WIDTH / 2;
|
||||
const y = 30 + i * 40;
|
||||
return (
|
||||
<text
|
||||
text={v.name}
|
||||
font={buttonFont}
|
||||
loc={[x, y, void 0, void 0, 0.5, 0.5]}
|
||||
cursor="pointer"
|
||||
filter={buttonFilter}
|
||||
fillStyle={v.colorTrans.ref.value}
|
||||
onEnter={() => enterHard(i)}
|
||||
onClick={() => clickButton(v.code, i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</container>
|
||||
</container>
|
||||
<container
|
||||
zIndex={15}
|
||||
loc={[MAIN_WIDTH - 20, MAIN_HEIGHT - 20, 120, 40, 1, 1]}
|
||||
>
|
||||
<g-rectr
|
||||
loc={[0, 0, 120, 40]}
|
||||
circle={[20]}
|
||||
fillStyle="rgba(0, 0, 0, 0.72)"
|
||||
/>
|
||||
<SoundVolume
|
||||
loc={[20, 0, 40, 40]}
|
||||
cursor="pointer"
|
||||
strokeStyle={soundColor.ref.value}
|
||||
onClick={toggleSound}
|
||||
/>
|
||||
{!fullscreen.value ? (
|
||||
<Fullscreen
|
||||
loc={[60, 0, 40, 40]}
|
||||
onClick={toggleFullscreen}
|
||||
cursor="pointer"
|
||||
/>
|
||||
) : (
|
||||
<ExitFullscreen
|
||||
loc={[60, 0, 40, 40]}
|
||||
onClick={toggleFullscreen}
|
||||
cursor="pointer"
|
||||
/>
|
||||
)}
|
||||
<g-line
|
||||
line={[25, 35, 55, 5]}
|
||||
strokeStyle="gray"
|
||||
lineWidth={3}
|
||||
lineCap="round"
|
||||
zIndex={5}
|
||||
noevent
|
||||
hidden={soundOpened.value}
|
||||
/>
|
||||
</container>
|
||||
</container>
|
||||
);
|
||||
}, gameTitleProps);
|
||||
|
||||
export const GameTitleUI = new GameUI('game-title', GameTitle);
|
||||
5
packages-user/client-modules/src/ui/types.ts
Normal file
5
packages-user/client-modules/src/ui/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { IUIDefaultPropsBase } from '@motajs/system';
|
||||
import { IClientBaseExtended } from '@user/client-base';
|
||||
|
||||
export interface IUIPropsBase
|
||||
extends IUIDefaultPropsBase, IClientBaseExtended {}
|
||||
@ -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<any>;
|
||||
}
|
||||
|
||||
const containerConfig = {
|
||||
props: ['controller']
|
||||
} satisfies SetupComponentOptions<UIContainerProps>;
|
||||
} satisfies SetupComponentOptions<IUIContainerProps>;
|
||||
|
||||
export const UIContainer = defineComponent<UIContainerProps>(props => {
|
||||
export const UIContainer = defineComponent<IUIContainerProps>(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(
|
||||
<b.ui.component
|
||||
{...b.vBind}
|
||||
@ -35,7 +36,7 @@ export const UIContainer = defineComponent<UIContainerProps>(props => {
|
||||
key={v.key}
|
||||
controller={data}
|
||||
instance={v}
|
||||
hidden={v.hidden && !v.alwaysShow}
|
||||
hidden={v.hidden.value}
|
||||
zIndex={i * 5}
|
||||
></v.ui.component>
|
||||
))
|
||||
|
||||
@ -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<any>
|
||||
> implements IUIController<PB> {
|
||||
readonly stack: IUIInstance<UIComponent, PB>[] = shallowReactive([]);
|
||||
readonly backIns: ShallowRef<IUIInstance<UIComponent, PB> | null>;
|
||||
|
||||
export interface IUICustomConfig {
|
||||
/**
|
||||
* 打开一个新的 UI
|
||||
* @param ins 要打开的 UI 实例
|
||||
* @param stack 当前的 UI 栈,还未将 UI 实例加入栈中
|
||||
*/
|
||||
open(ins: IUIInstance, stack: IUIInstance[]): void;
|
||||
background: IGameUI | null = null;
|
||||
active: Ref<boolean> = 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<boolean>;
|
||||
|
||||
/**
|
||||
* 隐藏一个 UI
|
||||
* @param ins 要隐藏的 UI 实例,还未进入隐藏状态
|
||||
* @param stack 当前的 UI 栈
|
||||
* @param index 这个 UI 实例在 UI 栈中的索引
|
||||
*/
|
||||
hide(ins: IUIInstance, stack: IUIInstance[], index: number): void;
|
||||
/** 当前的自定义 UI 控制策略 */
|
||||
private strategy: IUICustomStrategy<PB>;
|
||||
|
||||
/**
|
||||
* 显示一个 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<UIControllerEvent>
|
||||
implements IUIMountable
|
||||
{
|
||||
static controllers: Map<string, UIController> = new Map();
|
||||
|
||||
/** 当前的 ui 栈 */
|
||||
readonly stack: IUIInstance[] = reactive([]);
|
||||
/** UI 显示方式 */
|
||||
mode: UIMode = UIMode.LastOnlyStack;
|
||||
/** 这个 UI 实例的背景,当这个 UI 处于显示模式时,会显示背景 */
|
||||
background?: IGameUI;
|
||||
|
||||
/** 背景 UI 实例 */
|
||||
readonly backIns: ShallowRef<IUIInstance | null> = shallowRef(null);
|
||||
/** 当前是否显示背景 UI */
|
||||
readonly showBack: ComputedRef<boolean> = computed(
|
||||
() => this.userShowBack.value && this.sysShowBack.value
|
||||
);
|
||||
/** 当前 UI 是否激活 */
|
||||
readonly active: Ref<boolean> = ref(false);
|
||||
|
||||
/** 自定义显示模式下的配置信息 */
|
||||
private config?: IUICustomConfig;
|
||||
/** 是否维持背景 UI */
|
||||
private keepBack: boolean = false;
|
||||
/** 用户是否显示背景 UI */
|
||||
private readonly userShowBack: Ref<boolean> = ref(true);
|
||||
private readonly userShowBack: Ref<boolean>;
|
||||
/** 系统是否显示背景 UI */
|
||||
private readonly sysShowBack: Ref<boolean> = ref(false);
|
||||
private readonly sysShowBack: Ref<boolean>;
|
||||
|
||||
/**
|
||||
* 创建一个 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<C extends UIComponent>(
|
||||
background: IGameUI<C> | null,
|
||||
vBind: UIRawProps<C, PB>
|
||||
): IUIInstance<C, PB> | 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<T extends UIComponent>(back: IGameUI<T>, vBind: UIProps<T>) {
|
||||
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<T extends UIComponent>(
|
||||
ui: IGameUI<T>,
|
||||
vBind: UIProps<T>,
|
||||
open<C extends UIComponent>(
|
||||
ui: IGameUI<C>,
|
||||
vBind: UIRawProps<C, PB>,
|
||||
alwaysShow: boolean = false
|
||||
): IUIInstance<T> {
|
||||
): IUIInstance<C, PB> {
|
||||
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<UIComponent, PB>) {
|
||||
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<UIComponent, PB>): 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<UIComponent, PB>): 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<PB>) {
|
||||
this.strategy = stratygy;
|
||||
stratygy.update(this.stack);
|
||||
}
|
||||
}
|
||||
|
||||
class UILastOnlyStrategy implements IUICustomStrategy<any> {
|
||||
/** 是否启用栈模式 */
|
||||
readonly stack: boolean;
|
||||
|
||||
constructor(stack: boolean = true) {
|
||||
this.stack = stack;
|
||||
}
|
||||
|
||||
open(
|
||||
ins: IUIInstance<UIComponent, any>,
|
||||
stack: IUIInstance<UIComponent, any>[]
|
||||
): void {
|
||||
stack.push(ins);
|
||||
stack.forEach(v => v.hide());
|
||||
stack.findLast(v => !v.alwaysShow)?.show();
|
||||
}
|
||||
|
||||
close(
|
||||
_ins: IUIInstance<UIComponent, any>,
|
||||
stack: IUIInstance<UIComponent, any>[],
|
||||
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<UIComponent, any>): void {
|
||||
ins.hide();
|
||||
}
|
||||
|
||||
show(ins: IUIInstance<UIComponent, any>): void {
|
||||
ins.show();
|
||||
}
|
||||
|
||||
update(stack: IUIInstance<UIComponent, any>[]): void {
|
||||
stack.forEach(v => v.hide());
|
||||
stack.findLast(v => !v.alwaysShow)?.show();
|
||||
}
|
||||
}
|
||||
|
||||
class UIShowAllStrategy implements IUICustomStrategy<any> {
|
||||
/** 是否启用栈模式 */
|
||||
readonly stack: boolean;
|
||||
|
||||
constructor(stack: boolean = true) {
|
||||
this.stack = stack;
|
||||
}
|
||||
|
||||
open(
|
||||
ins: IUIInstance<UIComponent, any>,
|
||||
stack: IUIInstance<UIComponent, any>[]
|
||||
): void {
|
||||
stack.push(ins);
|
||||
}
|
||||
|
||||
close(
|
||||
_ins: IUIInstance<UIComponent, any>,
|
||||
stack: IUIInstance<UIComponent, any>[],
|
||||
index: number
|
||||
): void {
|
||||
if (this.stack) {
|
||||
stack.splice(index);
|
||||
} else {
|
||||
stack.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
hide(ins: IUIInstance<UIComponent, any>): void {
|
||||
ins.hide();
|
||||
}
|
||||
|
||||
show(ins: IUIInstance<UIComponent, any>): void {
|
||||
ins.show();
|
||||
}
|
||||
|
||||
update(stack: IUIInstance<UIComponent, any>[]): void {
|
||||
stack.forEach(v => v.show());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export * from './container';
|
||||
export * from './controller';
|
||||
export * from './instance';
|
||||
export * from './shared';
|
||||
export * from './types';
|
||||
export * from './ui';
|
||||
|
||||
@ -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<C extends UIComponent> implements IUIInstance<C> {
|
||||
private static counter: number = 0;
|
||||
/** 自增 UI 实例计数器 */
|
||||
let counter = 0;
|
||||
|
||||
readonly key: number = UIInstance.counter++;
|
||||
export class UIInstance<
|
||||
C extends UIComponent,
|
||||
PB extends IUIDefaultPropsBase<C>
|
||||
> implements IUIInstance<C, PB> {
|
||||
readonly key: number = counter++;
|
||||
readonly ui: IGameUI<C>;
|
||||
hidden: boolean = false;
|
||||
readonly hidden: Ref<boolean> = ref(false);
|
||||
readonly vBind: Reactive<UIRawProps<C, PB>>;
|
||||
readonly alwaysShow: boolean;
|
||||
|
||||
constructor(
|
||||
ui: IGameUI<C>,
|
||||
public vBind: UIProps<C>,
|
||||
public readonly alwaysShow: boolean = false
|
||||
vBind: UIRawProps<C, PB>,
|
||||
alwaysShow: boolean = false
|
||||
) {
|
||||
this.ui = markRaw(ui);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个 UI 实例的响应式数据的值
|
||||
* @param data 要设置的值
|
||||
* @param merge 是将传入的值与原先的值合并(true),还是将当前值覆盖掉原先的值(false),默认合并
|
||||
*/
|
||||
setVBind(data: Partial<Props<C>>, merge: boolean = true) {
|
||||
if (merge) {
|
||||
this.vBind = mergeProps(this.vBind, data) as UIProps<C>;
|
||||
} else {
|
||||
this.vBind = data as UIProps<C>;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
import { Props } from '@motajs/render';
|
||||
import {
|
||||
DefineComponent,
|
||||
DefineSetupFnComponent,
|
||||
Ref,
|
||||
ShallowRef,
|
||||
VNode
|
||||
} from 'vue';
|
||||
|
||||
export type UIComponent = DefineSetupFnComponent<any> | DefineComponent;
|
||||
|
||||
export interface UIComponentProps<T extends UIComponent = UIComponent> {
|
||||
controller: IUIMountable;
|
||||
instance: IUIInstance<T>;
|
||||
}
|
||||
|
||||
export type UIProps<C extends UIComponent = UIComponent> = Omit<
|
||||
Props<C>,
|
||||
keyof UIComponentProps<C>
|
||||
>;
|
||||
|
||||
export interface IGameUI<C extends UIComponent = UIComponent> {
|
||||
/** 这个 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<UIComponent>[];
|
||||
/** 当前的背景 UI */
|
||||
readonly backIns: ShallowRef<IUIInstance<UIComponent> | null>;
|
||||
/** 当前是否显示背景 UI */
|
||||
readonly showBack: Ref<boolean>;
|
||||
|
||||
/**
|
||||
* 隐藏一个 UI
|
||||
* @param ins 要隐藏的 UI 实例
|
||||
*/
|
||||
hide(ins: IUIInstance<UIComponent>): void;
|
||||
|
||||
/**
|
||||
* 显示一个 UI
|
||||
* @param ins 要显示的 UI 实例
|
||||
*/
|
||||
show(ins: IUIInstance<UIComponent>): void;
|
||||
|
||||
/**
|
||||
* 渲染这个 UI,可以直接嵌入渲染树中
|
||||
*/
|
||||
render(): VNode;
|
||||
|
||||
/**
|
||||
* 隐藏背景 UI
|
||||
*/
|
||||
hideBackground(): void;
|
||||
|
||||
/**
|
||||
* 显示背景 UI
|
||||
*/
|
||||
showBackground(): void;
|
||||
|
||||
/**
|
||||
* 打开一个 ui
|
||||
* @param ui 要打开的 ui
|
||||
* @param vBind 传递给这个 ui 的响应式数据
|
||||
* @param alwaysShow 这个 ui 是否保持开启,对于需要叠加显示的 ui 非常有用
|
||||
*/
|
||||
open<T extends UIComponent>(
|
||||
ui: IGameUI<T>,
|
||||
vBind: UIProps<T>,
|
||||
alwaysShow?: boolean
|
||||
): IUIInstance<T>;
|
||||
|
||||
/**
|
||||
* 关闭一个 ui
|
||||
* @param ui 要关闭的 ui 实例
|
||||
*/
|
||||
close(ui: IUIInstance<UIComponent>): void;
|
||||
|
||||
/**
|
||||
* 关闭所有或指定类型的所有 UI
|
||||
*/
|
||||
closeAll(ui?: IGameUI<UIComponent>): void;
|
||||
|
||||
/**
|
||||
* 维持背景,直到下次所有 UI 都被关闭
|
||||
*/
|
||||
keep(): IKeepController;
|
||||
}
|
||||
|
||||
export interface IUIInstance<C extends UIComponent = UIComponent> {
|
||||
/** 这个 ui 实例的唯一 key,用于 vue */
|
||||
readonly key: number;
|
||||
/** 这个 ui 实例的 ui 信息 */
|
||||
readonly ui: IGameUI<C>;
|
||||
/** 传递给这个 ui 实例的响应式数据 */
|
||||
readonly vBind: UIProps<C>;
|
||||
/** 当前元素是否被隐藏 */
|
||||
readonly hidden: boolean;
|
||||
/** 是否永远保持开启 */
|
||||
readonly alwaysShow: boolean;
|
||||
|
||||
/**
|
||||
* 设置这个 UI 实例的响应式数据的值
|
||||
* @param data 要设置的值
|
||||
* @param merge 是将传入的值与原先的值合并(true),还是将当前值覆盖掉原先的值(false),默认合并
|
||||
*/
|
||||
setVBind(data: Partial<Props<C>>, merge?: boolean): void;
|
||||
|
||||
/**
|
||||
* 隐藏这个 ui
|
||||
*/
|
||||
hide(): void;
|
||||
|
||||
/**
|
||||
* 显示这个 ui
|
||||
*/
|
||||
show(): void;
|
||||
}
|
||||
@ -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<string, any>,
|
||||
@ -10,3 +21,222 @@ export type SetupComponentOptions<
|
||||
emits?: E | EE[];
|
||||
slots?: S;
|
||||
};
|
||||
|
||||
export type UIComponent = DefineSetupFnComponent<any> | DefineComponent;
|
||||
|
||||
export interface IUIDefaultPropsBase<T extends UIComponent = UIComponent> {
|
||||
/** 当前打开的 UI 所属的控制器 */
|
||||
readonly controller: IUIController<this>;
|
||||
/** 当前 UI 实例对象 */
|
||||
readonly instance: IUIInstance<T, this>;
|
||||
}
|
||||
|
||||
export interface IGameUI<C extends UIComponent = UIComponent> {
|
||||
/** 这个 UI 的名称 */
|
||||
readonly name: string;
|
||||
/** 这个 UI 的 vue 组件对象 */
|
||||
readonly component: C;
|
||||
}
|
||||
|
||||
export interface IUICustomStrategy<PB extends IUIDefaultPropsBase<any>> {
|
||||
/**
|
||||
* 打开一个新的 UI
|
||||
* @param ins 要打开的 UI 实例
|
||||
* @param stack 当前的 UI 栈,还未将 UI 实例加入栈中
|
||||
*/
|
||||
open(
|
||||
ins: IUIInstance<UIComponent, PB>,
|
||||
stack: IUIInstance<UIComponent, PB>[]
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 关闭一个 UI
|
||||
* @param ins 要关闭的 UI 实例
|
||||
* @param stack 当前的 UI 栈,还未将 UI 实例移除
|
||||
* @param index 这个 UI 实例在 UI 栈中的索引
|
||||
*/
|
||||
close(
|
||||
ins: IUIInstance<UIComponent, PB>,
|
||||
stack: IUIInstance<UIComponent, PB>[],
|
||||
index: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 隐藏一个 UI
|
||||
* @param ins 要隐藏的 UI 实例,还未进入隐藏状态
|
||||
* @param stack 当前的 UI 栈
|
||||
* @param index 这个 UI 实例在 UI 栈中的索引
|
||||
*/
|
||||
hide(
|
||||
ins: IUIInstance<UIComponent, PB>,
|
||||
stack: IUIInstance<UIComponent, PB>[],
|
||||
index: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 显示一个 UI
|
||||
* @param ins 要显示的 UI 实例,还未进入显示状态
|
||||
* @param stack 当前的 UI 栈
|
||||
* @param index 这个 UI 实例在 UI 栈中的索引
|
||||
*/
|
||||
show(
|
||||
ins: IUIInstance<UIComponent, PB>,
|
||||
stack: IUIInstance<UIComponent, PB>[],
|
||||
index: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 更新所有 UI 的显示状态,一般会在显示模式更改时调用,用于初始化显示模式的显示状态
|
||||
* @param stack 当前的 UI 栈
|
||||
*/
|
||||
update(stack: IUIInstance<UIComponent, PB>[]): void;
|
||||
}
|
||||
|
||||
export interface IKeepController {
|
||||
/**
|
||||
* 安全关闭背景 UI,如果当前没有 UI 已开启,那么直接关闭,否则维持
|
||||
*/
|
||||
safelyUnload(): void;
|
||||
|
||||
/**
|
||||
* 不论当前是否有 UI 已开启,都关闭背景
|
||||
*/
|
||||
unload(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UI 组件在排除内置基础参数后的剩余参数类型
|
||||
*/
|
||||
export type UIRawProps<
|
||||
C extends UIComponent,
|
||||
PB extends IUIDefaultPropsBase<any>
|
||||
> = Omit<Props<C>, keyof PB>;
|
||||
|
||||
export interface IUIController<PB extends IUIDefaultPropsBase<any>> {
|
||||
/** 当前的 UI 栈 */
|
||||
readonly stack: IUIInstance[];
|
||||
/** 背景 UI 对象,当显示背景 UI 时,会显示此 UI 对应的实例 */
|
||||
readonly background: IGameUI | null;
|
||||
|
||||
/** 当前的背景 UI 实例的浅层响应式对象 */
|
||||
readonly backIns: ShallowRef<IUIInstance | null>;
|
||||
/** 当前 UI 是否已经被激活的响应式对象,即至少有一个 UI 正在显示,或处在 `keep` 环境下 */
|
||||
readonly active: Ref<boolean>;
|
||||
/** 当前是否显示背景的响应式对象,用于容器响应式判断是否需要显示背景 */
|
||||
readonly showBack: Ref<boolean>;
|
||||
|
||||
/**
|
||||
* 设置控制器的背景 UI
|
||||
* @param background 背景 UI
|
||||
* @param vBind 当打开背景时,传递给背景的 VBind 参数
|
||||
* @returns 背景对应的 UI 实例
|
||||
*/
|
||||
setBackground<C extends UIComponent>(
|
||||
background: IGameUI<C> | null,
|
||||
vBind: UIRawProps<C, PB>
|
||||
): IUIInstance<C, PB> | null;
|
||||
|
||||
/**
|
||||
* 当前是否显示背景 UI
|
||||
*/
|
||||
showingBack(): boolean;
|
||||
|
||||
/**
|
||||
* 隐藏背景 UI
|
||||
*/
|
||||
hideBackground(): void;
|
||||
|
||||
/**
|
||||
* 显示背景 UI
|
||||
*/
|
||||
showBackground(): void;
|
||||
|
||||
/**
|
||||
* 隐藏一个 UI
|
||||
* @param ins 要隐藏的 UI 实例
|
||||
*/
|
||||
hide(ins: IUIInstance<UIComponent, PB>): void;
|
||||
|
||||
/**
|
||||
* 显示一个 UI
|
||||
* @param ins 要显示的 UI 实例
|
||||
*/
|
||||
show(ins: IUIInstance<UIComponent, PB>): void;
|
||||
|
||||
/**
|
||||
* 打开一个 ui
|
||||
* @param ui 要打开的 ui
|
||||
* @param vBind 传递给这个 ui 的响应式数据
|
||||
* @param alwaysShow 这个 ui 是否保持开启,对于需要叠加显示的 ui 非常有用
|
||||
*/
|
||||
open<C extends UIComponent>(
|
||||
ui: IGameUI<C>,
|
||||
vBind: UIRawProps<C, PB>,
|
||||
alwaysShow?: boolean
|
||||
): IUIInstance<C, PB>;
|
||||
|
||||
/**
|
||||
* 关闭一个 ui
|
||||
* @param ui 要关闭的 ui 实例
|
||||
*/
|
||||
close(ui: IUIInstance<UIComponent, PB>): 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<PB>): void;
|
||||
}
|
||||
|
||||
export interface IUIInstance<
|
||||
C extends UIComponent = UIComponent,
|
||||
PB extends IUIDefaultPropsBase<C> = IUIDefaultPropsBase<C>
|
||||
> {
|
||||
/** 这个 ui 实例的唯一 key,用于 vue */
|
||||
readonly key: number;
|
||||
/** 这个 ui 实例的 ui 信息 */
|
||||
readonly ui: IGameUI<C>;
|
||||
/** 传递给这个 ui 实例的响应式数据 */
|
||||
readonly vBind: Reactive<UIRawProps<C, PB>>;
|
||||
/** 当前元素是否被隐藏 */
|
||||
readonly hidden: Ref<boolean>;
|
||||
/** 是否永远保持开启 */
|
||||
readonly alwaysShow: boolean;
|
||||
|
||||
/**
|
||||
* 隐藏这个 ui
|
||||
*/
|
||||
hide(): void;
|
||||
|
||||
/**
|
||||
* 显示这个 ui
|
||||
*/
|
||||
show(): void;
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { IGameUI, UIComponent } from './shared';
|
||||
import { IGameUI, UIComponent } from './types';
|
||||
|
||||
export class GameUI<C extends UIComponent> implements IGameUI<C> {
|
||||
// TODO: 这个静态成员是否可以考虑删除?
|
||||
|
||||
/** 当前的所有已声明 UI,用于全局性获取 */
|
||||
static list: Map<string, GameUI<UIComponent>> = new Map();
|
||||
|
||||
constructor(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user