feat: 提交资源

This commit is contained in:
han_han9
2025-10-28 21:55:41 +08:00
parent 591f398085
commit 55c4fcd9ae
2146 changed files with 172747 additions and 456 deletions

3
extensions/max-studio/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
dist
node_modules
temp

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,5 @@
registry=https://nexus.yangjie.link/repository/npm-snapshots/
//nexus.yangjie.link/repository/npm-snapshots/:username=${NPM_USERNAME}
//nexus.yangjie.link/repository/npm-snapshots/:_password=${NPM_PASSWORD}
//nexus.yangjie.link/repository/npm-snapshots/:email=${NPM_EMAIL}
//nexus.yangjie.link/repository/npm-snapshots/:always-auth=true

View File

@@ -0,0 +1,5 @@
registry=https://nexus.yangjie.link/repository/npm-releases/
//nexus.yangjie.link/repository/npm-releases/:username=${NPM_USERNAME}
//nexus.yangjie.link/repository/npm-releases/:_password=${NPM_PASSWORD}
//nexus.yangjie.link/repository/npm-releases/:email=${NPM_EMAIL}
//nexus.yangjie.link/repository/npm-releases/:always-auth=true

View File

@@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "面板数据 / Panel data",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"type": "object",
"description": "面板名 / Panel name",
"properties": {
"title": {
"type": "string",
"default": "Default Panel",
"description": "面板标题,支持 i18n:key / Panel title, support for i18n:key (required)"
},
"main": {
"type": "string",
"default": "dist/panels/default/index.js",
"description": "入口函数 / Entry function (required)"
},
"icon": {
"type": "string",
"description": "面板图标存放相对目录 / Relative directory for panel icon storage"
},
"type": {
"type": "string",
"enum": ["dockable", "simple"],
"default": "dockable",
"description": "面板类型dockable | simple / Panel type (dockable | simple)"
},
"flags": {
"type": "object",
"properties": {
"resizable": {
"type": "boolean",
"default": true,
"description": "是否可以改变大小,默认 true / Whether the size can be changed, default true"
},
"save": {
"type": "boolean",
"default": true,
"description": "是否需要保存,默认 false / Whether to save, default false"
},
"alwaysOnTop": {
"type": "boolean",
"default": true,
"description": "是否保持顶层显示,默认 false / Whether to keep the top level display, default false"
}
}
},
"size": {
"type": "object",
"description": "面板大小信息 / Panel size information",
"properties": {
"min-width": {
"type": "number",
"default": 200,
"description": "面板最小宽度 / Minimum panel width"
},
"min-height": {
"type": "number",
"default": 200,
"description": "面板最小高度 / Minimum panel height"
},
"width": {
"type": "number",
"default": 400,
"description": " 面板默认宽度 / Panel Default Width"
},
"height": {
"type": "number",
"default": 600,
"description": "面板默认高度 / Panel Default Height"
}
}
}
},
"required": ["title", "main"]
}
}
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "其他扩展插件的扩展配置 / Extended configuration for other extension plugins",
"properties": {
},
"required": []
}

View File

@@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "插件定义文件 / Extension definition file",
"properties": {
"author": {
"type": "string",
"description": "作者 / Author",
"default": "Cocos Creator Developer"
},
"contributions": {
"$ref": "./contributions/index.json"
},
"dependencies": {
"type": "object",
"description": "发布时所需的依赖库 / Dependencies required for publishing"
},
"description": {
"type": "string",
"description": "简要介绍扩展关键特性、用途,支持 i18n / Brief introduction of the key features and uses of the extension, supporting i18n"
},
"devDependencies": {
"type": "object",
"description": "开发时所需的依赖库 / Dependencies required for development"
},
"editor": {
"type": "string",
"description": "支持的 Cocos Creator 编辑器版本,支持 semver 格式 / Supported Cocos Creator editor version, supporting semver format"
},
"main": {
"type": "string",
"description": "入口函数 / Entry function",
"default": "./dist/index.js"
},
"name": {
"type": "string",
"description": "不能以 _ 或 . 开头、不能含有大写字母,也不能含有 URL 的非法字符例如 .、' 和 ,。 / Cannot start with _ or., cannot contain uppercase letters, and cannot contain URL illegal characters such as.,'and,",
"default": "Custom Extension"
},
"package_version": {
"type": "number",
"description": "扩展系统预留版本号 / Extension system reserved version number",
"default": 2
},
"panels": {
"$ref": "./base/panels.json"
},
"scripts": {
"type": "object",
"description": "NPM 脚本 / NPM scripts"
},
"version": {
"type": "string",
"description": "版本号字符串 / Version number string",
"default": "1.0.0"
}
},
"required": [
"author",
"name",
"package_version",
"version"
]
}

12
extensions/max-studio/README.md Executable file
View File

@@ -0,0 +1,12 @@
# Project Title
An blank extension.
## Install
```bash
# Install dependent modules
npm install
# build
npm run build
```

View File

@@ -0,0 +1,12 @@
# 项目简介
一份空白的扩展。
## 安装
```bash
# 安装依赖模块
npm install
# 构建
npm run build
```

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "2b36711d-b919-4ff7-8e47-c108085a99c9",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,13 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e95a5a7d-79a7-4056-97d7-5c000bf3caa9",
"files": [],
"subMetas": {},
"userData": {
"isBundle": true,
"bundleName": "max-core",
"priority": 7
}
}

View File

@@ -0,0 +1,35 @@
import { Component, director, js, Node } from "cc";
export default class NodeSingleton extends Component {
private static readonly _instances: Map<
new () => NodeSingleton,
NodeSingleton
> = new Map();
public static getInstance<T extends NodeSingleton>(this: new () => T): T {
if (!NodeSingleton._instances.has(this)) {
let instance: T | null = null;
const scene = director.getScene();
if (scene) {
instance = scene.getComponentInChildren(this);
}
if (!instance) {
const node = new Node(js.getClassName(this));
instance = node.addComponent(this);
scene.addChild(node);
director.addPersistRootNode(node);
}
NodeSingleton._instances.set(this, instance);
}
return NodeSingleton._instances.get(this) as T;
}
public static clearInstance<T extends NodeSingleton>(
this: new () => T,
): void {
NodeSingleton._instances.delete(this);
}
public static clearAll(): void {
this._instances.clear();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c33f6c95-6164-4b52-9252-0881713f7372",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,212 @@
import { js } from "cc";
import LogUtils from "./utils/LogUtils";
import TimeUtils from "./utils/TimeUtils";
import { EDITOR } from "cc/env";
// 移除这行导入,避免循环引用
// import { EventManager, GameEventMessage } from "./event/EventManager";
/**
* 单例装饰器配置接口
*/
export interface SingletonDecoratorOptions {
/** 是否自动实例化 */
auto?: boolean;
/** 分类标签 */
category?: string;
/** 描述信息 */
description?: string;
/** 初始化优先级(数字越小优先级越高) */
priority?: number;
}
export abstract class Singleton {
public isInit: boolean = false;
private static readonly _instances: Map<new () => Singleton, Singleton> = new Map();
public constructor() {
const constructor = this.constructor as new () => Singleton;
if (Singleton._instances.has(constructor)) {
throw new Error("请使用getInstance()获取实例,不要直接实例化!");
}
}
public static getInstance<T extends Singleton>(this: new () => T): T {
if (!Singleton._instances.has(this)) {
const instance = new this();
Singleton._instances.set(this, instance);
instance
.onInit()
.then(() => {
instance.isInit = true;
})
.catch((err) => {
LogUtils.error("Singleton", `初始化失败[${js.getClassName(this)}]:`, err);
});
}
return Singleton._instances.get(this) as T;
}
public static clearInstance<T extends Singleton>(this: new () => T): void {
const instance = Singleton._instances.get(this);
if (instance) {
instance.onRelease();
}
Singleton._instances.delete(this);
}
public static clearAll(): void {
Singleton._instances.clear();
}
protected async onInit() {}
protected onRelease() {}
}
/**
* 单例注册表,管理所有装饰的单例类
*/
export class SingletonRegistry {
private static readonly _registeredClasses: Map<new () => Singleton, SingletonDecoratorOptions> = new Map();
private static readonly _autoInstanceClasses: Array<{
cls: new () => Singleton;
priority: number;
options: SingletonDecoratorOptions;
}> = [];
private static _isInitialized: boolean = false;
/**
* 注册单例类
*/
public static register(cls: new () => Singleton, options: SingletonDecoratorOptions = {}): void {
const finalOptions: SingletonDecoratorOptions = {
auto: false,
priority: 100,
...options,
};
this._registeredClasses.set(cls, finalOptions);
if (finalOptions.auto) {
this._autoInstanceClasses.push({
cls,
priority: finalOptions.priority || 100,
options: finalOptions,
});
// 按优先级排序
this._autoInstanceClasses.sort((a, b) => a.priority - b.priority);
}
LogUtils.debug("SingletonRegistry", `注册单例类: ${js.getClassName(cls)}, 自动实例化: ${finalOptions.auto}`);
}
/**
* 初始化所有需要自动实例化的单例
*/
public static async initializeAutoInstances(): Promise<void> {
if (this._isInitialized) {
LogUtils.warn("SingletonRegistry", "自动实例化已经执行过,跳过重复执行");
return;
}
LogUtils.info("SingletonRegistry", `开始自动实例化 ${this._autoInstanceClasses.length} 个单例类`);
const promises = this._autoInstanceClasses.map(async (item) => {
try {
const className = js.getClassName(item.cls);
LogUtils.debug("SingletonRegistry", `正在实例化: ${className} (优先级: ${item.priority})`);
const instance: Singleton = (<any>item.cls).getInstance();
await TimeUtils.waitFor(() => instance.isInit, 100, 30000);
LogUtils.info("SingletonRegistry", `成功实例化: ${className}`);
} catch (err) {
const className = js.getClassName(item.cls);
LogUtils.error("SingletonRegistry", `自动实例化失败: ${className}`, err);
}
});
await Promise.all(promises);
this._isInitialized = true;
//抛出通用事件, 初始化已经结束
LogUtils.info("SingletonRegistry", "所有自动实例化单例类初始化完成");
}
/**
* 获取所有注册的类
*/
public static getAllRegisteredClasses(): Array<{
cls: new () => Singleton;
options: SingletonDecoratorOptions;
className: string;
isInstantiated: boolean;
}> {
const result: Array<any> = [];
for (const [cls, options] of this._registeredClasses.entries()) {
result.push({
cls,
options,
className: js.getClassName(cls),
isInstantiated: (cls as any)._instances?.has(cls) || false,
});
}
return result;
}
/**
* 根据分类获取类
*/
public static getClassesByCategory(category: string): Array<new () => Singleton> {
const result: Array<new () => Singleton> = [];
for (const [cls, options] of this._registeredClasses.entries()) {
if (options.category === category) {
result.push(cls);
}
}
return result;
}
/**
* 打印所有注册的类信息
*/
public static printAllClasses(): void {
LogUtils.info("SingletonRegistry", "=== 所有注册的单例类 ===");
for (const [cls, options] of this._registeredClasses.entries()) {
const className = js.getClassName(cls);
const isInstantiated = (cls as any)._instances?.has(cls) || false;
LogUtils.info(
"SingletonRegistry",
`${className} | 分类: ${options.category || "无"} | ` +
`自动实例化: ${options.auto ? "是" : "否"} | ` +
`优先级: ${options.priority || 100} | ` +
`状态: ${isInstantiated ? "已实例化" : "未实例化"} | ` +
`描述: ${options.description || "无"}`,
);
}
}
/**
* 重置初始化状态(用于测试)
*/
public static resetInitializationState(): void {
this._isInitialized = false;
}
}
/**
* 单例装饰器
* @param options 装饰器配置选项
*/
export function singleton(options: SingletonDecoratorOptions = {}) {
return function <T extends new () => Singleton>(constructor: T): T {
if (!EDITOR) {
SingletonRegistry.register(constructor, options);
}
return constructor;
};
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5136e9d4-0618-4f13-bc55-a759b2ea97fb",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "31f646f2-7047-4e2b-b958-60dd501ec95f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,106 @@
import { EventManager, GameEventMessage } from "../event/EventManager";
import { NetworkManager } from "../net/NetworkManager";
import { singleton, Singleton } from "../Singleton";
import StorageManager from "../storage/StorageManager";
import { Popup } from "../ui/default/DefaultPopup";
import LogUtils from "../utils/LogUtils";
import { IAccountInfo } from "./Types";
@singleton()
export default class AccountManager extends Singleton {
/** 账号信息 */
accountInfo: IAccountInfo | null = null;
/** 初始化 */
public async onInit() {
EventManager.getInstance().on(
GameEventMessage.GAME_CORE_INITIALIZED,
this.onGameCoreInitialized,
this,
);
}
private onGameCoreInitialized() {
void this.checkLogin();
}
private async checkLogin(isDialog: boolean = false) {
if (AccountManager.getInstance().accountInfo) {
EventManager.getInstance().emit(GameEventMessage.GAME_LOGIN, true);
return;
}
if (StorageManager.getInstance().account == null) {
Popup.show({
content: "请先注册",
confirm: "注册",
onConfirm: async () => {
await AccountManager.getInstance().register();
void this.checkLogin(true);
},
cancel: "取消",
onCancel: async () => {
void this.checkLogin(true);
},
});
} else {
if (!isDialog) {
await AccountManager.getInstance().login(
StorageManager.getInstance().account,
);
void this.checkLogin(true);
return;
}
Popup.show({
content: "请先登录",
confirm: "登录",
onConfirm: async () => {
await AccountManager.getInstance().login(
StorageManager.getInstance().account,
);
void this.checkLogin(true);
},
cancel: "取消",
onCancel: async () => {
void this.checkLogin(true);
},
});
}
}
public async register() {
const rsp = await NetworkManager.getInstance()
.getHttpClient()
.send<IAccountInfo>("/user/register", "POST", {
nickName: "未命名",
avatar: "https://avatars.githubusercontent.com/u/59302977",
});
if (rsp.success) {
this.accountInfo = rsp.data;
StorageManager.getInstance().account = this.accountInfo;
StorageManager.getInstance().loadData(
this.accountInfo.uid,
(<any>rsp.data).playerData,
);
LogUtils.log("注册成功", this.accountInfo);
}
}
public async login(accountInfo: IAccountInfo) {
const rsp = await NetworkManager.getInstance()
.getHttpClient()
.send<IAccountInfo>("/user/login", "POST", {
uid: accountInfo.uid,
});
if (rsp.success) {
this.accountInfo = rsp.data;
StorageManager.getInstance().account = this.accountInfo;
StorageManager.getInstance().loadData(
this.accountInfo.uid,
(<any>rsp.data).playerData,
);
LogUtils.log("登录成功", this.accountInfo);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "32bdb189-b596-4538-b022-cd9327f89b3e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,7 @@
import { IResponseData } from "../net/HttpRequest";
export interface IAccountInfo extends IResponseData {
uid: string;
nickName: string;
avatar: string;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b377f38b-3dbc-49a1-b34e-704b02322e2f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "2a22b5fa-076a-4f5f-852d-c018e5b09586",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,323 @@
import { NodeEventType } from "cc";
import { Node } from "cc";
import { EventManager } from "../event/EventManager";
/**
* 事件装饰器,用于自动绑定事件监听器
* 支持两种事件类型:
* 1. Node 节点内置事件NodeEventType- 使用 Node 的事件系统,自动选择事件目标
* 2. 自定义事件(字符串)- 使用 EventManager 全局事件系统
*
* 适用场景:
* - Cocos Creator 组件类:
* Node 事件会绑定到组件的 node自定义事件使用 EventManager
* 会在 onLoad 之后注册相关事件、onDestroy 之前注销相关事件
* - 普通类:
* 只支持自定义事件,使用 EventManager
* 会在构造函数中注册相关事件、不会主动销毁相关事件,需要手动添加 offEvent 装饰器注销事件
*
* @param key 事件类型,可以是字符串或 NodeEventType
* @param once 是否只监听一次,默认 false
*/
// 用于标记是否已经重写过生命周期方法
const LIFECYCLE_WRAPPED_SYMBOL = Symbol('lifecycleWrapped');
export function onEvent(
key: string | NodeEventType,
once: boolean = false,
) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// 保存事件绑定信息到类原型上
if (!target.__eventBindings) {
target.__eventBindings = [];
}
target.__eventBindings.push({
key,
once,
method: propertyKey,
originalMethod
});
// 只在第一次使用装饰器时重写生命周期方法
if (!target[LIFECYCLE_WRAPPED_SYMBOL]) {
target[LIFECYCLE_WRAPPED_SYMBOL] = true;
// 检查是否为 Cocos Creator 组件类
const isComponent = isCocosCreatoComponent(target);
if (isComponent) {
// Cocos Creator 组件:重写 onLoad 和 onDestroy
setupComponentLifecycle(target);
} else {
// 普通类:重写构造函数和添加销毁方法
setupPlainClassLifecycle(target);
}
}
return descriptor;
};
}
/**
* 判断是否为 Cocos Creator 组件类
*/
function isCocosCreatoComponent(target: any): boolean {
// 检查原型链中是否有 Component 相关的方法
let proto = target;
while (proto) {
if (proto.onLoad || proto.onDestroy || proto.node !== undefined) {
return true;
}
proto = Object.getPrototypeOf(proto);
if (proto === Object.prototype) break;
}
return false;
}
/**
* 为 Cocos Creator 组件设置生命周期
*/
function setupComponentLifecycle(target: any) {
// 保存原始的生命周期方法
const originalOnLoad = target.onLoad;
const originalOnDestroy = target.onDestroy;
// 重写 onLoad 方法来自动绑定事件
target.onLoad = function () {
// 调用原始的 onLoad
if (originalOnLoad) {
originalOnLoad.call(this);
}
// 绑定所有装饰器标记的事件
bindEvents.call(this);
};
// 重写 onDestroy 方法来自动解绑事件
target.onDestroy = function () {
// 解绑所有装饰器标记的事件
unbindEvents.call(this);
// 调用原始的 onDestroy
if (originalOnDestroy) {
originalOnDestroy.call(this);
}
};
}
/**
* 为普通类设置生命周期
*/
function setupPlainClassLifecycle(target_: any) {
// 保存原始构造函数
const originalConstructor = target_.constructor;
// 重写构造函数来自动绑定事件
target_.constructor = function (...args: any[]) {
// 调用原始构造函数
const result = originalConstructor.apply(this, args);
// 绑定事件(延迟到下一个事件循环,确保对象完全初始化)
setTimeout(() => {
bindEvents.call(this);
}, 0);
return result;
};
}
/**
* 绑定事件
*/
function bindEvents(this: any) {
if (this.__eventBindings) {
this.__eventBindings.forEach((binding: any) => {
const method = this[binding.method].bind(this);
if (isNodeEvent(binding.key)) {
// Node 节点内置事件,使用 Node 的事件系统
const eventTarget = getNodeEventTarget.call(this);
if (eventTarget) {
if (binding.once) {
eventTarget.once(binding.key, method);
} else {
eventTarget.on(binding.key, method);
}
} else {
console.warn(`无法为 Node 事件 "${binding.key}" 找到有效的事件目标`);
}
} else {
// 自定义事件,使用 EventManager
const eventManager = EventManager.getInstance();
if (binding.once) {
eventManager.once(binding.key, method, this);
} else {
eventManager.on(binding.key, method, this);
}
}
});
}
}
/**
* 解绑事件
*/
function unbindEvents(this: any) {
if (this.__eventBindings) {
this.__eventBindings.forEach((binding: any) => {
const method = this[binding.method].bind(this);
if (isNodeEvent(binding.key)) {
// Node 节点内置事件,使用 Node 的事件系统解绑
const eventTarget = getNodeEventTarget.call(this);
if (eventTarget) {
eventTarget.off(binding.key, method);
}
} else {
// 自定义事件,使用 EventManager 解绑
const eventManager = EventManager.getInstance();
eventManager.off(binding.key, method, this);
}
});
}
}
/**
* 获取 Node 事件的目标对象
*/
function getNodeEventTarget(this: any): any {
// 优先使用组件的 node
if (this.node) {
return this.node;
}
return this;
}
/**
* 主动删除事件监听装饰器
* @param key 可选,指定要删除的事件类型。如果不传则删除所有事件监听
*/
export function offEvent(key?: string | NodeEventType) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// 重写方法,在调用时执行事件解绑逻辑
descriptor.value = function (...args: any[]) {
// 先调用原始方法
const result = originalMethod.apply(this, args);
// 执行事件解绑
if (key) {
// 删除指定事件
removeSpecificEvent.call(this, key);
} else {
// 删除所有事件
removeAllEvents.call(this);
}
return result;
};
return descriptor;
};
}
/**
* 删除指定事件的监听
*/
function removeSpecificEvent(this: any, key: string | NodeEventType) {
if (!this.__eventBindings) {
return;
}
// 找到匹配的事件绑定
const bindingsToRemove = this.__eventBindings.filter((binding: any) => binding.key === key);
bindingsToRemove.forEach((binding: any) => {
const method = this[binding.method].bind(this);
if (isNodeEvent(binding.key)) {
// Node 节点内置事件
const eventTarget = getNodeEventTarget.call(this);
if (eventTarget) {
eventTarget.off(binding.key, method);
}
} else {
// 自定义事件
const eventManager = EventManager.getInstance();
eventManager.off(binding.key, method, this);
}
});
// 从绑定列表中移除
this.__eventBindings = this.__eventBindings.filter((binding: any) => binding.key !== key);
console.log(`已删除事件监听: ${key}`);
}
/**
* 删除所有事件监听
*/
function removeAllEvents(this: any) {
if (!this.__eventBindings) {
return;
}
const bindingsToRemove = [...this.__eventBindings];
bindingsToRemove.forEach((binding: any) => {
const method = this[binding.method].bind(this);
if (isNodeEvent(binding.key)) {
// Node 节点内置事件
const eventTarget = getNodeEventTarget.call(this);
if (eventTarget) {
eventTarget.off(binding.key, method);
}
} else {
// 自定义事件
const eventManager = EventManager.getInstance();
eventManager.off(binding.key, method, this);
}
});
// 清空绑定列表
this.__eventBindings = [];
console.log('已删除所有事件监听');
}
/**
* 判断是否为 Node 节点内置事件
* @param key 事件键
* @returns 是否为 Node 事件
*/
function isNodeEvent(key: string | NodeEventType): boolean {
// 检查是否为 NodeEventType 枚举值
if (typeof key === 'string') {
// 检查是否为 Node 内置事件字符串
const nodeEvents = [
// 触摸事件
'touch-start', 'touch-move', 'touch-end', 'touch-cancel',
// 鼠标事件
'mouse-down', 'mouse-move', 'mouse-up', 'mouse-enter', 'mouse-leave', 'mouse-wheel',
// 键盘事件
'key-down', 'key-up',
// 节点事件
'position-changed', 'rotation-changed', 'scale-changed', 'size-changed',
'anchor-changed', 'color-changed', 'child-added', 'child-removed', 'child-reorder',
// 组件事件
'active-in-hierarchy-changed',
// 其他常见 Node 事件
'node-destroyed'
];
return nodeEvents.includes(key);
}
// 如果是 NodeEventType 枚举,则为 Node 事件
return Object.values(NodeEventType).includes(key as NodeEventType);
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c4bfe233-9d4c-4fb1-93f7-6cc890aa5f16",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,24 @@
import { Constructor } from "cc";
const LAZY_PREFIX = "__lazy_";
export function lazy(type: Constructor, ...args: any[]): any {
return function (
target: any,
propertyKey: any,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const getter = function () {
if (!this[LAZY_PREFIX + propertyKey]) {
this[LAZY_PREFIX + propertyKey] = new type(...args);
}
return this[LAZY_PREFIX + propertyKey];
};
const setter = function (newValue: any) {
this[LAZY_PREFIX + propertyKey] = newValue;
};
delete (descriptor as any).initializer;
delete descriptor.writable;
return Object.assign(descriptor, { get: getter, set: setter });
};
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "efb635a3-0418-454c-bb4c-e11d56968aa2",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,29 @@
import { EventManager } from "../event/EventManager";
export const NOTIFY_PREFIX = "__notify_";
export function notify(eventName?: string): any {
return function (
target: any,
propertyKey: any,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const getter = function () {
return this[NOTIFY_PREFIX + propertyKey];
};
const setter = function (newValue: any) {
if (newValue == this[NOTIFY_PREFIX + propertyKey]) {
return;
}
if (eventName == undefined) {
eventName = target.constructor.name + "." + propertyKey;
}
this[NOTIFY_PREFIX + propertyKey] = newValue;
console.log(`Event ${eventName} emitted with value ${newValue}`);
EventManager.getInstance().emit(eventName, newValue);
};
delete (descriptor as any).initializer;
delete descriptor.writable;
return Object.assign(descriptor, { get: getter, set: setter });
};
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "02132acf-12b0-4fd5-a798-1d6b0ba097dd",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,318 @@
/**
* 性能优化相关装饰器
* 包含节流(throttle)和防抖(debounce)装饰器
*/
/**
* 节流装饰器 - 在指定时间间隔内最多执行一次
* @param delay 节流间隔时间(毫秒)
* @param options 配置选项
*/
export function throttle(delay: number, options: { leading?: boolean; trailing?: boolean } = {}) {
const { leading = true, trailing = true } = options;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let lastCallTime = 0;
let timeoutId: any = null;
let lastArgs: any[] = [];
let lastThis: any = null;
descriptor.value = function (...args: any[]) {
const now = Date.now();
const timeSinceLastCall = now - lastCallTime;
lastArgs = args;
lastThis = this;
const callFunction = () => {
lastCallTime = Date.now();
return originalMethod.apply(this, args);
};
// 如果是第一次调用且允许立即执行
if (lastCallTime === 0 && leading) {
return callFunction();
}
// 如果距离上次调用时间超过延迟时间
if (timeSinceLastCall >= delay) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
return callFunction();
}
// 如果允许尾随执行且没有待执行的定时器
if (trailing && !timeoutId) {
timeoutId = setTimeout(() => {
timeoutId = null;
if (trailing) {
lastCallTime = Date.now();
originalMethod.apply(lastThis, lastArgs);
}
}, delay - timeSinceLastCall);
}
};
return descriptor;
};
}
/**
* 防抖装饰器 - 延迟执行,如果在延迟期间再次调用则重新计时
* @param delay 防抖延迟时间(毫秒)
* @param options 配置选项
*/
export function debounce(delay: number, options: { immediate?: boolean } = {}) {
const { immediate = false } = options;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let timeoutId: any = null;
let hasBeenCalled = false;
descriptor.value = function (...args: any[]) {
const callNow = immediate && !hasBeenCalled;
// 清除之前的定时器
if (timeoutId) {
clearTimeout(timeoutId);
}
// 如果是立即执行模式且是第一次调用
if (callNow) {
hasBeenCalled = true;
const result = originalMethod.apply(this, args);
// 设置定时器重置状态
timeoutId = setTimeout(() => {
hasBeenCalled = false;
timeoutId = null;
}, delay);
return result;
}
// 设置延迟执行
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) {
originalMethod.apply(this, args);
} else {
hasBeenCalled = false;
}
}, delay);
};
return descriptor;
};
}
/**
* 高级节流装饰器 - 支持异步方法和返回值处理
* @param delay 节流间隔时间(毫秒)
* @param options 配置选项
*/
export function advancedThrottle(delay: number, options: {
leading?: boolean;
trailing?: boolean;
maxWait?: number;
} = {}) {
const { leading = true, trailing = true, maxWait } = options;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let lastCallTime = 0;
let lastInvokeTime = 0;
let timeoutId: any = null;
let maxTimeoutId: any = null;
let lastArgs: any[] = [];
let lastThis: any = null;
let result: any;
const invokeFunc = (time: number) => {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = [];
lastThis = null;
lastInvokeTime = time;
result = originalMethod.apply(thisArg, args);
return result;
};
const leadingEdge = (time: number) => {
lastInvokeTime = time;
timeoutId = setTimeout(timerExpired, delay);
return leading ? invokeFunc(time) : result;
};
const remainingWait = (time: number) => {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = delay - timeSinceLastCall;
return maxWait !== undefined
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
};
const shouldInvoke = (time: number) => {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return (lastCallTime === 0 ||
timeSinceLastCall >= delay ||
timeSinceLastCall < 0 ||
(maxWait !== undefined && timeSinceLastInvoke >= maxWait));
};
const timerExpired = () => {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timeoutId = setTimeout(timerExpired, remainingWait(time));
};
const trailingEdge = (time: number) => {
timeoutId = null;
if (trailing && lastArgs.length) {
return invokeFunc(time);
}
lastArgs = [];
lastThis = null;
return result;
};
descriptor.value = function (...args: any[]) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timeoutId === null) {
return leadingEdge(lastCallTime);
}
if (maxWait !== undefined) {
timeoutId = setTimeout(timerExpired, delay);
return invokeFunc(lastCallTime);
}
}
if (timeoutId === null) {
timeoutId = setTimeout(timerExpired, delay);
}
return result;
};
return descriptor;
};
}
/**
* 高级防抖装饰器 - 支持异步方法和取消功能
* @param delay 防抖延迟时间(毫秒)
* @param options 配置选项
*/
export function advancedDebounce(delay: number, options: {
immediate?: boolean;
maxWait?: number;
} = {}) {
const { immediate = false, maxWait } = options;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
let timeoutId: any = null;
let maxTimeoutId: any = null;
let lastCallTime = 0;
let lastInvokeTime = 0;
let lastArgs: any[] = [];
let lastThis: any = null;
let result: any;
const invokeFunc = (time: number) => {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = [];
lastThis = null;
lastInvokeTime = time;
result = originalMethod.apply(thisArg, args);
return result;
};
const shouldInvoke = (time: number) => {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return (lastCallTime === 0 ||
timeSinceLastCall >= delay ||
timeSinceLastCall < 0 ||
(maxWait !== undefined && timeSinceLastInvoke >= maxWait));
};
const timerExpired = () => {
const time = Date.now();
if (shouldInvoke(time)) {
timeoutId = null;
if (lastArgs.length) {
return invokeFunc(time);
}
return result;
}
timeoutId = setTimeout(timerExpired, delay);
};
descriptor.value = function (...args: any[]) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking && timeoutId === null) {
if (immediate) {
result = invokeFunc(time);
timeoutId = setTimeout(() => {
timeoutId = null;
}, delay);
return result;
}
}
if (timeoutId) {
clearTimeout(timeoutId);
}
if (maxTimeoutId) {
clearTimeout(maxTimeoutId);
}
timeoutId = setTimeout(timerExpired, delay);
if (maxWait !== undefined && maxTimeoutId === null) {
maxTimeoutId = setTimeout(() => {
maxTimeoutId = null;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (lastArgs.length) {
invokeFunc(Date.now());
}
}, maxWait);
}
return result;
};
return descriptor;
};
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "acacdae2-120a-4f5c-b81c-8a7feaec6926",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,15 @@
/**
* 装饰器模块导出
*/
// 事件相关装饰器
export * from './Event';
// 性能优化装饰器
export * from './Performance';
// 懒加载装饰器
export * from './Lazy';
// 通知装饰器
export * from './Notify';

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4702da4e-9d5f-4aec-8cad-bd6166bbad06",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6ea0d4f4-fc08-4ee7-b8cb-ca0c576bf6ce",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,141 @@
import { Singleton, singleton } from "../Singleton";
import LogUtils from "../utils/LogUtils";
import { isValid } from "cc";
/**
* 全局事件监听方法
* @param event 事件名
* @param args 事件参数
*/
export type ListenerFunc = (...args: unknown[]) => void;
/** 框架内部全局事件 */
export enum GameEventMessage {
/** 游戏从后台进入事件 */
GAME_SHOW = "GAME_ENTER",
/** 游戏切到后台事件 */
GAME_HIDE = "GAME_EXIT",
/** 游戏画笔尺寸变化事件 */
GAME_RESIZE = "GAME_RESIZE",
/** 游戏全屏事件 */
GAME_FULL_SCREEN = "GAME_FULL_SCREEN",
/** 游戏旋转屏幕事件 */
GAME_ORIENTATION = "GAME_ORIENTATION",
/** 引擎代码初始化完毕 */
GAME_CORE_INITIALIZED = "GAME_CORE_INITIALIZED",
/** 游戏初始化完成事件 */
GAME_LOGIN = "GAME_LOGIN",
}
export class EventData {
public event!: string;
public listener!: ListenerFunc;
public obj: unknown;
}
/**
* 全局消息管理
*/
@singleton()
export class EventManager extends Singleton {
private events: Map<string, Array<EventData>> = new Map();
/**
* 注册全局事件
* @param event 事件名
* @param listener 处理事件的侦听器函数
* @param obj 侦听函数绑定的作用域对象
*/
on(event: string, listener: ListenerFunc, obj: unknown) {
if (!event || !listener) {
LogUtils.warn(`注册【${event}】事件的侦听器函数为空`);
return;
}
let eds = this.events.get(event);
if (eds == null) {
eds = [];
this.events.set(event, eds);
}
// 使用 some 提前终止循环,一旦发现重复就停止查找
const isDuplicate = eds.some(
(bin) => bin.listener === listener && bin.obj === obj,
);
if (isDuplicate) {
LogUtils.warn(`名为【${event}】的事件重复注册侦听器`);
return; // 避免重复添加
}
const data: EventData = new EventData();
data.event = event;
data.listener = listener;
data.obj = obj;
eds.push(data);
}
/**
* 监听一次事件,事件响应后,该监听自动移除
* @param event 事件名
* @param listener 事件触发回调方法
* @param obj 侦听函数绑定的作用域对象
*/
once(event: string, listener: ListenerFunc, obj: unknown) {
const tempListener: ListenerFunc = (...args: unknown[]) => {
this.off(event, tempListener, obj);
listener.apply(obj, args);
};
this.on(event, tempListener, obj);
}
/**
* 移除全局事件
* @param event 事件名
* @param listener 处理事件的侦听器函数
* @param obj 侦听函数绑定的作用域对象
*/
off(event: string, listener: ListenerFunc, obj: unknown) {
const eds = this.events.get(event);
if (!eds) {
LogUtils.log(`名为【${event}】的事件不存在`);
return;
}
const index = eds.findIndex(
(bin) => bin.listener === listener && bin.obj === obj,
);
if (index !== -1) {
eds.splice(index, 1);
}
if (eds.length == 0) {
this.events.delete(event);
}
}
/**
* 触发全局事件
* @param event 事件名
* @param args 事件参数
*/
emit(event: string, ...args: unknown[]) {
const list = this.events.get(event);
if (list != null) {
const eds: Array<EventData> = list.slice();
for (let index = 0; index < eds.length; index++) {
const eventBin = eds[index];
if (isValid(eventBin.obj)) {
eventBin.listener.call(eventBin.obj, ...args);
} else {
LogUtils.warn(
`事件【${event}】的侦听器对象已被销毁,无法触发`,
);
// 注意:这里可能会导致数组长度变化,但因为是副本所以不影响循环
this.off(event, eventBin.listener, eventBin.obj);
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "204f0b8f-bfab-4404-96c1-a17a5cc43086",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "77812b7d-eeb3-456e-a00e-f485194c345d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,301 @@
import { IGuideConfig } from "./GuideData";
import { ResManager } from "../res/ResManager";
import { JsonAsset } from "cc";
import { Singleton } from "../Singleton";
import LogUtils from "../utils/LogUtils";
const TAG = "GuideConfigManager";
/**
* 引导配置管理器
*/
export class GuideConfigManager extends Singleton {
private _configs: Map<string, IGuideConfig> = new Map();
private _configVersion: string = "1.0.0";
protected async onInit() {
await this.loadDefaultConfigs();
}
/**
* 加载默认配置
*/
private async loadDefaultConfigs() {
// 这里可以加载默认的引导配置
const asset = await ResManager.getInstance().loadAsset<JsonAsset>(
"guide-config",
JsonAsset,
);
if (asset?.json) {
for (const key in asset.json) {
if (Object.prototype.hasOwnProperty.call(asset.json, key)) {
const element = asset.json[key];
GuideConfigManager.getInstance().addGuideConfig(element);
}
}
} else {
LogUtils.error(TAG, "加载默认引导配置失败");
}
}
/**
* 添加引导配置
* @param config 引导配置
* @returns 是否添加成功
*/
public addGuideConfig(config: IGuideConfig): boolean {
if (!config?.id) {
LogUtils.error(TAG, "引导配置无效");
return false;
}
if (this._configs.has(config.id)) {
LogUtils.warn(TAG, `引导配置已存在: ${config.id}`);
return false;
}
// 验证配置
if (!this.validateConfig(config)) {
LogUtils.error(TAG, `引导配置验证失败: ${config.id}`);
return false;
}
this._configs.set(config.id, config);
return true;
}
/**
* 更新引导配置
* @param config 引导配置
* @returns 是否更新成功
*/
public updateGuideConfig(config: IGuideConfig): boolean {
if (!config?.id) {
LogUtils.error(TAG, "引导配置无效");
return false;
}
if (!this._configs.has(config.id)) {
LogUtils.warn(TAG, `引导配置不存在: ${config.id}`);
return false;
}
// 验证配置
if (!this.validateConfig(config)) {
LogUtils.error(TAG, `引导配置验证失败: ${config.id}`);
return false;
}
this._configs.set(config.id, config);
return true;
}
/**
* 删除引导配置
* @param guideId 引导ID
* @returns 是否删除成功
*/
public removeGuideConfig(guideId: string): boolean {
if (!guideId) {
LogUtils.error(TAG, "引导ID无效");
return false;
}
if (!this._configs.has(guideId)) {
LogUtils.warn(TAG, `引导配置不存在: ${guideId}`);
return false;
}
this._configs.delete(guideId);
return true;
}
/**
* 获取引导配置
* @param guideId 引导ID
* @returns 引导配置
*/
public getGuideConfig(guideId: string): IGuideConfig | null {
return this._configs.get(guideId) || null;
}
/**
* 获取所有引导配置
* @returns 引导配置列表
*/
public getAllGuideConfigs(): IGuideConfig[] {
return Array.from(this._configs.values());
}
/**
* 获取启用的引导配置
* @returns 启用的引导配置列表
*/
public getEnabledGuideConfigs(): IGuideConfig[] {
return Array.from(this._configs.values()).filter(
(config) => config.enabled !== false,
);
}
/**
* 批量添加引导配置
* @param configs 引导配置列表
* @returns 成功添加的数量
*/
public addGuideConfigs(configs: IGuideConfig[]): number {
let successCount = 0;
for (const config of configs) {
if (this.addGuideConfig(config)) {
successCount++;
}
}
return successCount;
}
/**
* 清空所有配置
*/
public clearAllConfigs(): void {
this._configs.clear();
}
/**
* 验证引导配置
* @param config 引导配置
* @returns 是否有效
*/
private validateConfig(config: IGuideConfig): boolean {
// 检查基本字段
if (!config.id || !config.name) {
LogUtils.error(TAG, "引导配置缺少必要字段: id 或 name");
return false;
}
// 检查步骤
if (!config.steps || config.steps.length === 0) {
LogUtils.error(TAG, "引导配置缺少步骤");
return false;
}
// 检查步骤ID唯一性
const stepIds = new Set<string>();
for (const step of config.steps) {
if (!step.id) {
LogUtils.error(TAG, "引导步骤缺少ID");
return false;
}
if (stepIds.has(step.id)) {
LogUtils.error(TAG, `引导步骤ID重复: ${step.id}`);
return false;
}
stepIds.add(step.id);
}
// 检查开始步骤
if (config.startStepId && !stepIds.has(config.startStepId)) {
LogUtils.error(TAG, `开始步骤ID不存在: ${config.startStepId}`);
return false;
}
// 检查步骤引用
for (const step of config.steps) {
if (step.nextStepId && !stepIds.has(step.nextStepId)) {
LogUtils.error(
TAG,
`步骤 ${step.id} 的下一步骤ID不存在: ${step.nextStepId}`,
);
return false;
}
if (step.branches) {
for (const branch of step.branches) {
if (!stepIds.has(branch.nextStepId)) {
LogUtils.error(
TAG,
`步骤 ${step.id} 的分枝下一步骤ID不存在: ${branch.nextStepId}`,
);
return false;
}
}
}
}
return true;
}
/**
* 导出配置为JSON
* @param guideId 引导ID不传则导出所有
* @returns JSON字符串
*/
public exportToJSON(guideId?: string): string {
let data: Record<string, unknown>;
if (guideId) {
const config = this.getGuideConfig(guideId);
if (!config) {
LogUtils.error(TAG, `引导配置不存在: ${guideId}`);
return "";
}
data = { config: config };
} else {
data = {
version: this._configVersion,
configs: this.getAllGuideConfigs(),
};
}
try {
return JSON.stringify(data, null, 2);
} catch (err) {
LogUtils.error(TAG, "导出配置失败", err);
return "";
}
}
/**
* 从JSON导入配置
* @param jsonStr JSON字符串
* @returns 是否导入成功
*/
public importFromJSON(jsonStr: string): boolean {
try {
const data = JSON.parse(jsonStr);
if (data.config) {
// 单个配置
return this.addGuideConfig(data.config);
} else if (data.configs) {
// 多个配置
const successCount = this.addGuideConfigs(data.configs);
LogUtils.info(
TAG,
`导入配置成功: ${successCount}/${data.configs.length}`,
);
return successCount > 0;
} else {
LogUtils.error(TAG, "JSON格式不正确");
return false;
}
} catch (err) {
LogUtils.error(TAG, "导入配置失败", err);
return false;
}
}
/**
* 获取配置数量
*/
public getConfigCount(): number {
return this._configs.size;
}
/**
* 检查配置是否存在
* @param guideId 引导ID
*/
public hasConfig(guideId: string): boolean {
return this._configs.has(guideId);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "90bfebfd-3ce7-4d44-a7da-c4f7597a9163",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,239 @@
import { Vec2, Size } from "cc";
/**
* 引导步骤类型
*/
export enum GuideStepType {
/** 高亮显示 */
HIGHLIGHT = "highlight",
/** 点击引导 */
CLICK = "click",
/** 文本提示 */
TEXT = "text",
/** 等待 */
WAIT = "wait",
/** 自定义 */
CUSTOM = "custom",
}
/**
* 引导触发条件类型
*/
export enum GuideTriggerType {
/** 立即触发 */
IMMEDIATE = "immediate",
/** 延迟触发 */
DELAY = "delay",
/** 事件触发 */
EVENT = "event",
/** 条件触发 */
CONDITION = "condition",
}
/**
* 引导遮罩形状
*/
export enum GuideMaskShape {
/** 矩形 */
RECT = "rect",
/** 圆形 */
CIRCLE = "circle",
/** 椭圆 */
ELLIPSE = "ellipse",
}
/**
* 引导箭头方向
*/
export enum GuideArrowDirection {
UP = "up",
DOWN = "down",
LEFT = "left",
RIGHT = "right",
UP_LEFT = "up_left",
UP_RIGHT = "up_right",
DOWN_LEFT = "down_left",
DOWN_RIGHT = "down_right",
}
/**
* 引导状态
*/
export enum GuideState {
/** 未开始 */
NOT_STARTED = "not_started",
/** 进行中 */
RUNNING = "running",
/** 已完成 */
COMPLETED = "completed",
/** 已跳过 */
SKIPPED = "skipped",
/** 已暂停 */
PAUSED = "paused",
}
/**
* 引导分枝条件
*/
export interface IGuideCondition {
/** 条件类型 */
type: string;
/** 条件参数 */
params?: Record<string, unknown>;
/** 条件表达式 */
expression?: string;
}
/**
* 引导分枝配置
*/
export interface IGuideBranch {
/** 分枝ID */
id: string;
/** 分枝条件 */
condition: IGuideCondition;
/** 下一步骤ID */
nextStepId: string;
}
/**
* 引导步骤配置
*/
export interface IGuideStepConfig {
/** 步骤ID */
id: string;
/** 步骤类型 */
type: GuideStepType;
/** 步骤标题 */
title?: string;
/** 步骤描述 */
description?: string;
/** 目标节点路径 */
targetPath?: string;
/** 遮罩形状 */
maskShape?: GuideMaskShape;
/** 遮罩偏移 */
maskOffset?: Vec2;
/** 遮罩大小 */
maskSize?: Size;
/** 遮罩圆角 */
maskRadius?: number;
/** 提示文本位置 */
textPosition?: Vec2;
/** 箭头方向 */
arrowDirection?: GuideArrowDirection;
/** 箭头偏移 */
arrowOffset?: Vec2;
/** 等待时间(毫秒) */
waitTime?: number;
/** 触发条件 */
trigger?: {
type: GuideTriggerType;
params?: Record<string, unknown>;
};
/** 分枝配置 */
branches?: IGuideBranch[];
/** 下一步骤ID */
nextStepId?: string;
/** 是否可跳过 */
skippable?: boolean;
/** 是否自动进入下一步(当目标按钮被点击时) */
autoNextStep?: boolean;
/** 自定义数据 */
customData?: Record<string, unknown>;
}
/**
* 引导配置
*/
export interface IGuideConfig {
/** 引导ID */
id: string;
/** 引导名称 */
name: string;
/** 引导版本 */
version?: string;
/** 引导描述 */
description?: string;
/** 引导优先级 */
priority?: number;
/** 是否启用 */
enabled?: boolean;
/** 触发条件 */
trigger?: {
type: GuideTriggerType;
params?: Record<string, unknown>;
};
/** 引导步骤列表 */
steps: IGuideStepConfig[];
/** 开始步骤ID */
startStepId?: string;
/** 是否可重复 */
repeatable?: boolean;
/** 自定义数据 */
customData?: Record<string, unknown>;
}
/**
* 引导运行时数据
*/
export interface IGuideRuntimeData {
/** 引导配置 */
config: IGuideConfig;
/** 当前状态 */
state: GuideState;
/** 当前步骤ID */
currentStepId?: string;
/** 当前步骤索引 */
currentStepIndex: number;
/** 开始时间 */
startTime?: number;
/** 完成时间 */
completeTime?: number;
/** 暂停时间 */
pauseTime?: number;
/** 步骤历史 */
stepHistory: string[];
/** 自定义运行时数据 */
runtimeData?: Record<string, unknown>;
}
/**
* 引导事件类型
*/
export enum GuideEventType {
/** 引导开始 */
GUIDE_START = "guide_start",
/** 引导完成 */
GUIDE_COMPLETE = "guide_complete",
/** 引导跳过 */
GUIDE_SKIP = "guide_skip",
/** 引导暂停 */
GUIDE_PAUSE = "guide_pause",
/** 引导恢复 */
GUIDE_RESUME = "guide_resume",
/** 步骤开始 */
STEP_START = "step_start",
/** 步骤完成 */
STEP_COMPLETE = "step_complete",
/** 步骤跳过 */
STEP_SKIP = "step_skip",
/** 分枝选择 */
BRANCH_SELECT = "branch_select",
}
/**
* 引导事件数据
*/
export interface IGuideEvent {
/** 事件类型 */
type: GuideEventType;
/** 引导ID */
guideId: string;
/** 步骤ID */
stepId?: string;
/** 事件数据 */
data?: Record<string, unknown>;
/** 时间戳 */
timestamp: number;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "87d7cd52-ee8d-4b39-9fb9-b0f9c10d8bc3",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,570 @@
import {
IGuideRuntimeData,
IGuideStepConfig,
GuideState,
GuideEventType,
IGuideEvent,
IGuideBranch,
} from "./GuideData";
import { Node, find } from "cc";
import { GuideMaskComponent } from "./GuideMaskComponent";
import { GuideStepComponent } from "./GuideStepComponent";
import { GuideConfigManager } from "./GuideConfigManager";
import TimeManager from "../timer/TimerManager";
import UIManager from "../ui/UIManager";
import { UIType } from "../ui/UIDecorator";
import { GuideTargetComponent } from "./GuideTargetComponent";
import StorageManager, { GUIDE_HISTORY_KEY } from "../storage/StorageManager";
import { Singleton, singleton } from "../Singleton";
import { EventManager } from "../event/EventManager";
import LogUtils from "../utils/LogUtils";
const TAG = "GuideManager";
/**
* 新手引导管理器
*/
export class GuideManager extends Singleton {
private _configManager: GuideConfigManager;
private _eventManager: EventManager;
private _currentGuide: IGuideRuntimeData | null = null;
private _guideHistory: Map<string, IGuideRuntimeData> = null;
private _maskNode: Node | null = null;
private _stepNode: Node | null = null;
private _isRunning: boolean = false;
private _isPaused: boolean = false;
private _currentTargetNode: Node | null = null; // 当前目标节点
protected async onInit() {
this._configManager = GuideConfigManager.getInstance();
this._eventManager = EventManager.getInstance();
this.createGuideNodes();
}
/**
* 创建引导节点
*/
private createGuideNodes(): void {
const guideRoot = UIManager.getInstance().getUITypeNode(UIType.GUIDE);
if (!guideRoot) {
LogUtils.error(TAG, "引导节点不存在,无法创建引导节点");
return;
}
// 确保引导节点在最前面
// 优先查找遮罩节点,若无则创建
this._maskNode = guideRoot.getChildByName("GuideMask");
this._maskNode.active = false;
// 优先查找步骤节点,若无则创建
this._stepNode = guideRoot.getChildByName("GuideStep");
this._stepNode.active = false;
}
private loadGuideHistory(): void {
const parsedHistory =
StorageManager.getInstance().getData(GUIDE_HISTORY_KEY);
this._guideHistory = new Map();
if (parsedHistory) {
for (const [guideId, historyData] of parsedHistory) {
const config = this._configManager.getGuideConfig(guideId);
historyData.config = config;
this._guideHistory.set(guideId, historyData);
}
}
}
/**
* 开始引导
* @param guideId 引导ID
* @returns 是否成功开始
*/
public startGuide(guideId: string): boolean {
if (this._isRunning) {
LogUtils.warn(TAG, `引导正在运行中,无法开始新的引导: ${guideId}`);
return false;
}
const config = this._configManager.getGuideConfig(guideId);
if (!config) {
LogUtils.error(TAG, `引导配置不存在: ${guideId}`);
return false;
}
if (!config.enabled) {
LogUtils.warn(TAG, `引导已禁用: ${guideId}`);
return false;
}
// 检查是否已完成且不可重复
if (this._guideHistory == null) {
this.loadGuideHistory();
}
const history = this._guideHistory.get(guideId);
if (
history &&
history.state === GuideState.COMPLETED &&
!config.repeatable
) {
LogUtils.info(TAG, `引导已完成且不可重复: ${guideId}`);
return false;
}
// 创建运行时数据
this._currentGuide = {
config: config,
state: GuideState.RUNNING,
currentStepIndex: 0,
startTime: TimeManager.getInstance().getCorrectedTime(),
stepHistory: [],
};
// 设置开始步骤
const startStepId =
config.startStepId ||
(config.steps.length > 0 ? config.steps[0].id : null);
if (startStepId) {
this._currentGuide.currentStepId = startStepId;
}
this._isRunning = true;
this._isPaused = false;
// 触发引导开始事件
this.emitGuideEvent(GuideEventType.GUIDE_START, guideId);
// 开始第一步
this.executeCurrentStep();
LogUtils.info(TAG, `引导开始: ${guideId}`);
return true;
}
/**
* 停止引导
* @param completed 是否完成
*/
public stopGuide(completed: boolean = false): void {
if (!this._currentGuide) {
return;
}
const guideId = this._currentGuide.config.id;
// 更新状态
this._currentGuide.state = completed
? GuideState.COMPLETED
: GuideState.SKIPPED;
this._currentGuide.completeTime =
TimeManager.getInstance().getCorrectedTime();
// 保存到历史
this._guideHistory.set(guideId, this._currentGuide);
// 隐藏UI
this.hideGuideUI();
// 触发事件
const eventType = completed
? GuideEventType.GUIDE_COMPLETE
: GuideEventType.GUIDE_SKIP;
this.emitGuideEvent(eventType, guideId);
// 重置状态
this._currentGuide = null;
this._isRunning = false;
this._isPaused = false;
const tempGuideHistory = {};
for (const [guideId, historyData] of this._guideHistory) {
const tempHistoryData = JSON.parse(JSON.stringify(historyData));
tempHistoryData.config = undefined;
tempGuideHistory[guideId] = tempHistoryData;
}
StorageManager.getInstance().setData(
GUIDE_HISTORY_KEY,
tempGuideHistory,
);
LogUtils.info(TAG, `引导${completed ? "完成" : "跳过"}: ${guideId}`);
}
/**
* 暂停引导
*/
public pauseGuide(): void {
if (!this._currentGuide || this._isPaused) {
return;
}
this._isPaused = true;
this._currentGuide.pauseTime = Date.now();
this.hideGuideUI();
this.emitGuideEvent(
GuideEventType.GUIDE_PAUSE,
this._currentGuide.config.id,
);
LogUtils.info(TAG, `引导暂停: ${this._currentGuide.config.id}`);
}
/**
* 恢复引导
*/
public resumeGuide(): void {
if (!this._currentGuide || !this._isPaused) {
return;
}
this._isPaused = false;
this._currentGuide.pauseTime = undefined;
this.executeCurrentStep();
this.emitGuideEvent(
GuideEventType.GUIDE_RESUME,
this._currentGuide.config.id,
);
LogUtils.info(TAG, `引导恢复: ${this._currentGuide.config.id}`);
}
/**
* 执行当前步骤
*/
private executeCurrentStep(): void {
if (!this._currentGuide || this._isPaused) {
return;
}
const currentStep = this.getCurrentStep();
if (!currentStep) {
LogUtils.warn(TAG, "当前步骤不存在,引导结束");
this.stopGuide(true);
return;
}
// 记录步骤历史
this._currentGuide.stepHistory.push(currentStep.id);
// 触发步骤开始事件
this.emitGuideEvent(
GuideEventType.STEP_START,
this._currentGuide.config.id,
currentStep.id,
);
// 显示引导UI
this.showGuideUI(currentStep);
LogUtils.info(TAG, `执行引导步骤: ${currentStep.id}`);
}
/**
* 下一步
*/
public nextStep(): void {
LogUtils.info(TAG, `开始下一步`);
if (!this._currentGuide) {
return;
}
const currentStep = this.getCurrentStep();
if (!currentStep) {
return;
}
// 触发步骤完成事件
this.emitGuideEvent(
GuideEventType.STEP_COMPLETE,
this._currentGuide.config.id,
currentStep.id,
);
// 检查分枝
const nextStepId = this.getNextStepId(currentStep);
if (nextStepId) {
this.goToStep(nextStepId);
} else {
// 没有下一步,引导完成
this.stopGuide(true);
}
}
/**
* 跳转到指定步骤
* @param stepId 步骤ID
*/
public goToStep(stepId: string): void {
if (!this._currentGuide) {
return;
}
const stepIndex = this._currentGuide.config.steps.findIndex(
(step) => step.id === stepId,
);
if (stepIndex === -1) {
LogUtils.error(TAG, `步骤不存在: ${stepId}`);
return;
}
this._currentGuide.currentStepId = stepId;
this._currentGuide.currentStepIndex = stepIndex;
this.executeCurrentStep();
}
public getCurrentGuideId(): string | null {
if (!this._currentGuide) {
return null;
}
return this._currentGuide.config.id;
}
/**
* 获取当前步骤
*/
public getCurrentStep(): IGuideStepConfig | null {
if (!this._currentGuide) {
return null;
}
if (this._currentGuide.currentStepId) {
return (
this._currentGuide.config.steps.find(
(step) => step.id === this._currentGuide.currentStepId,
) || null
);
}
if (
this._currentGuide.currentStepIndex <
this._currentGuide.config.steps.length
) {
return this._currentGuide.config.steps[
this._currentGuide.currentStepIndex
];
}
return null;
}
/**
* 获取下一步骤ID
* @param currentStep 当前步骤
*/
private getNextStepId(currentStep: IGuideStepConfig): string | null {
// 检查分枝条件
if (currentStep.branches && currentStep.branches.length > 0) {
for (const branch of currentStep.branches) {
if (this.evaluateBranchCondition(branch)) {
this.emitGuideEvent(
GuideEventType.BRANCH_SELECT,
this._currentGuide.config.id,
currentStep.id,
{
branchId: branch.id,
nextStepId: branch.nextStepId,
},
);
return branch.nextStepId;
}
}
}
// 使用默认下一步
if (currentStep.nextStepId) {
return currentStep.nextStepId;
}
// 使用顺序下一步
const currentIndex = this._currentGuide.currentStepIndex;
if (currentIndex + 1 < this._currentGuide.config.steps.length) {
return this._currentGuide.config.steps[currentIndex + 1].id;
}
return null;
}
/**
* 评估分枝条件
* @param branch 分枝配置
*/
private evaluateBranchCondition(branch: IGuideBranch): boolean {
// TODO: 实现条件评估逻辑
// 这里可以根据条件类型和参数进行具体的条件判断
return false;
}
/**
* 显示引导UI
* @param step 步骤配置
*/
private showGuideUI(step: IGuideStepConfig): void {
// 显示遮罩
if (this._maskNode) {
this._maskNode.active = true;
const maskComponent =
this._maskNode.getComponent(GuideMaskComponent);
if (maskComponent) {
maskComponent.showMask(step);
}
}
// 显示步骤UI
if (this._stepNode) {
this._stepNode.active = true;
const stepComponent =
this._stepNode.getComponent(GuideStepComponent);
if (stepComponent) {
stepComponent.showStep(step);
}
}
// 在目标节点上添加 GuideTargetComponent
this.addTargetComponent(step);
}
/**
* 隐藏引导UI
*/
private hideGuideUI(): void {
if (this._maskNode) {
this._maskNode.active = false;
}
if (this._stepNode) {
this._stepNode.active = false;
}
// 移除目标节点上的 GuideTargetComponent
this.removeTargetComponent();
}
/**
* 在目标节点上添加 GuideTargetComponent
* @param step 步骤配置
*/
private addTargetComponent(step: IGuideStepConfig): void {
if (!this._currentGuide) {
return;
}
// 先移除之前的组件
this.removeTargetComponent();
// 查找目标节点
let targetNode: Node | null = null;
// 通过路径查找
if (step.targetPath) {
targetNode = find(step.targetPath);
}
if (!targetNode) {
LogUtils.warn(
TAG,
`未找到目标节点,无法添加 GuideTargetComponent: ${step.targetPath}`,
);
return;
}
// 检查是否已经有 GuideTargetComponent
let targetComponent = targetNode.getComponent(GuideTargetComponent);
if (!targetComponent) {
targetComponent = targetNode.addComponent(GuideTargetComponent);
}
// 设置引导信息
targetComponent.setGuideInfo(
this._currentGuide.config.id,
step.id,
step,
);
this._currentTargetNode = targetNode;
LogUtils.info(TAG, `已在目标节点添加 GuideTargetComponent: ${step.id}`);
}
/**
* 移除目标节点上的 GuideTargetComponent
*/
private removeTargetComponent(): void {
if (!this._currentTargetNode) {
return;
}
const targetComponent =
this._currentTargetNode.getComponent(GuideTargetComponent);
if (targetComponent) {
const stepId = targetComponent.getStepId();
targetComponent.destroy();
LogUtils.info(
TAG,
`已移除目标节点的 GuideTargetComponent: ${stepId}`,
);
}
this._currentTargetNode = null;
}
/**
* 触发引导事件
* @param type 事件类型
* @param guideId 引导ID
* @param stepId 步骤ID
* @param data 事件数据
*/
private emitGuideEvent(
type: GuideEventType,
guideId: string,
stepId?: string,
data?: Record<string, unknown>,
): void {
const event: IGuideEvent = {
type: type,
guideId: guideId,
stepId: stepId,
data: data,
timestamp: Date.now(),
};
this._eventManager.emit(type, event);
}
/**
* 获取当前引导
*/
public getCurrentGuide(): IGuideRuntimeData | null {
return this._currentGuide;
}
/**
* 是否正在运行引导
*/
public isRunning(): boolean {
return this._isRunning;
}
/**
* 是否暂停
*/
public isPaused(): boolean {
return this._isPaused;
}
/**
* 获取引导历史
* @param guideId 引导ID
*/
public getGuideHistory(guideId: string): IGuideRuntimeData | null {
return this._guideHistory.get(guideId) || null;
}
/**
* 清除引导历史
* @param guideId 引导ID不传则清除所有
*/
public clearGuideHistory(guideId?: string): void {
if (guideId) {
this._guideHistory.delete(guideId);
} else {
this._guideHistory.clear();
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c0d3d7a2-d0ce-4060-bcb9-0c83426243c0",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,521 @@
import {
Component,
_decorator,
Graphics,
UITransform,
Vec2,
Vec3,
Size,
view,
Node,
find,
Widget,
Color,
EventTouch,
Input,
} from "cc";
import { IGuideStepConfig, GuideMaskShape } from "./GuideData";
import UIManager from "../ui/UIManager";
import { StringUtils } from "../utils/StringUtils";
import { GuideManager } from "./GuideManager";
import LogUtils from "../utils/LogUtils";
const { ccclass, property } = _decorator;
const TAG = "GuideMaskComponent";
/**
* 引导遮罩组件
* 实现挖洞效果和按钮屏蔽功能
*/
@ccclass("GuideMaskComponent")
export class GuideMaskComponent extends Component {
@property({
displayName: "遮罩颜色",
tooltip: "遮罩的颜色",
})
public maskColor: Color = new Color(0, 0, 0, 128);
@property({
displayName: "高亮边框颜色",
tooltip: "高亮区域边框的颜色",
})
public highlightBorderColor: Color = new Color(255, 255, 255, 255);
@property({
displayName: "高亮边框宽度",
tooltip: "高亮区域边框的宽度",
})
public highlightBorderWidth: number = 2;
private _graphics: Graphics | null = null;
private _uiTransform: UITransform | null = null;
private _currentStep: IGuideStepConfig | null = null;
private _targetNode: Node | null = null;
private _screenSize: Size = new Size();
protected onLoad(): void {
this.initComponents();
this.setupMask();
this.setupTouchEvents();
}
protected onEnable(): void {
this.updateScreenSize();
// onEnable 时重新启用触摸事件
this.enableTouchEvents();
}
protected onDisable(): void {
this.disableTouchEvents();
}
/**
* 显示遮罩
* @param step 引导步骤配置
*/
public showMask(step: IGuideStepConfig): void {
this._currentStep = step;
this.findTargetNode();
this.drawMask();
// 显示遮罩时确保触摸事件已启用
this.enableTouchEvents();
}
/**
* 隐藏遮罩
*/
public hideMask(): void {
if (this._graphics) {
this._graphics.clear();
}
this._currentStep = null;
this._targetNode = null;
// 隐藏遮罩时禁用触摸事件
this.disableTouchEvents();
}
/**
* 初始化组件
*/
private initComponents(): void {
// 添加Graphics组件
this._graphics = this.node.getComponent(Graphics);
if (!this._graphics) {
this._graphics = this.node.addComponent(Graphics);
}
// 添加UITransform组件
this._uiTransform = this.node.getComponent(UITransform);
if (!this._uiTransform) {
this._uiTransform = this.node.addComponent(UITransform);
}
// 添加Widget组件使其铺满屏幕
let widget = this.node.getComponent(Widget);
if (!widget) {
widget = this.node.addComponent(Widget);
}
widget.isAlignTop = true;
widget.isAlignBottom = true;
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.top = 0;
widget.bottom = 0;
widget.left = 0;
widget.right = 0;
// 添加这行关键设置
widget.alignMode = Widget.AlignMode.ON_WINDOW_RESIZE;
}
/**
* 设置遮罩
*/
private setupMask(): void {
this.updateScreenSize();
if (this._uiTransform) {
// 设置锚点为左下角
this._uiTransform.setAnchorPoint(0, 0);
// 设置内容尺寸为屏幕尺寸
this._uiTransform.setContentSize(this._screenSize);
}
}
/**
* 更新屏幕尺寸
*/
private updateScreenSize(): void {
const visibleSize = view.getVisibleSize();
this._screenSize.width = visibleSize.width;
this._screenSize.height = visibleSize.height;
}
/**
* 查找目标节点
*/
private findTargetNode(): void {
if (!this._currentStep) {
return;
}
this._targetNode = null;
// 通过路径查找
if (this._currentStep.targetPath) {
this._targetNode = find(this._currentStep.targetPath);
}
if (!this._targetNode) {
LogUtils.warn(
TAG,
`未找到目标节点: ${this._currentStep.targetPath}`
);
}
}
/**
* 绘制遮罩
*/
private drawMask(): void {
if (!this._graphics || !this._currentStep) {
return;
}
this._graphics.clear();
// 绘制全屏遮罩 - 使用简单的 (0,0) 起点
this._graphics.fillColor = this.maskColor;
this._graphics.fillRect(
0,
0,
this._uiTransform.contentSize.width,
this._uiTransform.contentSize.height
);
// 绘制挖洞
this.drawHole();
}
/**
* 绘制挖洞效果
*/
private drawHole(): void {
if (!this._graphics || !this._targetNode || !this._currentStep) {
return;
}
// 获取目标节点的世界坐标和尺寸
const targetTransform = this._targetNode.getComponent(UITransform);
if (!targetTransform) {
LogUtils.warn(TAG, "目标节点没有UITransform组件");
return;
}
const worldPos = this._targetNode.getWorldPosition();
const targetSize = targetTransform.contentSize;
// 转换为本地坐标 - 修复坐标转换问题
const localPos = new Vec3();
// 使用父节点Canvas进行坐标转换而不是当前遮罩节点
const canvas = find("Canvas");
if (canvas) {
const canvasTransform = canvas.getComponent(UITransform);
canvasTransform.convertToNodeSpaceAR(worldPos, localPos);
// 由于遮罩节点锚点在左下角(0,0),需要调整坐标
// Canvas 的坐标系原点在中心,需要转换到左下角坐标系
localPos.x += canvasTransform.contentSize.width / 2;
localPos.y += canvasTransform.contentSize.height / 2;
// 调整挖洞位置,使其以目标节点中心为基准
localPos.x -= targetSize.width / 2;
localPos.y -= targetSize.height / 2;
} else {
// 备用方案:直接使用世界坐标
localPos.set(worldPos.x, worldPos.y, 0);
}
// 应用偏移
const offset = this._currentStep.maskOffset || Vec2.ZERO;
localPos.x += offset.x;
localPos.y += offset.y;
// 应用自定义尺寸
const holeSize =
this._currentStep.maskSize ||
new Size(targetSize.width, targetSize.height);
// 根据形状绘制挖洞
const shape = this._currentStep.maskShape || GuideMaskShape.RECT;
this.drawHoleByShape(localPos, holeSize, shape);
}
/**
* 根据形状绘制挖洞
* @param pos 位置
* @param size 尺寸
* @param shape 形状
*/
private drawHoleByShape(
pos: Vec3,
size: Size,
shape: GuideMaskShape
): void {
if (!this._graphics) {
return;
}
switch (shape) {
case GuideMaskShape.RECT:
this.drawRectHole(pos, size);
break;
case GuideMaskShape.CIRCLE:
this.drawCircleHole(pos, Math.min(size.width, size.height) / 2);
break;
case GuideMaskShape.ELLIPSE:
this.drawEllipseHole(pos, size.width / 2, size.height / 2);
break;
}
}
/**
* 绘制矩形挖洞
* @param pos 位置
* @param size 尺寸
*/
private drawRectHole(pos: Vec3, size: Size): void {
if (!this._graphics) {
return;
}
const graphics = this._graphics;
const x = pos.x - size.width / 2;
const y = pos.y - size.height / 2;
const radius = this._currentStep?.maskRadius || 0;
// 清除矩形区域(挖洞)
graphics.fillColor = Color.TRANSPARENT;
if (radius > 0) {
graphics.roundRect(x, y, size.width, size.height, radius);
} else {
graphics.rect(x, y, size.width, size.height);
}
graphics.fill();
// 绘制高亮边框
if (this.highlightBorderWidth > 0) {
graphics.strokeColor = this.highlightBorderColor;
graphics.lineWidth = this.highlightBorderWidth;
if (radius > 0) {
graphics.roundRect(x, y, size.width, size.height, radius);
} else {
graphics.rect(x, y, size.width, size.height);
}
graphics.stroke();
}
}
/**
* 绘制圆形挖洞
* @param pos 位置
* @param radius 半径
*/
private drawCircleHole(pos: Vec3, radius: number): void {
if (!this._graphics) {
return;
}
const graphics = this._graphics;
// 清除圆形区域(挖洞)
graphics.fillColor = Color.TRANSPARENT;
graphics.circle(pos.x, pos.y, radius);
graphics.fill();
// 绘制高亮边框
if (this.highlightBorderWidth > 0) {
graphics.strokeColor = this.highlightBorderColor;
graphics.lineWidth = this.highlightBorderWidth;
graphics.circle(pos.x, pos.y, radius);
graphics.stroke();
}
}
/**
* 绘制椭圆挖洞
* @param pos 位置
* @param radiusX X轴半径
* @param radiusY Y轴半径
*/
private drawEllipseHole(pos: Vec3, radiusX: number, radiusY: number): void {
if (!this._graphics) {
return;
}
const graphics = this._graphics;
// 清除椭圆区域(挖洞)
graphics.fillColor = Color.TRANSPARENT;
graphics.ellipse(pos.x, pos.y, radiusX, radiusY);
graphics.fill();
// 绘制高亮边框
if (this.highlightBorderWidth > 0) {
graphics.strokeColor = this.highlightBorderColor;
graphics.lineWidth = this.highlightBorderWidth;
graphics.ellipse(pos.x, pos.y, radiusX, radiusY);
graphics.stroke();
}
}
/**
* 设置触摸事件
*/
private setupTouchEvents(): void {
this.enableTouchEvents();
}
/**
* 启用触摸事件
*/
private enableTouchEvents(): void {
if (!this.node) {
return;
}
// 先移除旧的监听器
this.disableTouchEvents();
// 注册新的监听器
this.node.on(Input.EventType.TOUCH_START, this.handleTouchEvent, this);
this.node.on(Input.EventType.TOUCH_END, this.handleTouchEvent, this);
this.node.on(Input.EventType.TOUCH_MOVE, this.handleTouchEvent, this);
this.node.on(Input.EventType.TOUCH_CANCEL, this.handleTouchEvent, this);
}
/**
* 禁用触摸事件
*/
private disableTouchEvents(): void {
if (!this.node) {
return;
}
this.node.off(Input.EventType.TOUCH_START, this.handleTouchEvent, this);
this.node.off(Input.EventType.TOUCH_END, this.handleTouchEvent, this);
this.node.off(Input.EventType.TOUCH_MOVE, this.handleTouchEvent, this);
this.node.off(
Input.EventType.TOUCH_CANCEL,
this.handleTouchEvent,
this
);
}
private handleTouchEvent(event: EventTouch): void {
if (
event.type == Input.EventType.TOUCH_END &&
StringUtils.isEmpty(this._currentStep?.targetPath)
) {
GuideManager.getInstance().nextStep();
return;
}
const worldPos = event.getLocation();
// 检查是否在挖洞区域内
if (this.isPointInHole(new Vec3(worldPos.x, worldPos.y, 0))) {
// 允许事件穿透
event.propagationStopped = false;
event.preventSwallow = true;
return;
}
// 阻止事件传播到下层节点
event.propagationStopped = true;
}
/**
* 检查点是否在挖洞区域内
* @param screenPos 屏幕坐标
* @returns 是否在挖洞区域内
*/
public isPointInHole(screenPos: Vec3): boolean {
if (!this._targetNode || !this._currentStep) {
return false;
}
const targetTransform = this._targetNode.getComponent(UITransform);
if (!targetTransform) {
LogUtils.warn(TAG, "目标节点没有UITransform组件");
return false;
}
// 获取目标节点的世界坐标和尺寸
const targetWorldPos = this._targetNode.getWorldPosition();
const targetScreenPos = UIManager.getInstance()
.getCamera()
.worldToScreen(targetWorldPos);
const targetSize = targetTransform.contentSize;
const holeSize =
this._currentStep.maskSize ||
new Size(targetSize.width, targetSize.height);
const shape = this._currentStep.maskShape || GuideMaskShape.RECT;
// 应用偏移
const offset = this._currentStep.maskOffset || Vec2.ZERO;
const adjustedWorldPos = new Vec3(
targetScreenPos.x + offset.x,
targetScreenPos.y + offset.y,
targetScreenPos.z
);
// 计算相对于目标节点中心的偏移
const deltaX = screenPos.x - adjustedWorldPos.x;
const deltaY = screenPos.y - adjustedWorldPos.y;
let result = false;
switch (shape) {
case GuideMaskShape.RECT:
result =
Math.abs(deltaX) <= holeSize.width / 2 &&
Math.abs(deltaY) <= holeSize.height / 2;
break;
case GuideMaskShape.CIRCLE:
const radius = Math.min(holeSize.width, holeSize.height) / 2;
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
const radiusSquared = radius * radius;
result = distanceSquared <= radiusSquared;
break;
case GuideMaskShape.ELLIPSE:
const radiusX = holeSize.width / 2;
const radiusY = holeSize.height / 2;
const ellipseValue =
(deltaX * deltaX) / (radiusX * radiusX) +
(deltaY * deltaY) / (radiusY * radiusY);
result = ellipseValue <= 1;
break;
default:
LogUtils.warn(TAG, `未知的挖洞形状: ${shape}`);
result = false;
}
return result;
}
/**
* 获取当前目标节点
*/
public getTargetNode(): Node | null {
return this._targetNode;
}
/**
* 刷新遮罩
*/
public refresh(): void {
if (this._currentStep) {
this.showMask(this._currentStep);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8924c51a-a141-4c6f-bfa1-82f5a20b3fbf",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,339 @@
import {
Component,
_decorator,
Node,
Label,
UITransform,
Vec3,
Vec2,
find,
} from "cc";
import { IGuideStepConfig, GuideArrowDirection } from "./GuideData";
import { GuideManager } from "./GuideManager";
import NodeUtils from "../utils/NodeUtils";
import LogUtils from "../utils/LogUtils";
const { ccclass, property } = _decorator;
const TAG = "GuideStepComponent";
/**
* 引导步骤组件
* 支持文本提示、箭头指向等UI元素
*/
@ccclass("GuideStepComponent")
export class GuideStepComponent extends Component {
@property({
displayName: "默认提示位置偏移",
tooltip: "提示框相对于目标的默认偏移",
})
public defaultTipOffset: Vec2 = new Vec2(0, 100);
@property({
displayName: "默认箭头偏移",
tooltip: "箭头相对于目标的默认偏移",
})
public defaultArrowOffset: Vec2 = new Vec2(0, 50);
@property({
type: Node,
displayName: "提示节点",
tooltip: "提示框的节点",
})
private tipNode: Node = null;
@property({
type: Node,
displayName: "箭头节点",
tooltip: "箭头的节点",
})
private arrowNode: Node = null;
private _currentStep: IGuideStepConfig | null = null;
private _targetNode: Node | null = null;
private _guideManager: GuideManager | null = null;
protected onLoad(): void {
this._guideManager = GuideManager.getInstance();
this.initDefaultUI();
this.node.addComponent(UITransform);
}
/**
* 创建默认UI
*/
private initDefaultUI(): void {
const nextButton = NodeUtils.findChildRecursive(this.node, "NextBtn");
nextButton.onClick(() => {
this._guideManager?.nextStep();
}, this);
nextButton.active = false;
const skipButton = NodeUtils.findChildRecursive(this.node, "SkipBtn");
skipButton.onClick(() => {
this._guideManager?.stopGuide(false);
}, this);
this.tipNode.active = false;
}
/**
* 显示引导步骤
* @param step 步骤配置
*/
public showStep(step: IGuideStepConfig): void {
this.hideStep();
this._currentStep = step;
this.findTargetNode();
this.showTip();
this.showArrow();
}
/**
* 隐藏引导步骤
*/
public hideStep(): void {
this.hideTip();
this.hideArrow();
this._currentStep = null;
this._targetNode = null;
}
/**
* 查找目标节点
*/
private findTargetNode(): void {
if (!this._currentStep) {
return;
}
this._targetNode = null;
// 通过路径查找
if (this._currentStep.targetPath) {
this._targetNode = find(this._currentStep.targetPath);
}
if (!this._targetNode) {
LogUtils.warn(
TAG,
`未找到目标节点: ${this._currentStep.targetPath}`
);
}
}
/**
* 显示提示
*/
private showTip(): void {
if (!this._currentStep) {
return;
}
if (!this.tipNode) {
return;
}
// 更新提示内容
this.updateTipContent();
// 设置提示位置
this.updateTipPosition();
// 显示提示
this.tipNode.active = true;
}
/**
* 更新提示内容
*/
private updateTipContent(): void {
if (!this.tipNode || !this._currentStep) {
return;
}
// 更新标题
const titleLabel = this.tipNode.getComponentInChildren(Label);
if (titleLabel && this._currentStep.title) {
titleLabel.string = this._currentStep.title;
}
// 更新描述
const descLabel = this.tipNode
.getChildByName("Description")
?.getComponent(Label);
if (descLabel) {
descLabel.string = this._currentStep.description;
}
// 更新按钮状态
const skipButton = this.tipNode.getChildByName("SkipButton");
if (skipButton) {
skipButton.active = this._currentStep.skippable !== false;
}
}
/**
* 更新提示位置
*/
private updateTipPosition(): void {
if (!this.tipNode) {
return;
}
const position = new Vec3(Vec3.ZERO);
if (this._currentStep?.textPosition) {
position.x = this._currentStep.textPosition.x;
position.y = this._currentStep.textPosition.y;
} else if (this._targetNode) {
// 相对于目标节点定位
const targetWorldPos = this._targetNode.getWorldPosition();
const localPos = new Vec3();
this.node
.getComponent(UITransform)
.convertToNodeSpaceAR(targetWorldPos, localPos);
position.x = localPos.x + this.defaultTipOffset.x;
position.y = localPos.y + this.defaultTipOffset.y;
}
this.tipNode.setPosition(position);
}
/**
* 隐藏提示
*/
private hideTip(): void {
if (this.tipNode) {
this.tipNode.active = false;
}
}
/**
* 显示箭头
*/
private showArrow(): void {
if (!this._currentStep || !this._targetNode) {
this.arrowNode.active = false;
return;
}
if (!this.arrowNode) {
return;
}
// 设置箭头位置和方向
this.updateArrowPosition();
this.updateArrowDirection();
// 显示箭头
this.arrowNode.active = true;
}
/**
* 更新箭头位置
*/
private updateArrowPosition(): void {
if (!this.arrowNode || !this._targetNode) {
return;
}
const position = new Vec3(Vec3.ZERO);
if (this._currentStep?.arrowOffset) {
const targetWorldPos = this._targetNode.getWorldPosition();
const localPos = new Vec3();
this.node
.getComponent(UITransform)
.convertToNodeSpaceAR(targetWorldPos, localPos);
position.x = localPos.x + this._currentStep.arrowOffset.x;
position.y = localPos.y + this._currentStep.arrowOffset.y;
} else {
const targetWorldPos = this._targetNode.getWorldPosition();
const localPos = new Vec3();
this.node
.getComponent(UITransform)
.convertToNodeSpaceAR(targetWorldPos, localPos);
position.x = localPos.x + this.defaultArrowOffset.x;
position.y = localPos.y + this.defaultArrowOffset.y;
}
this.arrowNode.setPosition(position);
}
/**
* 更新箭头方向
*/
private updateArrowDirection(): void {
if (!this.arrowNode || !this._currentStep) {
return;
}
const direction =
this._currentStep.arrowDirection || GuideArrowDirection.DOWN;
let angle = 0;
switch (direction) {
case GuideArrowDirection.UP:
angle = 0;
break;
case GuideArrowDirection.DOWN:
angle = 180;
break;
case GuideArrowDirection.LEFT:
angle = 90;
break;
case GuideArrowDirection.RIGHT:
angle = -90;
break;
case GuideArrowDirection.UP_LEFT:
angle = 45;
break;
case GuideArrowDirection.UP_RIGHT:
angle = -45;
break;
case GuideArrowDirection.DOWN_LEFT:
angle = 135;
break;
case GuideArrowDirection.DOWN_RIGHT:
angle = -135;
break;
}
this.arrowNode.setRotationFromEuler(0, 0, angle);
}
/**
* 隐藏箭头
*/
private hideArrow(): void {
if (this.arrowNode) {
this.arrowNode.active = false;
}
}
/**
* 获取当前步骤
*/
public getCurrentStep(): IGuideStepConfig | null {
return this._currentStep;
}
/**
* 获取目标节点
*/
public getTargetNode(): Node | null {
return this._targetNode;
}
/**
* 刷新显示
*/
public refresh(): void {
if (this._currentStep) {
this.showStep(this._currentStep);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "6881b25d-9574-41af-a9e8-3bee7b53349f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,147 @@
import { _decorator, Component, Button } from "cc";
import { GuideManager } from "./GuideManager";
import { IGuideStepConfig } from "./GuideData";
import LogUtils from "../utils/LogUtils";
const { ccclass } = _decorator;
const TAG = "GuideTargetComponent";
/**
* 引导目标组件
* 用于标识当前引导步骤的目标节点
*/
@ccclass("GuideTargetComponent")
export class GuideTargetComponent extends Component {
private _guideId: string = "";
private _stepId: string = "";
private _stepConfig: IGuideStepConfig | null = null;
private _buttonComponent: Button | null = null;
onLoad() {
this.setupButtonListener();
}
onDestroy() {
this.removeButtonListener();
LogUtils.info(
TAG,
`引导目标组件已销毁: ${this._guideId}-${this._stepId}`
);
}
/**
* 设置引导信息
* @param guideId 引导ID
* @param stepId 步骤ID
* @param stepConfig 步骤配置
*/
public setGuideInfo(
guideId: string,
stepId: string,
stepConfig: IGuideStepConfig
): void {
this._guideId = guideId;
this._stepId = stepId;
this._stepConfig = stepConfig;
// 重新设置按钮监听
this.setupButtonListener();
LogUtils.info(
TAG,
`设置引导目标: ${guideId}-${stepId}, 自动下一步: ${stepConfig.autoNextStep || false}`
);
}
/**
* 获取引导ID
*/
public getGuideId(): string {
return this._guideId;
}
/**
* 获取步骤ID
*/
public getStepId(): string {
return this._stepId;
}
/**
* 获取步骤配置
*/
public getStepConfig(): IGuideStepConfig | null {
return this._stepConfig;
}
/**
* 设置按钮监听
*/
private setupButtonListener(): void {
// 移除之前的监听
this.removeButtonListener();
// 检查是否有Button组件
this._buttonComponent = this.node.getComponent(Button);
if (!this._buttonComponent) {
return;
}
// 只有配置了自动进入下一步才添加监听
if (!this._stepConfig?.autoNextStep) {
return;
}
// 添加点击监听
this._buttonComponent.node.on(
Button.EventType.CLICK,
this.onButtonClick,
this
);
LogUtils.info(
TAG,
`已添加按钮点击监听: ${this._guideId}-${this._stepId}`
);
}
/**
* 移除按钮监听
*/
private removeButtonListener(): void {
if (this._buttonComponent) {
this._buttonComponent.node.off(
Button.EventType.CLICK,
this.onButtonClick,
this
);
this._buttonComponent = null;
}
}
/**
* 按钮点击处理
*/
private onButtonClick(): void {
if (!this._stepConfig?.autoNextStep) {
return;
}
LogUtils.info(
TAG,
`按钮被点击,自动进入下一步: ${this._guideId}-${this._stepId}`
);
// 延迟执行,确保按钮的原始点击事件先执行
this.scheduleOnce(() => {
const guideManager = GuideManager.getInstance();
if (
guideManager &&
guideManager.getCurrentGuide()?.config.id === this._guideId &&
this._stepId == guideManager.getCurrentStep()?.id
) {
guideManager.nextStep();
}
}, 0.1);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c85c1205-8f1f-4cf8-b6b5-0e2a8029d276",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "fa254158-c411-49a6-98e5-506cf9e1dca8",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,412 @@
import { HttpError, HttpResponse, IResponseData } from "./HttpRequest";
import LogUtils from "../utils/LogUtils";
import { HttpMethod, NetworkConfig } from "./Types";
import { StringUtils } from "../utils/StringUtils";
import { Toast } from "../ui/default/DefaultToast";
const TAG = "HttpClient";
interface RequestQueue<T = any> {
abortController: AbortController;
callbacks: Array<(response: HttpResponse<T>) => void>;
responsePromise: Promise<HttpResponse<T>>;
}
export default class HttpClient {
private rootUrl: string = "";
private headers: Map<string, string> = new Map();
private requestQueues: Map<string, RequestQueue<any>> = new Map();
private readonly timeout: number = 30000; // 30秒超时
private readonly maxRetries: number = 3;
private token: string = "";
constructor(cfg: NetworkConfig) {
// 设置默认请求头
this.headers.set("Content-Type", "application/json");
this.rootUrl = cfg.httpRootUrl;
this.timeout = cfg.defaultTimeout;
this.maxRetries = cfg.defaultRetryCount;
}
public setHeader(key: string, value: string) {
this.headers.set(key, value);
}
public async send<T extends IResponseData>(
url: string,
method: HttpMethod,
data: unknown,
callback: (response: HttpResponse<T>) => void = null,
headers?: Map<string, string>
): Promise<HttpResponse<T>> {
// 生成请求签名,包含 URL、方法、数据和头部信息
const requestSignature = this.generateRequestSignature(
url,
method,
data,
headers
);
// 检查是否已有相同的请求在进行中
const reqInfo = this.requestQueues.get(requestSignature);
if (reqInfo) {
// 复用现有请求,将回调添加到队列中
reqInfo.callbacks.push(callback);
return reqInfo.responsePromise;
}
if (!url.toLowerCase().startsWith("http")) {
url = this.rootUrl + url;
}
// 创建新的请求
const abortController = new AbortController();
const callbacks = [callback];
const responsePromise = this.executeRequest<T>(
url,
method,
data,
headers,
abortController
);
// 将请求添加到队列
this.requestQueues.set(requestSignature, {
abortController,
callbacks,
responsePromise,
});
// 处理响应并通知所有回调
responsePromise
.then((response) => {
const queue = this.requestQueues.get(requestSignature);
if (queue) {
for (const tempCallback of queue.callbacks) {
try {
if (tempCallback) {
tempCallback(response);
}
} catch (callError) {
LogUtils.error(
TAG,
"回调函数执行失败",
callError.message,
callError.stack
);
}
}
this.requestQueues.delete(requestSignature);
}
})
.catch((err) => {
LogUtils.error(TAG, "请求失败", err.message, err.stack);
const queue = this.requestQueues.get(requestSignature);
if (queue) {
const errorResponse: HttpResponse<T> = {
success: false,
message: HttpError.UNKNOWN_ERROR,
};
for (const tempCallback of queue.callbacks) {
try {
if (tempCallback) {
tempCallback(errorResponse);
}
} catch (callError) {
LogUtils.error(
TAG,
"回调函数执行失败",
callError.message,
callError.stack
);
}
}
this.requestQueues.delete(requestSignature);
}
});
return responsePromise;
}
private async executeRequest<T extends IResponseData>(
url: string,
method: HttpMethod,
data: unknown,
headers?: Map<string, string>,
abortController?: AbortController,
retryCount: number = 0
): Promise<HttpResponse<T>> {
try {
// 合并请求头
const requestHeaders = new Headers();
// 添加全局头部
for (const [key, value] of this.headers.entries()) {
requestHeaders.set(key, value);
}
// 添加请求特定头部
if (headers) {
for (const [key, value] of headers.entries()) {
requestHeaders.set(key, value);
}
}
if (!StringUtils.isEmpty(this.token)) {
let auth = this.token;
if (!this.token.startsWith("Bearer ")) {
auth = `Bearer ${this.token}`;
}
requestHeaders.set("Authorization", auth);
}
// 构建请求配置
const requestConfig: RequestInit = {
method,
headers: requestHeaders,
signal: abortController?.signal,
};
// 处理请求数据
if (method === "POST" && data) {
requestConfig.body = JSON.stringify(data);
} else if (method === "GET" && data) {
const paramString = this.getParamString(data);
url += "?" + paramString;
}
// 创建超时Promise
const timeoutPromise = new Promise<never>((resolve, reject) => {
setTimeout(() => {
reject(new Error("Request timeout"));
}, this.timeout);
});
// 执行请求
const response = await Promise.race([
fetch(url, requestConfig),
timeoutPromise,
]);
// 检查响应状态
if (!response.ok) {
const error =
response.status >= 500
? new Error("Server error")
: new Error(`HTTP ${response.status}`);
throw error;
}
// 解析响应数据
let responseData: HttpResponse<T>;
try {
responseData = await response.json();
} catch (err) {
LogUtils.warn("HttpClient", "响应数据解析失败", err);
return {
success: false,
message: err.message,
};
}
if (
!responseData.success &&
!StringUtils.isEmpty(responseData.message)
) {
Toast.show(responseData.message);
}
if (response.headers.get("Authorization")?.startsWith("Bearer ")) {
LogUtils.log(
"HttpClient",
"获取到新的token",
response.headers.get("Authorization")
);
this.token = response.headers.get("Authorization");
}
return responseData;
} catch (err: any) {
// 处理取消请求
if (err.name === "AbortError") {
LogUtils.info("HttpClient", "请求已取消", url);
return {
success: false,
message: HttpError.CANCEL,
};
}
if (retryCount < this.maxRetries) {
LogUtils.info(
"HttpClient",
`重试请求 ${retryCount + 1}/${this.maxRetries}`,
url
);
return this.executeRequest(
url,
method,
data,
headers,
abortController,
retryCount + 1
);
}
let errMessage = HttpError.UNKNOWN_ERROR;
// 处理超时
if (err.message === "Request timeout") {
errMessage = HttpError.TIMEOUT;
} else if (err.message === "Server error") {
errMessage = HttpError.NO_NETWORK;
}
return {
success: false,
message: errMessage,
};
}
}
/**
* 取消指定签名的请求
*/
public cancelRequest(
url: string,
method: "GET" | "POST",
data: any,
headers?: Map<string, string>
): boolean {
const requestSignature = this.generateRequestSignature(
url,
method,
data,
headers
);
const queue = this.requestQueues.get(requestSignature);
if (queue) {
// 中止 fetch 请求
queue.abortController.abort();
// 通知所有回调请求被取消
const cancelResponse: HttpResponse<any> = {
success: false,
message: HttpError.CANCEL,
};
for (const callback of queue.callbacks) {
callback(cancelResponse);
}
// 清理队列
this.requestQueues.delete(requestSignature);
LogUtils.info("HttpClient", "请求已取消", requestSignature);
return true;
}
return false;
}
/**
* 取消所有进行中的请求
*/
public cancelAllRequests(): void {
const cancelResponse: HttpResponse<any> = {
success: false,
message: HttpError.CANCEL,
};
for (const [, queue] of this.requestQueues.entries()) {
queue.abortController.abort();
for (const callback of queue.callbacks) {
callback(cancelResponse);
}
}
const count = this.requestQueues.size;
this.requestQueues.clear();
LogUtils.info("HttpClient", `已取消所有请求,共 ${count}`);
}
/**
* 获取当前进行中的请求数量
*/
public getPendingRequestCount(): number {
return this.requestQueues.size;
}
/**
* 检查指定请求是否正在进行中
*/
public isRequestPending(
url: string,
method: "GET" | "POST",
data: any,
headers?: Map<string, string>
): boolean {
const requestSignature = this.generateRequestSignature(
url,
method,
data,
headers
);
return this.requestQueues.has(requestSignature);
}
/**
* 生成请求签名,用于识别相同的请求
*/
private generateRequestSignature(
url: string,
method: string,
data: any,
headers?: Map<string, string>
): string {
// 确保 URL 是完整的
if (!url.toLowerCase().startsWith("http")) {
url = this.rootUrl + url;
}
// 序列化数据
const dataStr = data ? JSON.stringify(data) : "";
// 序列化头部信息(合并全局头部和请求特定头部)
const allHeaders = new Map(this.headers);
if (headers) {
for (const [key, value] of headers.entries()) {
allHeaders.set(key, value);
}
}
const headersArray = Array.from(allHeaders.entries()).sort();
const headersStr = JSON.stringify(headersArray);
// 生成签名
return `${method}:${url}:${dataStr}:${headersStr}`;
}
/**
* 将参数对象转换为查询字符串
*/
private getParamString(parameters: any): string {
if (!parameters) return "";
const params = new URLSearchParams();
for (const name in parameters) {
const data = parameters[name];
if (data instanceof Object && !(data instanceof Array)) {
for (const key in data) {
params.append(key, data[key]);
}
} else {
params.append(name, data);
}
}
return params.toString();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "730e6326-afb0-46ca-be45-32e4eccc822a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,156 @@
/** 请求事件 */
export enum HttpError {
/** 断网 */
NO_NETWORK = "http_request_no_network",
/** 未知错误 */
UNKNOWN_ERROR = "http_request_unknown_error",
/** 请求超时 */
TIMEOUT = "http_request_timout",
/** 重复请求 */
REAPEAT = "http_request_repeat",
/** 取消请求 */
CANCEL = "http_request_cancel",
}
export interface IResponseData {
/** 库存数据 */
inventoryDataList?: {
id: string;
count: number;
}[];
}
export interface HttpResponse<T extends IResponseData> {
/** 是否请求成功 */
success: boolean;
/** 请求返回数据 */
data?: T;
/** 请求错误数据 */
message?: HttpError;
}
export default class HttpRequest<T = any> {
private url: string;
private method: "POST" | "GET";
private postData: any;
private request: XMLHttpRequest;
private callback: (response: HttpResponse<T>) => void;
private maxRetries: number = 3;
private retryCount: number = 0;
private headers: Map<string, string> = new Map();
constructor(
url: string,
parameters: any,
method: "POST" | "GET",
callback: (response: HttpResponse<T>) => void,
isOpenTimeout: boolean = true,
) {
this.callback = callback;
if (parameters && method == "GET") {
const parametersStr = this.getParamString(parameters);
url += url.indexOf("?") > -1 ? "&" + parametersStr : "?" + parametersStr;
}
this.url = url;
this.postData = parameters;
this.createHttpRequest();
}
public setHeader(key: string, value: string) {
this.headers.set(key, value);
this.request.setRequestHeader(key, value);
}
public send() {
this.request.send(this.postData);
}
/**
* 中止请求
*/
public abort() {
if (this.request) {
this.request.abort();
}
}
private onTimeout() {
this.handleError(HttpError.TIMEOUT);
}
private onLoadend() {
if (this.request.status == 500) {
this.handleError(HttpError.NO_NETWORK);
}
}
private onError() {
this.handleError(HttpError.UNKNOWN_ERROR);
}
private onReadyStateChange() {
if (this.request.readyState != 4) {
return;
}
if (this.request.status == 200) {
try {
const res = JSON.parse(this.request.responseText);
this.callback({
success: true,
data: res,
});
} catch (error) {
this.callback({
success: true,
message: this.request.response,
});
}
} else {
this.handleError(HttpError.UNKNOWN_ERROR);
}
}
private handleError(error: HttpError) {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
this.request.abort();
this.createHttpRequest();
this.send();
} else {
this.callback({
success: false,
message: error,
});
}
}
private createHttpRequest() {
this.request = new XMLHttpRequest();
this.request.open(this.method, this.url);
this.request.timeout = 30000;
this.request.ontimeout = this.onTimeout.bind(this);
this.request.onloadend = this.onLoadend.bind(this);
this.request.onerror = this.onError.bind(this);
this.request.onreadystatechange = this.onReadyStateChange.bind(this);
if (this.headers != null) {
for (const [key, value] of this.headers.entries()) {
this.request.setRequestHeader(key, value);
}
}
}
private getParamString(parameters: any) {
let result = "";
for (const name in parameters) {
const data = parameters[name];
if (data instanceof Object) {
for (const key in data) result += `${key}=${data[key]}&`;
} else {
result += `${name}=${data}&`;
}
}
return result.substring(0, result.length - 1);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "76617246-abfb-4bfe-8178-9fddff43a135",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,89 @@
import LogUtils from "../utils/LogUtils";
import { WebSocketClient } from "./WebSocketClient";
import { SerializerManager } from "./Serializer";
import { WebSocketConfig } from "./Types";
import { singleton, Singleton } from "../Singleton";
import { ResManager } from "../res/ResManager";
import { JsonAsset } from "cc";
import { StringUtils } from "../utils/StringUtils";
import HttpClient from "./HttpClient";
const TAG = "Network";
/**
* 网络管理器
*/
@singleton({auto: true})
export class NetworkManager extends Singleton {
private httpClient: HttpClient;
private webSocketClients = new Map<string, WebSocketClient>();
private serializerManager: SerializerManager;
protected async onInit() {
LogUtils.info(TAG, "初始化网络管理器");
this.serializerManager = new SerializerManager();
const asset = await ResManager.getInstance().loadAsset<JsonAsset>(
"net-config",
JsonAsset,
);
if (StringUtils.isEmpty(asset?.json)) {
LogUtils.error(TAG, "加载网络配置失败");
return;
}
this.httpClient = new HttpClient(asset.json);
LogUtils.info(TAG, "网络管理器初始化完成");
}
/**
* 获取HTTP客户端
*/
public getHttpClient(): HttpClient {
return this.httpClient;
}
/**
* 创建WebSocket客户端
*/
public createWebSocketClient(
name: string,
config: WebSocketConfig,
): WebSocketClient {
if (this.webSocketClients.has(name)) {
LogUtils.warn(TAG, `WebSocket客户端已存在: ${name}`);
return this.webSocketClients.get(name)!;
}
const client = new WebSocketClient(config);
this.webSocketClients.set(name, client);
LogUtils.info(TAG, `创建WebSocket客户端: ${name}`);
return client;
}
/**
* 获取WebSocket客户端
*/
public getWebSocketClient(name: string): WebSocketClient | null {
return this.webSocketClients.get(name) || null;
}
/**
* 获取序列化管理器
*/
public getSerializerManager(): SerializerManager {
return this.serializerManager;
}
/**
* 销毁网络管理器
*/
public destroy(): void {
LogUtils.info(TAG, "销毁网络管理器");
for (const [, client] of this.webSocketClients)
void client.disconnect();
this.webSocketClients.clear();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "07ed05f0-db2f-4006-b25d-5a9e271c5826",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,92 @@
import LogUtils from "../utils/LogUtils";
import { SerializationType } from "./Types";
/**
* 数据序列化器接口
*/
export interface ISerializer {
serialize(data: any): ArrayBuffer | string;
deserialize(data: ArrayBuffer | string): any;
}
/**
* JSON序列化器
*/
export class JsonSerializer implements ISerializer {
serialize(data: any): string {
return JSON.stringify(data);
}
deserialize(data: string): any {
return JSON.parse(data);
}
}
/**
* Protobuf序列化器
*/
export class ProtobufSerializer implements ISerializer {
private protoRoot: any;
constructor(protoRoot: any) {
this.protoRoot = protoRoot;
}
serialize(data: any): ArrayBuffer {
// TODO: 实现Protobuf序列化逻辑
throw new Error("Protobuf序列化未实现");
}
deserialize(data: ArrayBuffer): any {
// TODO: 实现Protobuf反序列化逻辑
throw new Error("Protobuf反序列化未实现");
}
}
/**
* 序列化器管理器
*/
export class SerializerManager {
private static readonly TAG = "SerializerManager";
private serializers = new Map<SerializationType, ISerializer>();
constructor() {
this.registerSerializer("json", new JsonSerializer());
}
/**
* 注册序列化器
*/
public registerSerializer(
type: SerializationType,
serializer: ISerializer
): void {
this.serializers.set(type, serializer);
LogUtils.debug(SerializerManager.TAG, `注册序列化器: ${type}`);
}
/**
* 序列化数据
*/
public serialize(data: any, type: SerializationType): ArrayBuffer | string {
const serializer = this.serializers.get(type);
if (!serializer) {
throw new Error(`未找到序列化器: ${type}`);
}
return serializer.serialize(data);
}
/**
* 反序列化数据
*/
public deserialize(
data: ArrayBuffer | string,
type: SerializationType
): any {
const serializer = this.serializers.get(type);
if (!serializer) {
throw new Error(`未找到序列化器: ${type}`);
}
return serializer.deserialize(data);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b2799e8e-596a-49b9-a71b-898e604601aa",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,101 @@
/**
* 网络模块核心类型定义
*/
/** HTTP请求方法 */
export enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
PATCH = "PATCH",
HEAD = "HEAD",
OPTIONS = "OPTIONS",
}
/** 数据序列化类型 */
export type SerializationType = "json" | "protobuf" | "binary" | "text";
/** 网络请求状态 */
export enum NetworkStatus {
IDLE = "idle",
CONNECTING = "connecting",
CONNECTED = "connected",
DISCONNECTED = "disconnected",
ERROR = "error",
RECONNECTING = "reconnecting",
}
/** HTTP请求配置 */
export interface HttpRequestConfig {
url: string;
method: HttpMethod;
headers?: Record<string, string>;
data?: any;
timeout?: number;
retryCount?: number;
retryDelay?: number;
responseType?: SerializationType;
withCredentials?: boolean;
}
/** HTTP响应数据 */
export interface HttpResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: Record<string, string>;
config: HttpRequestConfig;
}
/** WebSocket配置 */
export interface WebSocketConfig {
url: string;
protocols?: string[];
reconnectInterval?: number;
maxReconnectAttempts?: number;
heartbeatInterval?: number;
heartbeatTimeout?: number;
binaryType?: "blob" | "arraybuffer";
autoReconnect?: boolean;
}
/** WebSocket消息 */
export interface WebSocketMessage {
id: string;
type: SerializationType;
data: any;
timestamp: number;
}
/** 网络错误 */
export interface NetworkError {
code: string;
message: string;
details?: any;
timestamp: number;
}
/** Worker消息类型 */
export enum WorkerMessageType {
HTTP_REQUEST = "http_request",
HTTP_RESPONSE = "http_response",
WS_CONNECT = "ws_connect",
WS_DISCONNECT = "ws_disconnect",
WS_SEND = "ws_send",
WS_MESSAGE = "ws_message",
WS_STATUS = "ws_status",
ERROR = "error",
HEARTBEAT = "heartbeat",
}
/** 网络配置 */
export interface NetworkConfig {
httpRootUrl?: string;
wsRootUrl?: string;
workerPath?: string;
defaultTimeout?: number;
defaultRetryCount?: number;
defaultRetryDelay?: number;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "2c350f80-6720-4da3-8d80-ed9485b75eab",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,98 @@
import LogUtils from "../utils/LogUtils";
import {
WebSocketConfig,
WebSocketMessage,
NetworkStatus,
SerializationType,
} from "./Types";
const TAG = "Network";
/**
* WebSocket客户端
*/
export class WebSocketClient {
private config: WebSocketConfig;
private status: NetworkStatus = NetworkStatus.IDLE;
private messageHandlers = new Map<
string,
(message: WebSocketMessage) => void
>();
private statusHandlers: Array<(status: NetworkStatus) => void> = [];
constructor(config: WebSocketConfig) {
this.config = {
reconnectInterval: 5000,
maxReconnectAttempts: 5,
heartbeatInterval: 30000,
heartbeatTimeout: 10000,
binaryType: "arraybuffer",
autoReconnect: true,
...config,
};
this.setupWorkerHandlers();
}
/**
* 连接WebSocket
*/
public async connect(): Promise<void> {
// TODO: 实现WebSocket连接逻辑
LogUtils.info(TAG, `连接WebSocket: ${this.config.url}`);
this.setStatus(NetworkStatus.CONNECTING);
}
/**
* 断开连接
*/
public async disconnect(): Promise<void> {
// TODO: 实现WebSocket断开逻辑
LogUtils.info(TAG, "断开WebSocket连接");
}
/**
* 发送消息
*/
public async send(
data: any,
type: SerializationType = "json",
): Promise<void> {
// TODO: 实现消息发送逻辑
LogUtils.debug(TAG, `发送WebSocket消息: ${type}`);
}
/**
* 注册消息处理器
*/
public onMessage(
type: string,
handler: (message: WebSocketMessage) => void,
): void {
this.messageHandlers.set(type, handler);
}
/**
* 注册状态变化处理器
*/
public onStatusChange(handler: (status: NetworkStatus) => void): void {
this.statusHandlers.push(handler);
}
/**
* 获取当前状态
*/
public getStatus(): NetworkStatus {
return this.status;
}
private setupWorkerHandlers(): void {
// TODO: 设置Worker消息处理器
}
private setStatus(status: NetworkStatus): void {
if (this.status !== status) {
this.status = status;
for (const handler of this.statusHandlers) handler(status);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5dc01552-598f-40da-8382-3f2dd4799ece",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "fffeb1ff-b3ae-42be-a006-629da68909b3",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "9ed356c0-4f7d-4699-a4d9-4d649726e57b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,269 @@
import { _decorator, Component, Node, Label } from "cc";
import { RedPointManager } from "./RedPointManager";
import { IRedPointData, RedPointState, IRedPointEvent } from "./RedPointData";
const { ccclass, property, executeInEditMode } = _decorator;
/**
* 红点组件
* 用于在Cocos Creator节点上显示红点
*/
@ccclass("RedPointComponent")
@executeInEditMode
export class RedPointComponent extends Component {
@property({
displayName: "红点ID",
tooltip: "红点的唯一标识符",
})
public redPointId: string = "";
@property({
displayName: "最大显示数字",
tooltip: "超过此数字显示为99+",
})
public maxNumber: number = 99;
@property({
displayName: "红点节点",
type: Node,
tooltip: "显示红点圆点的节点",
})
public dotNode: Node | null = null;
@property({
displayName: "数字标签",
type: Label,
tooltip: "显示数字的Label组件",
})
public numberLabel: Label | null = null;
private _currentData: IRedPointData | null = null;
private _isListening: boolean = false;
protected onLoad(): void {
this.findChildNodes();
}
protected onEnable(): void {
if (this.redPointId && !this._isListening) {
this.startListening();
}
this.updateDisplay();
}
protected onDisable(): void {
this.stopListening();
}
protected onDestroy(): void {
this.stopListening();
}
/**
* 自动查找子节点
*/
private findChildNodes(): void {
if (!this.dotNode) {
this.dotNode = this.node.getChildByName("RedDot");
}
if (!this.numberLabel) {
const numberNode = this.node.getChildByName("RedNumber");
if (numberNode) {
this.numberLabel = numberNode.getComponent(Label);
}
}
}
/**
* 开始监听红点变化
*/
private startListening(): void {
if (this._isListening) return;
RedPointManager.getInstance().addListener(
this.redPointId,
this.onRedPointChanged.bind(this),
);
this._isListening = true;
// 获取当前数据
this._currentData = RedPointManager.getInstance().getRedPoint(
this.redPointId,
);
}
/**
* 停止监听红点变化
*/
private stopListening(): void {
if (!this._isListening) return;
RedPointManager.getInstance().removeListener(
this.redPointId,
this.onRedPointChanged.bind(this),
);
this._isListening = false;
}
/**
* 红点变化回调
* @param event 红点事件
*/
private onRedPointChanged(event: IRedPointEvent): void {
if (event.redPointId === this.redPointId) {
this._currentData =
RedPointManager.getInstance()?.getRedPoint(this.redPointId) ||
null;
this.updateDisplay();
}
}
/**
* 更新显示
*/
private updateDisplay(): void {
if (!this._currentData?.enabled) {
this.hideAll();
return;
}
const state = this._currentData.state;
const count = this._currentData.count;
if (state === RedPointState.NONE) {
this.hideAll();
return;
}
if (state === RedPointState.SHOW) {
this.showDot(true);
this.showNumber(false);
return;
}
if (state === RedPointState.NUMBER) {
this.showDot(false);
this.showNumber(true, count);
return;
}
}
/**
* 显示/隐藏红点
* @param show 是否显示
*/
private showDot(show: boolean): void {
if (this.dotNode) {
this.dotNode.active = show;
}
}
/**
* 显示/隐藏数字
* @param show 是否显示
* @param count 数字
*/
private showNumber(show: boolean, count: number = 0): void {
if (this.numberLabel) {
this.numberLabel.node.active = show;
}
if (show && this.numberLabel) {
let displayText = count.toString();
if (count > this.maxNumber) {
displayText = this.maxNumber + "+";
}
this.numberLabel.string = displayText;
}
}
/**
* 隐藏所有
*/
private hideAll(): void {
this.showDot(false);
this.showNumber(false);
}
/**
* 设置红点ID
* @param redPointId 红点ID
*/
public setRedPointId(redPointId: string): void {
if (this.redPointId === redPointId) return;
this.stopListening();
this.redPointId = redPointId;
if (this.enabled && this.redPointId) {
this.startListening();
this.updateDisplay();
}
}
/**
* 手动刷新显示
*/
public refresh(): void {
this._currentData = RedPointManager.getInstance().getRedPoint(
this.redPointId,
);
this.updateDisplay();
}
/**
* 获取当前红点数据
*/
public getCurrentData(): IRedPointData | null {
return this._currentData;
}
/**
* 是否有红点
*/
public hasRedPoint(): boolean {
return this._currentData
? this._currentData.enabled &&
this._currentData.state !== RedPointState.NONE
: false;
}
/**
* 获取红点数量
*/
public getCount(): number {
return this._currentData ? this._currentData.count : 0;
}
/**
* 创建红点节点
* @param parent 父节点
* @param redPointId 红点ID
* @returns 红点组件
*/
public static create(parent: Node, redPointId: string): RedPointComponent {
const redPointNode = new Node("RedPoint");
parent.addChild(redPointNode);
const component = redPointNode.addComponent(RedPointComponent);
component.redPointId = redPointId;
return component;
}
/**
* 快速设置红点
* @param node 节点
* @param redPointId 红点ID
* @returns 红点组件
*/
public static setup(node: Node, redPointId: string): RedPointComponent {
let component = node.getComponent(RedPointComponent);
if (!component) {
component = node.addComponent(RedPointComponent);
}
component.setRedPointId(redPointId);
return component;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8fbf52b3-a3d6-43a9-a663-1e63c87201d5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,85 @@
/**
* 红点数据接口
* 简单易用的红点系统核心数据结构
*/
/** 红点状态枚举 */
export enum RedPointState {
/** 无红点 */
NONE = 0,
/** 显示红点 */
SHOW = 1,
/** 显示数字 */
NUMBER = 2,
}
/** 红点数据接口 */
export interface IRedPointData {
/** 红点唯一标识 */
id: string;
/** 红点状态 */
state: RedPointState;
/** 红点数量当state为NUMBER时显示 */
count: number;
/** 是否启用 */
enabled: boolean;
/** 父节点ID用于层级关系 */
parentId?: string;
/** 子节点ID列表 */
children?: string[];
/** 最后更新时间 */
updateTime: number;
/** 自定义数据 */
customData?: unknown;
}
/** 红点配置接口 */
export interface IRedPointConfig {
/** 红点ID */
id: string;
/** 显示名称 */
name?: string;
/** 是否自动计算(基于子节点) */
autoCalculate?: boolean;
/** 最大显示数量超过显示99+ */
maxCount?: number;
/** 是否持久化 */
persistent?: boolean;
/** 自定义计算函数 */
calculator?: () => { state: RedPointState; count: number };
}
/** 红点事件类型 */
export enum RedPointEventType {
/** 红点状态改变 */
STATE_CHANGED = "redpoint_state_changed",
/** 红点数量改变 */
COUNT_CHANGED = "redpoint_count_changed",
/** 红点添加 */
ADDED = "redpoint_added",
/** 红点移除 */
REMOVED = "redpoint_removed",
}
/** 红点事件数据 */
export interface IRedPointEvent {
/** 事件类型 */
type: RedPointEventType;
/** 红点ID */
redPointId: string;
/** 旧值 */
oldValue?: unknown;
/** 新值 */
newValue?: unknown;
/** 时间戳 */
timestamp: number;
}
/** 红点监听器 */
export type RedPointListener = (event: IRedPointEvent) => void;
/** 红点过滤器 */
export type RedPointFilter = (data: IRedPointData) => boolean;
/** 红点排序器 */
export type RedPointSorter = (a: IRedPointData, b: IRedPointData) => number;

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a5ded0f5-f34f-4d00-a1b5-d42ad177edd9",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,432 @@
import { EventManager } from "../event/EventManager";
import NodeSingleton from "../NodeSingleton";
import TimeManager from "../timer/TimerManager";
import LogUtils from "../utils/LogUtils";
import {
IRedPointData,
IRedPointConfig,
RedPointState,
RedPointEventType,
IRedPointEvent,
RedPointListener,
RedPointFilter,
RedPointSorter,
} from "./RedPointData";
/**
* 红点管理器
* 简单易用的红点系统核心管理类
*/
export class RedPointManager extends NodeSingleton {
private _redPoints: Map<string, IRedPointData> = new Map();
private _configs: Map<string, IRedPointConfig> = new Map();
private _listeners: Map<string, RedPointListener[]> = new Map();
private _storageKey = "redpoint_data";
protected onLoad(): void {
this.loadFromStorage();
}
/**
* 注册红点配置
* @param config 红点配置
*/
public registerConfig(config: IRedPointConfig): void {
this._configs.set(config.id, config);
// 如果红点不存在,创建默认红点
if (!this._redPoints.has(config.id)) {
this.createRedPoint(config.id);
}
}
/**
* 创建红点
* @param id 红点ID
* @param parentId 父节点ID
*/
public createRedPoint(id: string, parentId?: string): IRedPointData {
if (this._redPoints.has(id)) {
return this._redPoints.get(id);
}
const redPoint: IRedPointData = {
id,
state: RedPointState.NONE,
count: 0,
enabled: true,
parentId,
children: [],
updateTime: TimeManager.getInstance().getCorrectedTime(),
};
this._redPoints.set(id, redPoint);
// 建立父子关系
if (parentId && this._redPoints.has(parentId)) {
const parent = this._redPoints.get(parentId);
if (!parent.children) parent.children = [];
if (parent.children.indexOf(id) === -1) {
parent.children.push(id);
}
}
this.emitEvent({
type: RedPointEventType.ADDED,
redPointId: id,
newValue: redPoint,
timestamp: TimeManager.getInstance().getCorrectedTime(),
});
return redPoint;
}
/**
* 获取红点数据
* @param id 红点ID
*/
public getRedPoint(id: string): IRedPointData | null {
return this._redPoints.get(id) || null;
}
/**
* 设置红点状态
* @param id 红点ID
* @param state 红点状态
* @param count 红点数量
*/
public setRedPoint(
id: string,
state: RedPointState,
count: number = 0,
): void {
let redPoint = this._redPoints.get(id);
if (!redPoint) {
redPoint = this.createRedPoint(id);
}
const oldState = redPoint.state;
const oldCount = redPoint.count;
redPoint.state = state;
redPoint.count = Math.max(0, count);
redPoint.updateTime = Date.now();
// 触发事件
if (oldState !== state) {
this.emitEvent({
type: RedPointEventType.STATE_CHANGED,
redPointId: id,
oldValue: oldState,
newValue: state,
timestamp: Date.now(),
});
}
if (oldCount !== count) {
this.emitEvent({
type: RedPointEventType.COUNT_CHANGED,
redPointId: id,
oldValue: oldCount,
newValue: count,
timestamp: Date.now(),
});
}
// 更新父节点
this.updateParentRedPoint(id);
this.saveToStorage();
}
/**
* 显示红点
* @param id 红点ID
* @param count 数量(可选)
*/
public showRedPoint(id: string, count?: number): void {
if (count !== undefined && count > 0) {
this.setRedPoint(id, RedPointState.NUMBER, count);
} else {
this.setRedPoint(id, RedPointState.SHOW, 0);
}
}
/**
* 隐藏红点
* @param id 红点ID
*/
public hideRedPoint(id: string): void {
this.setRedPoint(id, RedPointState.NONE, 0);
}
/**
* 增加红点数量
* @param id 红点ID
* @param count 增加的数量
*/
public addRedPointCount(id: string, count: number): void {
const redPoint = this.getRedPoint(id);
const currentCount = redPoint ? redPoint.count : 0;
this.setRedPoint(id, RedPointState.NUMBER, currentCount + count);
}
/**
* 减少红点数量
* @param id 红点ID
* @param count 减少的数量
*/
public reduceRedPointCount(id: string, count: number): void {
const redPoint = this.getRedPoint(id);
if (!redPoint) return;
const newCount = Math.max(0, redPoint.count - count);
const newState =
newCount > 0 ? RedPointState.NUMBER : RedPointState.NONE;
this.setRedPoint(id, newState, newCount);
}
/**
* 移除红点
* @param id 红点ID
*/
public removeRedPoint(id: string): void {
const redPoint = this._redPoints.get(id);
if (!redPoint) return;
// 移除父子关系
if (redPoint.parentId) {
const parent = this._redPoints.get(redPoint.parentId);
if (parent?.children) {
const index = parent.children.indexOf(id);
if (index !== -1) {
parent.children.splice(index, 1);
}
}
}
// 移除子节点
if (redPoint.children) {
for (const childId of redPoint.children) {
this.removeRedPoint(childId);
}
}
this._redPoints.delete(id);
this._configs.delete(id);
this.emitEvent({
type: RedPointEventType.REMOVED,
redPointId: id,
oldValue: redPoint,
timestamp: Date.now(),
});
this.saveToStorage();
}
/**
* 更新父节点红点状态
* @param childId 子节点ID
*/
private updateParentRedPoint(childId: string): void {
const child = this._redPoints.get(childId);
if (!child?.parentId) return;
const parent = this._redPoints.get(child.parentId);
if (!parent) return;
const config = this._configs.get(child.parentId);
if (config && config.autoCalculate === false) return;
// 计算子节点状态
let hasShow = false;
let totalCount = 0;
if (parent.children) {
for (const childId of parent.children) {
const child = this._redPoints.get(childId);
if (
child &&
child.enabled &&
child.state !== RedPointState.NONE
) {
hasShow = true;
if (child.state === RedPointState.NUMBER) {
totalCount += child.count;
}
}
}
}
// 更新父节点状态
const newState = hasShow
? totalCount > 0
? RedPointState.NUMBER
: RedPointState.SHOW
: RedPointState.NONE;
const newCount = totalCount;
if (parent.state !== newState || parent.count !== newCount) {
const oldState = parent.state;
const oldCount = parent.count;
parent.state = newState;
parent.count = newCount;
parent.updateTime = Date.now();
// 触发事件
if (oldState !== newState) {
this.emitEvent({
type: RedPointEventType.STATE_CHANGED,
redPointId: parent.id,
oldValue: oldState,
newValue: newState,
timestamp: Date.now(),
});
}
if (oldCount !== newCount) {
this.emitEvent({
type: RedPointEventType.COUNT_CHANGED,
redPointId: parent.id,
oldValue: oldCount,
newValue: newCount,
timestamp: Date.now(),
});
}
// 递归更新父节点的父节点
this.updateParentRedPoint(parent.id);
}
}
/**
* 添加事件监听器
* @param redPointId 红点ID为空则监听所有红点
* @param listener 监听器
*/
public addListener(
redPointId: string | null,
listener: RedPointListener,
): void {
const key = redPointId || "*";
if (!this._listeners.has(key)) {
this._listeners.set(key, []);
}
this._listeners.get(key).push(listener);
}
/**
* 移除事件监听器
* @param redPointId 红点ID
* @param listener 监听器
*/
public removeListener(
redPointId: string | null,
listener: RedPointListener,
): void {
const key = redPointId || "*";
const listeners = this._listeners.get(key);
if (listeners) {
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
}
}
/**
* 触发事件
* @param event 事件数据
*/
private emitEvent(event: IRedPointEvent): void {
// 触发特定红点监听器
const specificListeners = this._listeners.get(event.redPointId);
if (specificListeners) {
for (const listener of specificListeners) listener(event);
}
// 触发全局监听器
const globalListeners = this._listeners.get("*");
if (globalListeners) {
for (const listener of globalListeners) listener(event);
}
// 通过事件管理器分发
EventManager.getInstance().emit(event.type, event);
}
/**
* 获取所有红点数据
* @param filter 过滤器
* @param sorter 排序器
*/
public getAllRedPoints(
filter?: RedPointFilter,
sorter?: RedPointSorter,
): IRedPointData[] {
let result = Array.from(this._redPoints.values());
if (filter) {
result = result.filter(filter);
}
if (sorter) {
result.sort(sorter);
}
return result;
}
/**
* 获取活跃的红点(有显示状态的)
*/
public getActiveRedPoints(): IRedPointData[] {
return this.getAllRedPoints(
(data) => data.enabled && data.state !== RedPointState.NONE,
);
}
/**
* 保存到本地存储
*/
private saveToStorage(): void {
try {
const data = {
redPoints: Array.from(this._redPoints.entries()),
configs: Array.from(this._configs.entries()),
timestamp: Date.now(),
};
if (typeof localStorage !== "undefined") {
localStorage.setItem(this._storageKey, JSON.stringify(data));
}
} catch (err) {
LogUtils.warn("RedPointManager: 保存数据失败", err);
}
}
/**
* 从本地存储加载
*/
private loadFromStorage(): void {
try {
if (typeof localStorage !== "undefined") {
const dataString = localStorage.getItem(this._storageKey);
if (dataString) {
const data = JSON.parse(dataString);
if (data.redPoints) {
this._redPoints = new Map(data.redPoints);
}
if (data.configs) {
this._configs = new Map(data.configs);
}
}
}
} catch (err) {
LogUtils.warn("RedPointManager: 加载数据失败", err);
}
}
protected onDestroy(): void {
this.saveToStorage();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "16f67373-1f2f-4945-95ad-d6962b1a5215",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "0768a376-ca6e-4114-abf7-91b3579c8040",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,156 @@
import { _decorator, Sprite, SpriteFrame } from "cc";
import LogUtils from "../utils/LogUtils";
import { ResManager } from "./ResManager";
import { EDITOR } from "cc/env";
const { ccclass, property } = _decorator;
const TAG = "LocalSprite";
@ccclass("LocalSprite")
export default class LocalSprite extends Sprite {
@property({
displayName: "Bundle名称",
tooltip: "资源包名称默认为resources",
visible: true,
})
private _bundleName: string = "resources";
@property({
displayName: "资源路径",
tooltip: "SpriteFrame资源路径",
visible: true,
})
private _assetPath: string = "";
private _isLoading: boolean = false;
private _loadingKey: string = "";
/**
* 获取Bundle名称
*/
get bundleName(): string {
return this._bundleName;
}
/**
* 设置Bundle名称
*/
set bundleName(value: string) {
const newValue = value || "resources";
if (this._bundleName !== newValue) {
this._bundleName = newValue;
void this.loadSpriteFrame();
}
}
/**
* 获取资源路径
*/
get assetPath(): string {
return this._assetPath;
}
/**
* 设置资源路径
*/
set assetPath(value: string) {
if (this._assetPath !== value) {
this._assetPath = value;
void this.loadSpriteFrame();
}
}
/**
* 获取是否正在加载
*/
get isLoading(): boolean {
return this._isLoading;
}
public onLoad(): void {
super.onLoad();
if (EDITOR) {
return;
}
void this.loadSpriteFrame();
}
/**
* 设置Bundle和资源路径
*/
public setAsset(assetPath: string, bundleName: string = "resources"): void {
// 先释放旧资源
if (this._bundleName && this._assetPath) {
ResManager.getInstance().releaseAsset(
this._bundleName,
this._assetPath,
);
}
this._bundleName = bundleName || "resources";
this._assetPath = assetPath;
void this.loadSpriteFrame();
}
/**
* 加载SpriteFrame
*/
private async loadSpriteFrame(): Promise<void> {
// 检查路径是否有效
if (!this._assetPath) {
LogUtils.warn(TAG, "资源路径为空");
this.spriteFrame = null;
return;
}
const loadKey = `${this._bundleName}/${this._assetPath}`;
// 防止重复加载
if (this._isLoading && loadKey == this._loadingKey) {
LogUtils.warn(
TAG,
`正在加载中,跳过重复请求: ${this._bundleName}/${this._assetPath}`,
);
return;
}
this._isLoading = true;
this._loadingKey = loadKey;
try {
LogUtils.log(
TAG,
`开始加载SpriteFrame: ${this._bundleName}/${this._assetPath}`,
);
const spriteFrame =
await ResManager.getInstance().loadAsset<SpriteFrame>(
this._assetPath,
SpriteFrame,
this._bundleName,
);
if (spriteFrame && this.isValid && loadKey == this._loadingKey) {
this.spriteFrame = spriteFrame;
LogUtils.log(
TAG,
`加载SpriteFrame成功: ${this._bundleName}/${this._assetPath}`,
);
} else {
LogUtils.log(
TAG,
`加载SpriteFrame失败: ${this._bundleName}/${this._assetPath}`,
);
}
} catch (err) {
LogUtils.error(
TAG,
`加载SpriteFrame失败: ${this._bundleName}/${this._assetPath}`,
err,
);
if (this.isValid) {
this.spriteFrame = null;
}
} finally {
this._isLoading = false;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "21febc68-719c-4eab-90d3-193f9f15abba",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,112 @@
import { _decorator, Sprite } from "cc";
import { EDITOR } from "cc/env";
import { UITransform } from "cc";
import { StringUtils } from "../utils/StringUtils";
import { RemoteSpriteCache } from "./RemoteSpriteCache";
import LogUtils from "../utils/LogUtils";
const { ccclass, property } = _decorator;
@ccclass("RemoteSprite")
export default class RemoteSprite extends Sprite {
/**
* 远程图片URL
*/
@property({
displayName: "远程图片URL",
tooltip: "远程图片的URL地址",
visible: true,
})
private _remoteUrl: string = "";
public get remoteUrl(): string {
return this._remoteUrl;
}
public set remoteUrl(value: string) {
const newValue = value || "";
if (this._remoteUrl !== newValue) {
this._remoteUrl = newValue;
if (this._remoteUrl !== this._currentUrl) {
this.loadRemoteSprite(this._remoteUrl).catch((err) => {
LogUtils.error(
"RemoteSprite",
`加载远程图片失败: ${this._remoteUrl}`,
err,
);
// 可以添加默认图片或错误状态处理
if (this.isValid) {
this.spriteFrame = null;
}
});
}
}
}
// 私有属性
private _currentUrl: string = "";
public onLoad(): void {
super.onLoad();
if (EDITOR) {
return;
}
RemoteSpriteCache.getInstance().checkAndRegisterSprite(
this._currentUrl,
this.spriteFrame,
);
if (
!StringUtils.isEmpty(this._remoteUrl) &&
StringUtils.isEmpty(this._currentUrl)
) {
this.loadRemoteSprite(this._remoteUrl).catch((err) => {
LogUtils.error(
"RemoteSprite",
`onLoad加载远程图片失败: ${this._remoteUrl}`,
err,
);
});
}
}
private async loadRemoteSprite(url: string): Promise<void> {
if (StringUtils.isEmpty(url)) {
return;
}
// URL 相同,且已经加载完成,直接返回
if (this._currentUrl === url && this.spriteFrame) {
return;
}
this.release();
const sp = await RemoteSpriteCache.getInstance().loadSpriteFrame(url);
// 检查是否在加载过程中URL发生了变化
if (this._remoteUrl !== url || this._currentUrl === url) {
RemoteSpriteCache.getInstance().releaseResource(url);
return;
}
this.spriteFrame = sp;
this._currentUrl = url;
}
public setSize(widthOrHeight: number, height?: number): void {
height ??= widthOrHeight;
this.sizeMode = Sprite.SizeMode.CUSTOM;
this.getComponent(UITransform).setContentSize(widthOrHeight, height);
}
public release(): void {
if (!StringUtils.isEmpty(this._currentUrl)) {
RemoteSpriteCache.getInstance()?.releaseResource(this._currentUrl);
}
this._currentUrl = "";
this.spriteFrame = null;
}
public onDestroy(): void {
this.release();
super.onDestroy();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "2dcda316-1f1c-4372-a8df-2405ccf618f3",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,422 @@
import {
_decorator,
SpriteFrame,
ImageAsset,
Texture2D,
assetManager,
} from "cc";
import NodeSingleton from "../NodeSingleton";
import LogUtils from "../utils/LogUtils";
import { StringUtils } from "../utils/StringUtils";
const { ccclass } = _decorator;
enum LoadState {
NONE,
LOADING,
LOADED,
FAILED,
}
// 添加加载超时和重试机制
const LOAD_TIMEOUT = 30000; // 30秒超时
const MAX_RETRY_COUNT = 3;
// 资源缓存项接口
class CacheItem {
spriteFrame: SpriteFrame | null;
loadState: LoadState;
loadPromise: Promise<SpriteFrame> | null;
private _refCount: number;
private _retryCount: number = 0;
private _loadStartTime: number = 0;
get refCount(): number {
return this._refCount;
}
set refCount(value: number) {
this._lastAccessTime = Date.now();
this._refCount = value;
}
private _lastAccessTime: number;
get lastAccessTime(): number {
return this._lastAccessTime;
}
constructor() {
this.loadState = LoadState.NONE;
this.refCount = 0;
}
get retryCount(): number {
return this._retryCount;
}
incrementRetry(): void {
this._retryCount++;
}
resetRetry(): void {
this._retryCount = 0;
}
setLoadStartTime(): void {
this._loadStartTime = Date.now();
}
isLoadTimeout(): boolean {
return Date.now() - this._loadStartTime > LOAD_TIMEOUT;
}
}
const TAG = "RemoteSpriteCache";
const CLEANUP_CHECK_INTERVAL = 10; // 10秒检查一次
const CACHE_EXPIRE_TIME = 1 * 60 * 1000; // 1分钟
// 添加最大缓存数量限制
const MAX_CACHE_SIZE = 100; // 最大缓存数量
@ccclass("RemoteSpriteCache")
export class RemoteSpriteCache extends NodeSingleton {
private readonly _cache = new Map<string, CacheItem>();
private _lastCleanupTime = 0;
private _maxCacheSize = MAX_CACHE_SIZE;
/**
* 设置最大缓存数量
*/
public setMaxCacheSize(size: number): void {
this._maxCacheSize = Math.max(10, size);
LogUtils.info(TAG, `设置最大缓存数量: ${this._maxCacheSize}`);
}
protected init(): void {
this._lastCleanupTime = Date.now();
LogUtils.log(TAG, "RemoteSpriteCache initialized");
}
/**
* 检查并注册精灵
*/
public checkAndRegisterSprite(
url: string,
spriteFrame: SpriteFrame,
): boolean {
if (spriteFrame && !StringUtils.isEmpty(url)) {
const cacheItem = this._cache.get(url);
if (cacheItem && cacheItem.spriteFrame === spriteFrame) {
cacheItem.refCount++;
return true;
}
}
return false;
}
protected update(dt: number): void {
const currentTime = Date.now();
// 每隔指定时间检查一次过期资源
if (
currentTime - this._lastCleanupTime >=
CLEANUP_CHECK_INTERVAL * 1000
) {
this.cleanupUnusedResources();
this._lastCleanupTime = currentTime;
}
}
/**
* 加载SpriteFrame带重试机制
*/
public async loadSpriteFrame(url: string): Promise<SpriteFrame> {
let cacheItem = this._cache.get(url);
if (cacheItem) {
if (
cacheItem.loadState === LoadState.LOADED &&
cacheItem.spriteFrame
) {
cacheItem.refCount++;
return cacheItem.spriteFrame;
} else if (
cacheItem.loadState === LoadState.LOADING &&
cacheItem.loadPromise
) {
try {
const spriteFrame = await cacheItem.loadPromise;
if (spriteFrame) {
cacheItem.refCount++;
}
return spriteFrame;
} catch (err) {
LogUtils.error(TAG, `等待加载失败: ${url}`, err);
// 如果等待失败,重新尝试加载
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
}
}
// 如果加载失败且重试次数未达到上限,重新尝试
if (
cacheItem.loadState === LoadState.FAILED &&
cacheItem.retryCount < MAX_RETRY_COUNT
) {
LogUtils.warn(
TAG,
`重试加载: ${url}, 第${cacheItem.retryCount + 1}`,
);
cacheItem.incrementRetry();
return this.doLoadSpriteFrame(url, cacheItem);
}
if (cacheItem.loadState === LoadState.FAILED) {
throw new Error(`加载失败且已达到最大重试次数: ${url}`);
}
}
cacheItem = new CacheItem();
this._cache.set(url, cacheItem);
return this.doLoadSpriteFrame(url, cacheItem);
}
private async doLoadSpriteFrame(
url: string,
cacheItem: CacheItem,
): Promise<SpriteFrame> {
cacheItem.loadState = LoadState.LOADING;
cacheItem.setLoadStartTime();
cacheItem.loadPromise = new Promise<SpriteFrame>((resolve, reject) => {
const timeoutId = setTimeout(() => {
LogUtils.error(TAG, `加载超时: ${url}`);
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
reject(new Error(`加载超时: ${url}`));
}, LOAD_TIMEOUT);
assetManager.loadRemote(url, (err, asset: ImageAsset) => {
clearTimeout(timeoutId);
if (err) {
LogUtils.error(TAG, `loadRemote error: ${url}`, err);
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
reject(err);
} else {
try {
asset.addRef();
const texture = new Texture2D();
texture.image = asset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
spriteFrame.addRef();
cacheItem.refCount++;
cacheItem.spriteFrame = spriteFrame;
cacheItem.loadState = LoadState.LOADED;
cacheItem.loadPromise = null;
cacheItem.resetRetry();
resolve(spriteFrame);
} catch (createErr) {
LogUtils.error(
TAG,
`创建SpriteFrame失败: ${url}`,
createErr,
);
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
reject(createErr);
}
}
});
});
return await cacheItem.loadPromise;
}
/**
* 释放资源引用
*/
public releaseResource(url: string): void {
if (url) {
const cacheItem = this._cache.get(url);
if (cacheItem) {
cacheItem.refCount--;
}
}
}
/**
* 清理未使用的资源
*/
public cleanupUnusedResources(): void {
const currentCacheSize = this._cache.size;
const targetSize = Math.floor(this._maxCacheSize * 0.8); // 目标大小为阈值的80%
// 如果未达到阈值仅清理引用计数为0且缓存超时的资源
if (currentCacheSize < this._maxCacheSize) {
this.cleanupExpiredResources();
return;
}
// 达到阈值时按使用时间排序清理引用计数为0的资源
this.cleanupByUsageTime(targetSize);
}
/**
* 清理过期的资源引用计数为0且超时
*/
private cleanupExpiredResources(): void {
const urlsToRemove: string[] = [];
let cleanedCount = 0;
const currentTime = Date.now();
for (const [url, cacheItem] of this._cache.entries()) {
if (
cacheItem.spriteFrame &&
cacheItem.loadState === LoadState.LOADED &&
cacheItem.refCount <= 0 &&
cacheItem.lastAccessTime < currentTime - CACHE_EXPIRE_TIME
) {
this.destroyCacheItem(cacheItem);
urlsToRemove.push(url);
cleanedCount++;
}
}
// 从缓存中移除已清理的项
for (const url of urlsToRemove) {
this._cache.delete(url);
}
if (cleanedCount > 0) {
LogUtils.log(TAG, `清理过期资源: ${cleanedCount}`);
}
}
/**
* 按使用时间排序清理资源只清理引用计数为0的资源
*/
private cleanupByUsageTime(targetSize: number): void {
// 直接筛选并排序需要清理的资源
const entriesToClean = Array.from(this._cache.entries())
.filter(
([_, cacheItem]) =>
cacheItem.refCount <= 0 &&
cacheItem.loadState === LoadState.LOADED,
)
.sort((a, b) => a[1].lastAccessTime - b[1].lastAccessTime);
let cleanedCount = 0;
// 清理资源直到达到目标大小
for (const [url, cacheItem] of entriesToClean) {
if (this._cache.size <= targetSize) {
break;
}
this.destroyCacheItem(cacheItem);
this._cache.delete(url);
cleanedCount++;
}
if (cleanedCount > 0) {
const finalSize = this._cache.size;
LogUtils.log(
TAG,
`缓存清理完成: 清理了 ${cleanedCount} 个资源,当前缓存大小: ${finalSize}/${this._maxCacheSize}`,
);
}
}
/**
* 销毁缓存项中的资源
*/
private destroyCacheItem(cacheItem: CacheItem): void {
if (!cacheItem.spriteFrame) {
return;
}
try {
// 先获取texture引用
let texture = cacheItem.spriteFrame.texture as Texture2D;
let imageAsset: ImageAsset = null;
// 如果已加入动态合图必须取原始的Texture2D
if (
cacheItem.spriteFrame.packable &&
cacheItem.spriteFrame.original
) {
texture = cacheItem.spriteFrame.original._texture as Texture2D;
}
// 获取ImageAsset引用
if (texture?.image) {
imageAsset = texture.image;
}
// 先销毁spriteFrame这会自动处理对texture的引用
if (cacheItem.spriteFrame.isValid) {
cacheItem.spriteFrame.destroy();
}
// 再销毁texture
if (texture?.isValid) {
texture.destroy();
}
// 最后减少ImageAsset的引用计数
if (imageAsset?.isValid) {
imageAsset.decRef();
}
} catch (err) {
LogUtils.error(TAG, `销毁缓存项时发生错误`, err);
}
}
/**
* 获取缓存统计信息
*/
public getCacheStats(): {
total: number;
loaded: number;
loading: number;
failed: number;
} {
const stats = { total: 0, loaded: 0, loading: 0, failed: 0 };
for (const [, cacheItem] of this._cache.entries()) {
stats.total++;
switch (cacheItem.loadState) {
case LoadState.LOADED:
stats.loaded++;
break;
case LoadState.LOADING:
stats.loading++;
break;
case LoadState.FAILED:
stats.failed++;
break;
}
}
return stats;
}
/**
* 清空所有缓存
*/
public clearAllCache(): void {
for (const [, cacheItem] of this._cache.entries()) {
if (cacheItem.spriteFrame?.isValid) {
cacheItem.spriteFrame.destroy();
}
}
this._cache.clear();
LogUtils.log(TAG, "All cache cleared");
}
protected onDestroy(): void {
this.clearAllCache();
super.onDestroy();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "72082034-0f4c-43b7-b108-0019caeec10a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,683 @@
import { Asset, assetManager, AssetManager, resources } from "cc";
import LogUtils from "../utils/LogUtils";
import { singleton, Singleton } from "../Singleton";
/**
* 加载状态枚举
*/
export enum LoadState {
NONE = 0,
LOADING = 1,
LOADED = 2,
FAILED = 3,
}
/**
* 加载任务接口
*/
interface LoadTask {
url: string;
type: "bundle" | "asset";
bundleName?: string;
assetPath?: string;
assetType?: typeof Asset;
resolve: (result: any) => void;
reject: (error: Error) => void;
}
/**
* Bundle信息接口
*/
interface BundleInfo {
bundle: AssetManager.Bundle;
state: LoadState;
refCount: number;
isRemote: boolean;
}
/**
* 资源信息接口
*/
interface AssetInfo {
asset: Asset;
state: LoadState;
refCount: number;
bundleName: string;
}
/**
* 资源管理器
*/
@singleton()
export class ResManager extends Singleton {
private static readonly TAG = "ResManager";
private static readonly DEFAULT_BUNDLE = "resources";
// Bundle缓存
private bundleCache = new Map<string, BundleInfo>();
// 资源缓存
private assetCache = new Map<string, AssetInfo>();
// 加载队列
private loadQueue: LoadTask[] = [];
// 当前正在加载的任务数量
private loadingCount = 0;
// 最大并发加载数量
private maxConcurrentLoads = 30;
// 是否正在处理队列
private isProcessingQueue = false;
// Bundle加载等待队列
private bundleWaitQueue = new Map<string, Array<{ resolve: Function; reject: Function }>>();
// 资源加载等待队列
private assetWaitQueue = new Map<string, Array<{ resolve: Function; reject: Function }>>();
protected async onInit() {
this.initDefaultBundle();
}
/**
* 初始化默认Bundle(resources)
*/
private initDefaultBundle(): void {
this.bundleCache.set(ResManager.DEFAULT_BUNDLE, {
bundle: resources,
state: LoadState.LOADED,
refCount: 1,
isRemote: false,
});
LogUtils.info(ResManager.TAG, "初始化默认Bundle: resources");
}
/**
* 设置最大并发加载数量
* @param count 最大并发数
*/
public setMaxConcurrentLoads(count: number): void {
this.maxConcurrentLoads = Math.max(1, count);
LogUtils.info(ResManager.TAG, `设置最大并发加载数量: ${this.maxConcurrentLoads}`);
}
/**
* 加载远端Bundle
* @param bundleName Bundle名称
* @param url Bundle的远端URL
* @returns Promise<Bundle>
*/
public async loadRemoteBundle(bundleName: string, url: string): Promise<AssetManager.Bundle> {
const cachedBundle = this.bundleCache.get(bundleName);
// 检查缓存
if (cachedBundle) {
if (cachedBundle.state === LoadState.LOADED) {
cachedBundle.refCount++;
LogUtils.debug(ResManager.TAG, `使用缓存Bundle: ${bundleName}`);
return cachedBundle.bundle;
} else if (cachedBundle.state === LoadState.LOADING) {
// 正在加载中,等待加载完成
return this.waitForBundleLoad(bundleName);
}
}
// 标记为加载中
this.bundleCache.set(bundleName, {
bundle: null,
state: LoadState.LOADING,
refCount: 1,
isRemote: true,
});
return new Promise<AssetManager.Bundle>((resolve, reject) => {
const task: LoadTask = {
url,
type: "bundle",
bundleName,
resolve,
reject,
};
this.addToQueue(task);
});
}
/**
* 加载本地Bundle
* @param bundleName Bundle名称
* @returns Promise<Bundle>
*/
public async loadLocalBundle(bundleName: string): Promise<AssetManager.Bundle> {
const cachedBundle = this.bundleCache.get(bundleName);
// 检查缓存
if (cachedBundle) {
if (cachedBundle.state === LoadState.LOADED) {
cachedBundle.refCount++;
LogUtils.debug(ResManager.TAG, `使用缓存Bundle: ${bundleName}`);
return cachedBundle.bundle;
} else if (cachedBundle.state === LoadState.LOADING) {
// 正在加载中,等待加载完成
return this.waitForBundleLoad(bundleName);
}
}
// 标记为加载中
this.bundleCache.set(bundleName, {
bundle: null as any,
state: LoadState.LOADING,
refCount: 1,
isRemote: false,
});
return new Promise<AssetManager.Bundle>((resolve, reject) => {
const task: LoadTask = {
url: bundleName, // 本地Bundle使用bundleName作为url
type: "bundle",
bundleName,
resolve,
reject,
};
this.addToQueue(task);
});
}
/**
* 加载资源
* @param assetPath 资源路径
* @param assetType 资源类型
* @param bundleName Bundle名称不传则默认为resources
* @returns Promise<T>
*/
public async loadAsset<T extends Asset>(
assetPath: string,
assetType: typeof Asset,
bundleName?: string,
): Promise<T> {
const targetBundle = bundleName || ResManager.DEFAULT_BUNDLE;
const cacheKey = `${targetBundle}/${assetPath}`;
const cachedAsset = this.assetCache.get(cacheKey);
// 检查缓存
if (cachedAsset) {
if (cachedAsset.state === LoadState.LOADED) {
cachedAsset.refCount++;
LogUtils.debug(ResManager.TAG, `使用缓存资源: ${cacheKey}`);
return cachedAsset.asset as T;
} else if (cachedAsset.state === LoadState.LOADING) {
// 正在加载中,等待加载完成
return this.waitForAssetLoad<T>(cacheKey);
}
}
// 确保Bundle已加载
await this.ensureBundleLoaded(targetBundle);
// 标记为加载中
this.assetCache.set(cacheKey, {
asset: null,
state: LoadState.LOADING,
refCount: 1,
bundleName: targetBundle,
});
return new Promise<T>((resolve, reject) => {
const task: LoadTask = {
url: cacheKey,
type: "asset",
bundleName: targetBundle,
assetPath,
assetType,
resolve,
reject,
};
this.addToQueue(task);
});
}
/**
* 确保Bundle已加载
* @param bundleName Bundle名称
*/
private async ensureBundleLoaded(bundleName: string): Promise<void> {
const bundleInfo = this.bundleCache.get(bundleName);
if (!bundleInfo || bundleInfo.state === LoadState.NONE) {
// Bundle不存在或状态为NONE尝试加载本地Bundle
LogUtils.info(ResManager.TAG, `Bundle ${bundleName} 不存在或未加载尝试加载本地Bundle`);
try {
await this.loadLocalBundle(bundleName);
} catch (err) {
LogUtils.error(
ResManager.TAG,
`Bundle ${bundleName} 本地加载失败请检查Bundle是否存在或调用 loadRemoteBundle 加载远程Bundle`,
err,
);
}
return;
}
if (bundleInfo.state === LoadState.FAILED) {
throw new Error(`Bundle ${bundleName} 加载失败`);
}
if (bundleInfo.state === LoadState.LOADING) {
// Bundle正在加载中等待加载完成
LogUtils.info(ResManager.TAG, `Bundle ${bundleName} 正在加载中,等待加载完成`);
await this.waitForBundleLoad(bundleName);
}
}
/**
* 释放Bundle
* @param bundleName Bundle名称
*/
public releaseBundle(bundleName: string): void {
// 不允许释放默认Bundle
if (bundleName === ResManager.DEFAULT_BUNDLE) {
LogUtils.warn(ResManager.TAG, "不允许释放默认Bundle: resources");
return;
}
const bundleInfo = this.bundleCache.get(bundleName);
if (!bundleInfo) {
LogUtils.warn(ResManager.TAG, `Bundle ${bundleName} 不存在`);
return;
}
bundleInfo.refCount--;
LogUtils.debug(ResManager.TAG, `Bundle ${bundleName} 引用计数: ${bundleInfo.refCount}`);
if (bundleInfo.refCount <= 0) {
// 释放Bundle中的所有资源
this.releaseAssetsInBundle(bundleName);
// 释放Bundle
if (bundleInfo.bundle && bundleInfo.isRemote) {
assetManager.removeBundle(bundleInfo.bundle);
}
this.bundleCache.delete(bundleName);
LogUtils.info(ResManager.TAG, `释放Bundle: ${bundleName}`);
}
}
/**
* 释放资源
* @param assetPath 资源路径
* @param bundleName Bundle名称不传则默认为resources
*/
public releaseAsset(assetPath: string, bundleName?: string): void {
const targetBundle = bundleName || ResManager.DEFAULT_BUNDLE;
const cacheKey = `${targetBundle}/${assetPath}`;
const assetInfo = this.assetCache.get(cacheKey);
if (!assetInfo) {
LogUtils.warn(ResManager.TAG, `资源 ${cacheKey} 不存在`);
return;
}
assetInfo.refCount--;
LogUtils.debug(ResManager.TAG, `资源 ${cacheKey} 引用计数: ${assetInfo.refCount}`);
if (assetInfo.refCount <= 0) {
if (assetInfo.asset) {
assetInfo.asset.decRef();
}
this.assetCache.delete(cacheKey);
LogUtils.info(ResManager.TAG, `释放资源: ${cacheKey}`);
}
}
/**
* 清理所有缓存保留默认Bundle
*/
public clearAll(): void {
// 释放所有资源
for (const [, assetInfo] of this.assetCache) {
if (assetInfo.asset) {
assetInfo.asset.decRef();
}
}
this.assetCache.clear();
// 释放所有远端Bundle
for (const [key, bundleInfo] of this.bundleCache) {
if (key !== ResManager.DEFAULT_BUNDLE && bundleInfo.bundle && bundleInfo.isRemote) {
assetManager.removeBundle(bundleInfo.bundle);
}
}
// 清空Bundle缓存但保留默认Bundle
const defaultBundle = this.bundleCache.get(ResManager.DEFAULT_BUNDLE);
this.bundleCache.clear();
if (defaultBundle) {
this.bundleCache.set(ResManager.DEFAULT_BUNDLE, defaultBundle);
}
// 清空加载队列
this.loadQueue = [];
this.loadingCount = 0;
this.bundleWaitQueue.clear();
this.assetWaitQueue.clear();
LogUtils.info(ResManager.TAG, "清理所有缓存完成");
}
/**
* 获取缓存状态信息
*/
public getCacheInfo(): {
bundleCount: number;
assetCount: number;
queueLength: number;
loadingCount: number;
} {
return {
bundleCount: this.bundleCache.size,
assetCount: this.assetCache.size,
queueLength: this.loadQueue.length,
loadingCount: this.loadingCount,
};
}
/**
* 添加任务到队列
* @param task 加载任务
*/
private addToQueue(task: LoadTask): void {
this.loadQueue.push(task);
void this.processQueue();
}
/**
* 处理加载队列
*/
private async processQueue(): Promise<void> {
if (this.isProcessingQueue || this.loadingCount >= this.maxConcurrentLoads) {
return;
}
this.isProcessingQueue = true;
while (this.loadQueue.length > 0 && this.loadingCount < this.maxConcurrentLoads) {
const task = this.loadQueue.shift();
if (!task) break;
this.loadingCount++;
this.executeTask(task)
.then(() => {
this.loadingCount--;
void this.processQueue();
})
.catch((err) => {
LogUtils.error(ResManager.TAG, `队列任务执行失败: ${task.url}`, err);
this.loadingCount--;
void this.processQueue();
});
}
this.isProcessingQueue = false;
}
/**
* 执行加载任务
* @param task 加载任务
*/
private async executeTask(task: LoadTask): Promise<void> {
try {
await (task.type === "bundle" ? this.loadBundleTask(task) : this.loadAssetTask(task));
} catch (err) {
LogUtils.error(ResManager.TAG, `加载任务失败: ${task.url}`, err);
task.reject(err as Error);
}
}
/**
* 执行Bundle加载任务
* @param task 加载任务
*/
private async loadBundleTask(task: LoadTask): Promise<void> {
const { url, bundleName, resolve, reject } = task;
if (!bundleName) {
reject(new Error("Bundle名称不能为空"));
return;
}
const bundleInfo = this.bundleCache.get(bundleName);
const isRemote = bundleInfo?.isRemote ?? false;
// 根据是否为远端Bundle选择不同的加载方式
if (isRemote) {
// 远端Bundle加载
assetManager.loadBundle(url, (err, bundle) => {
this.handleBundleLoadResult(bundleName, err, bundle, resolve, reject);
});
} else {
// 本地Bundle加载
assetManager.loadBundle(bundleName, (err, bundle) => {
this.handleBundleLoadResult(bundleName, err, bundle, resolve, reject);
});
}
}
/**
* 处理Bundle加载结果
* @param bundleName Bundle名称
* @param err 错误信息
* @param bundle Bundle对象
* @param resolve Promise resolve函数
* @param reject Promise reject函数
*/
private handleBundleLoadResult(
bundleName: string,
err: Error | null,
bundle: AssetManager.Bundle,
resolve: (result: AssetManager.Bundle) => void,
reject: (error: Error) => void,
): void {
const bundleInfo = this.bundleCache.get(bundleName);
if (err) {
LogUtils.error(ResManager.TAG, `Bundle加载失败: ${bundleName}`, err);
if (bundleInfo) {
bundleInfo.state = LoadState.FAILED;
}
// 通知所有等待的任务
this.notifyBundleWaiters(bundleName, false, new Error(`Bundle加载失败: ${err.message}`));
reject(new Error(`Bundle加载失败: ${err.message}`));
return;
}
if (bundleInfo) {
bundleInfo.bundle = bundle;
bundleInfo.state = LoadState.LOADED;
}
LogUtils.info(ResManager.TAG, `Bundle加载成功: ${bundleName}`);
// 通知所有等待的任务
this.notifyBundleWaiters(bundleName, true, bundle);
resolve(bundle);
}
/**
* 执行资源加载任务
* @param task 加载任务
*/
private async loadAssetTask(task: LoadTask): Promise<void> {
const { url, bundleName, assetPath, assetType, resolve, reject } = task;
if (!bundleName || !assetPath || !assetType) {
reject(new Error("资源加载参数不完整"));
return;
}
const bundleInfo = this.bundleCache.get(bundleName);
if (!bundleInfo?.bundle) {
reject(new Error(`Bundle ${bundleName} 不存在`));
return;
}
bundleInfo.bundle.load(assetPath, assetType, (err, asset) => {
const assetInfo = this.assetCache.get(url);
if (err) {
LogUtils.error(ResManager.TAG, `资源加载失败: ${url}`, err);
if (assetInfo) {
assetInfo.state = LoadState.FAILED;
}
// 通知所有等待的任务
this.notifyAssetWaiters(url, false, new Error(`资源加载失败: ${err.message}`));
reject(new Error(`资源加载失败: ${err.message}`));
return;
}
if (assetInfo) {
assetInfo.asset = asset;
assetInfo.state = LoadState.LOADED;
}
LogUtils.info(ResManager.TAG, `资源加载成功: ${url}`);
// 通知所有等待的任务
this.notifyAssetWaiters(url, true, asset);
resolve(asset);
});
}
/**
* 等待Bundle加载完成
* @param bundleName Bundle名称
*/
private async waitForBundleLoad(bundleName: string): Promise<AssetManager.Bundle> {
return new Promise((resolve, reject) => {
const bundleInfo = this.bundleCache.get(bundleName);
if (bundleInfo?.state === LoadState.LOADED) {
bundleInfo.refCount++;
resolve(bundleInfo.bundle);
return;
}
if (bundleInfo?.state === LoadState.FAILED) {
reject(new Error(`Bundle ${bundleName} 加载失败`));
return;
}
// 添加到等待队列
if (!this.bundleWaitQueue.has(bundleName)) {
this.bundleWaitQueue.set(bundleName, []);
}
this.bundleWaitQueue.get(bundleName).push({ resolve, reject });
});
}
/**
* 等待资源加载完成
* @param cacheKey 缓存键
*/
private async waitForAssetLoad<T extends Asset>(cacheKey: string): Promise<T> {
return new Promise((resolve, reject) => {
const assetInfo = this.assetCache.get(cacheKey);
if (assetInfo?.state === LoadState.LOADED) {
assetInfo.refCount++;
resolve(assetInfo.asset as T);
return;
}
if (assetInfo?.state === LoadState.FAILED) {
reject(new Error(`资源 ${cacheKey} 加载失败`));
return;
}
// 添加到等待队列
if (!this.assetWaitQueue.has(cacheKey)) {
this.assetWaitQueue.set(cacheKey, []);
}
this.assetWaitQueue.get(cacheKey).push({ resolve, reject });
});
}
/**
* 通知Bundle等待者
* @param bundleName Bundle名称
* @param success 是否成功
* @param result 结果或错误
*/
private notifyBundleWaiters(bundleName: string, success: boolean, result: any): void {
const waiters = this.bundleWaitQueue.get(bundleName);
if (!waiters) return;
for (const waiter of waiters) {
if (success) {
const bundleInfo = this.bundleCache.get(bundleName);
if (bundleInfo) {
bundleInfo.refCount++;
}
waiter.resolve(result);
} else {
waiter.reject(result);
}
}
this.bundleWaitQueue.delete(bundleName);
}
/**
* 通知资源等待者
* @param cacheKey 缓存键
* @param success 是否成功
* @param result 结果或错误
*/
private notifyAssetWaiters(cacheKey: string, success: boolean, result: any): void {
const waiters = this.assetWaitQueue.get(cacheKey);
if (!waiters) return;
for (const waiter of waiters) {
if (success) {
const assetInfo = this.assetCache.get(cacheKey);
if (assetInfo) {
assetInfo.refCount++;
}
waiter.resolve(result);
} else {
waiter.reject(result);
}
}
this.assetWaitQueue.delete(cacheKey);
}
/**
* 释放Bundle中的所有资源
* @param bundleName Bundle名称
*/
private releaseAssetsInBundle(bundleName: string): void {
const assetsToRemove: string[] = [];
for (const [key, assetInfo] of this.assetCache) {
if (assetInfo.bundleName === bundleName) {
if (assetInfo.asset) {
assetInfo.asset.decRef();
}
assetsToRemove.push(key);
}
}
for (const key of assetsToRemove) {
this.assetCache.delete(key);
}
LogUtils.debug(ResManager.TAG, `释放Bundle ${bundleName} 中的 ${assetsToRemove.length} 个资源`);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "16315e98-964a-4632-beb3-44842a65a045",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e05b4383-3170-4dd4-af7c-cc04bad7b4c2",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,148 @@
import AccountManager from "../account/AccountManager";
import { IAccountInfo } from "../account/Types";
import { NetworkManager } from "../net/NetworkManager";
import { Singleton } from "../Singleton";
import LogUtils from "../utils/LogUtils";
import { StringUtils } from "../utils/StringUtils";
const ACCOUNT_SAVE_KEY: string = "account_save_key";
const ACCOUNT_DATA_SAVE_KEY: string = "account_data_save_key";
export const GUIDE_HISTORY_KEY: string = "guide_history_key";
const VERSION_KEY: string = "version";
export default class StorageManager extends Singleton {
private storageData: Record<string, any> = {};
private needSaveRemote: boolean = false;
private isSaving: boolean = false;
private ignoreSaveKeys: Set<string> = new Set([GUIDE_HISTORY_KEY]);
private saveInterval: number | NodeJS.Timeout = 0;
private saveKeys: Set<string> = new Set();
protected async onInit(): Promise<void> {
this.saveInterval = setInterval(this.saveRemoteData.bind(this), 30000);
}
public get account(): IAccountInfo {
const accountInfo = localStorage.getItem(ACCOUNT_SAVE_KEY);
if (accountInfo) {
return JSON.parse(accountInfo) as IAccountInfo;
}
return null;
}
public set account(value: IAccountInfo) {
if (!value) {
return;
}
localStorage.setItem(ACCOUNT_SAVE_KEY, JSON.stringify(value));
}
public setData(key: string, value: any) {
// 如果是基础类型, 需要优先判断值是否有修改
if (
typeof value === "number" ||
typeof value === "string" ||
typeof value === "boolean"
) {
if (this.storageData[key] === value) {
return;
}
}
this.storageData[key] = value;
if (this.ignoreSaveKeys.has(key)) {
this.saveData();
return;
}
this.saveKeys.add(key);
this.storageData[VERSION_KEY] += 1;
this.saveData();
this.needSaveRemote = true;
}
public getData<T = any>(key: string) {
return this.storageData[key] as T;
}
public loadData(uid: string, remoteData: Record<string, any>) {
const localDataValue = localStorage.getItem(
`${ACCOUNT_DATA_SAVE_KEY}_${uid}`,
);
let localData: Record<string, any> = {};
if (!StringUtils.isEmpty(localDataValue)) {
localData = JSON.parse(localDataValue);
}
const compareResult = this.compareData(localData, remoteData);
if (compareResult > 0) {
this.storageData = localData;
this.needSaveRemote = true;
} else if (compareResult < 0) {
this.storageData = remoteData;
}
}
private saveData() {
const uid = AccountManager.getInstance().accountInfo.uid;
if (uid) {
localStorage.setItem(
`${ACCOUNT_DATA_SAVE_KEY}_${uid}`,
JSON.stringify(this.storageData),
);
LogUtils.info("StorageManager", "saveData", uid);
}
}
private async saveRemoteData() {
if (!this.needSaveRemote || this.isSaving || this.saveKeys.size <= 0) {
return;
}
this.isSaving = true;
const httpClient = NetworkManager.getInstance().getHttpClient();
const uploadData: Record<string, any> = {};
for (const key of this.saveKeys) {
uploadData[key] = this.storageData[key];
}
uploadData[VERSION_KEY] = this.storageData[VERSION_KEY];
const rsp = await httpClient.send("/user/update", "POST", uploadData);
if (rsp.success) {
this.saveKeys.clear();
}
this.isSaving = false;
}
private compareData(
localData: Record<string, any>,
remoteData: Record<string, any>,
): number {
if (
localData[VERSION_KEY] === undefined ||
isNaN(localData[VERSION_KEY])
) {
localData[VERSION_KEY] = 1;
}
if (
remoteData[VERSION_KEY] === undefined ||
isNaN(remoteData[VERSION_KEY])
) {
remoteData[VERSION_KEY] = 1;
}
if (localData[VERSION_KEY] > remoteData[VERSION_KEY]) {
return 1;
} else if (localData[VERSION_KEY] < remoteData[VERSION_KEY]) {
return -1;
} else {
return 0;
}
}
protected onRelease(): void {
if (this.saveInterval) {
clearInterval(this.saveInterval);
this.saveInterval = 0;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "3ab1f6fe-c52e-48ac-9fc0-7cfbeb17a654",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,21 @@
/**
* 存储配置接口
*/
export interface StorageConfig {
enableRemoteSync?: boolean;
remoteBaseUrl?: string;
/** 同步时间间隔(毫秒)默认1分钟 */
syncInterval?: number;
remoteIgnoreList?: string[];
}
/**
* 存储项接口
*/
export interface StorageItem<T = any> {
value: T;
timestamp: number;
version: number;
needSync?: boolean;
lastSyncTime?: number;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "30d7b260-c453-42fa-a47a-9c2295081772",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "0efdbac2-799b-4086-a224-398e3481d588",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,241 @@
/**
* 任务类型枚举
*/
export enum GlobalTaskType {
/** 每日任务 */
DAILY = "daily",
/** 每周任务 */
WEEKLY = "weekly",
/** 每月任务 */
MONTHLY = "monthly",
/** 赛季任务 */
SEASONAL = "seasonal",
/** 活动任务 */
EVENT = "event",
/** 成就任务 */
ACHIEVEMENT = "achievement",
/** 主线任务 */
MAIN = "main",
/** 支线任务 */
SIDE = "side",
}
export type TaskType = GlobalTaskType | string;
/**
* 任务状态枚举
*/
export enum TaskStatus {
/** 未激活 */
INACTIVE = "inactive",
/** 进行中 */
ACTIVE = "active",
/** 已完成 */
COMPLETED = "completed",
/** 已领取奖励 */
CLAIMED = "claimed",
/** 已过期 */
EXPIRED = "expired",
/** 已锁定 */
LOCKED = "locked",
}
/**
* 任务目标类型
*/
export enum TaskTargetType {
/** 数量目标 */
COUNT = "count",
/** 收集目标 */
COLLECT = "collect",
/** 击败目标 */
DEFEAT = "defeat",
/** 达到等级 */
REACH_LEVEL = "reach_level",
/** 消费目标 */
SPEND = "spend",
/** 获得目标 */
GAIN = "gain",
/** 使用目标 */
USE = "use",
/** 完成目标 */
COMPLETE = "complete",
}
/**
* 任务重置周期
*/
export enum TaskResetCycle {
/** 不重置 */
NEVER = "never",
/** 每日重置 */
DAILY = "daily",
/** 每周重置 */
WEEKLY = "weekly",
/** 每月重置 */
MONTHLY = "monthly",
/** 赛季重置 */
SEASONAL = "seasonal",
}
/**
* 奖励类型
*/
export enum RewardType {
/** 金币 */
GOLD = "gold",
/** 经验 */
EXP = "exp",
/** 道具 */
ITEM = "item",
/** 装备 */
EQUIPMENT = "equipment",
/** 货币 */
CURRENCY = "currency",
/** 称号 */
TITLE = "title",
}
/**
* 任务奖励配置
*/
export interface ITaskReward {
/** 奖励类型 */
type: RewardType;
/** 奖励ID */
id: string;
/** 奖励数量 */
amount: number;
/** 奖励名称 */
name?: string;
/** 奖励描述 */
description?: string;
/** 奖励图标 */
icon?: string;
}
/**
* 任务目标配置
*/
export interface ITaskTarget {
/** 目标ID */
id: string;
/** 目标类型 */
type: TaskTargetType;
/** 目标描述 */
description: string;
/** 目标数量 */
targetCount: number;
/** 当前进度 */
currentCount: number;
/** 目标参数 */
params?: Record<string, unknown>;
}
/**
* 任务条件配置
*/
export interface ITaskCondition {
/** 条件类型 */
type: string;
/** 条件参数 */
params?: Record<string, unknown>;
/** 条件表达式 */
expression?: string;
}
/**
* 任务配置接口
*/
export interface ITaskConfig {
/** 任务ID */
id: string;
/** 任务名称 */
name: string;
/** 任务描述 */
description: string;
/** 任务类型 */
type: TaskType;
/** 任务优先级 */
priority: number;
/** 任务图标 */
icon?: string;
/** 任务目标列表 */
targets: ITaskTarget[];
/** 任务奖励列表 */
rewards: ITaskReward[];
/** 解锁条件 */
unlockConditions?: ITaskCondition[];
/** 完成条件 */
completeConditions?: ITaskCondition[];
/** 重置周期 */
resetCycle: TaskResetCycle;
/** 开始时间 */
startTime?: number;
/** 结束时间 */
endTime?: number;
/** 是否启用 */
enabled: boolean;
/** 是否可重复 */
repeatable: boolean;
/** 前置任务ID列表 */
prerequisites?: string[];
/** 自定义数据 */
customData?: Record<string, unknown>;
}
/**
* 任务运行时数据
*/
export interface ITaskRuntimeData {
/** 任务配置 */
config: ITaskConfig;
/** 任务状态 */
status: TaskStatus;
/** 任务目标进度 */
targets: ITaskTarget[];
/** 激活时间 */
activatedTime?: number;
/** 完成时间 */
completedTime?: number;
/** 领取时间 */
claimedTime?: number;
/** 过期时间 */
expiredTime?: number;
/** 重置时间 */
resetTime?: number;
/** 自定义运行时数据 */
runtimeData?: Record<string, unknown>;
}
/**
* 任务事件类型
*/
export enum TaskEventType {
/** 任务激活 */
TASK_ACTIVATED = "task_activated",
/** 任务进度更新 */
TASK_PROGRESS_UPDATED = "task_progress_updated",
/** 任务完成 */
TASK_COMPLETED = "task_completed",
/** 任务奖励领取 */
TASK_REWARD_CLAIMED = "task_reward_claimed",
/** 任务过期 */
TASK_EXPIRED = "task_expired",
/** 任务重置 */
TASK_RESET = "task_reset",
}
/**
* 任务事件数据
*/
export interface ITaskEventData {
/** 事件类型 */
type: TaskEventType;
/** 任务ID */
taskId: string;
/** 事件时间 */
timestamp: number;
/** 事件数据 */
data?: Record<string, unknown>;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "208a38dd-f8fb-4792-b872-751c0f081679",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,553 @@
import LogUtils from "../utils/LogUtils";
import { singleton, Singleton } from "../Singleton";
import {
ITaskConfig,
ITaskRuntimeData,
ITaskReward,
TaskStatus,
TaskType,
TaskEventType,
} from "./TaskData";
import { TaskTypeManager } from "./TaskTypeManager";
import { EventManager } from "../event/EventManager";
import TimeManager from "../timer/TimerManager";
import { ResManager } from "../res/ResManager";
import { JsonAsset } from "cc";
const TAG = "TaskManager";
/**
* 任务管理器
*/
@singleton()
export class TaskManager extends Singleton {
/** 任务配置映射 */
private taskConfigs: Map<string, ITaskConfig> = new Map();
/** 任务运行时数据映射 */
private taskRuntimeData: Map<string, ITaskRuntimeData> = new Map();
/** 任务类型管理器 */
private typeManager: TaskTypeManager;
/** 是否已初始化 */
private initialized = false;
/**
* 初始化方法(由 Singleton 基类调用)
*/
protected async onInit() {
LogUtils.info(TAG, "TaskManager 单例初始化");
if (this.initialized) {
LogUtils.warn(TAG, "任务管理器已经初始化");
return;
}
try {
LogUtils.info(TAG, "开始初始化任务管理器");
this.typeManager = TaskTypeManager.getInstance();
// 加载任务配置
await this.loadTaskConfigs();
// 加载任务运行时数据
await this.loadTaskRuntimeData();
// 检查任务状态
this.checkTaskStatus();
this.initialized = true;
LogUtils.info(TAG, "任务管理器初始化完成");
} catch (err) {
LogUtils.error(TAG, "任务管理器初始化失败:", err);
throw err;
}
}
/**
* 加载任务配置
*/
private async loadTaskConfigs(): Promise<void> {
// TODO: 从配置文件或服务器加载任务配置
LogUtils.info(TAG, "加载任务配置");
const jsonAsset = await ResManager.getInstance().loadAsset<JsonAsset>(
"task-configs",
JsonAsset,
);
const taskConfigs = jsonAsset.json as Record<string, ITaskConfig[]>;
for (const taskType of Object.keys(taskConfigs)) {
const configs = taskConfigs[taskType];
for (const config of configs) {
this.addTaskConfig(config);
}
}
}
/**
* 加载任务运行时数据
*/
private async loadTaskRuntimeData(): Promise<void> {
// TODO: 从本地存储加载任务运行时数据
LogUtils.info(TAG, "加载任务运行时数据");
}
/**
* 检查任务状态
*/
private checkTaskStatus(): void {
for (const [taskId, runtimeData] of this.taskRuntimeData) {
const config = runtimeData.config;
// 检查任务是否过期
if (
runtimeData.status === TaskStatus.ACTIVE &&
runtimeData.activatedTime &&
this.typeManager.isTaskExpired(
config,
runtimeData.activatedTime,
)
) {
this.expireTask(taskId);
continue;
}
// 检查任务是否需要重置
if (
runtimeData.resetTime &&
this.typeManager.shouldResetTask(config, runtimeData.resetTime)
) {
this.resetTask(taskId);
continue;
}
// 检查任务是否可以激活
if (
runtimeData.status === TaskStatus.INACTIVE &&
this.canActivateTask(config)
) {
this.activateTask(taskId);
}
}
}
/**
* 添加任务配置
*/
public addTaskConfig(config: ITaskConfig): boolean {
try {
// 验证任务配置
if (!this.typeManager.validateTaskConfig(config)) {
LogUtils.warn(TAG, `任务配置验证失败: ${config.id}`);
return false;
}
this.taskConfigs.set(config.id, config);
// 创建运行时数据
const runtimeData: ITaskRuntimeData = {
config: config,
status: TaskStatus.INACTIVE,
targets: config.targets.map((target) => ({ ...target })),
resetTime: Date.now(),
};
this.taskRuntimeData.set(config.id, runtimeData);
LogUtils.info(TAG, `添加任务配置: ${config.id}`);
return true;
} catch (err) {
LogUtils.error(TAG, `添加任务配置失败: ${config.id}`, err);
return false;
}
}
/**
* 移除任务配置
*/
public removeTaskConfig(taskId: string): boolean {
try {
this.taskConfigs.delete(taskId);
this.taskRuntimeData.delete(taskId);
LogUtils.info(TAG, `移除任务配置: ${taskId}`);
return true;
} catch (err) {
LogUtils.error(TAG, `移除任务配置失败: ${taskId}`, err);
return false;
}
}
/**
* 获取任务配置
*/
public getTaskConfig(taskId: string): ITaskConfig | undefined {
return this.taskConfigs.get(taskId);
}
/**
* 获取任务运行时数据
*/
public getTaskRuntimeData(taskId: string): ITaskRuntimeData | undefined {
return this.taskRuntimeData.get(taskId);
}
/**
* 获取指定类型的任务列表
*/
public getTasksByType(type: TaskType): ITaskRuntimeData[] {
const tasks: ITaskRuntimeData[] = [];
for (const runtimeData of this.taskRuntimeData.values()) {
if (runtimeData.config.type === type) {
tasks.push(runtimeData);
}
}
return tasks.sort((a, b) => a.config.priority - b.config.priority);
}
/**
* 获取指定状态的任务列表
*/
public getTasksByStatus(status: TaskStatus): ITaskRuntimeData[] {
const tasks: ITaskRuntimeData[] = [];
for (const runtimeData of this.taskRuntimeData.values()) {
if (runtimeData.status === status) {
tasks.push(runtimeData);
}
}
return tasks.sort((a, b) => a.config.priority - b.config.priority);
}
/**
* 激活任务
*/
public activateTask(taskId: string): boolean {
try {
const runtimeData = this.taskRuntimeData.get(taskId);
if (!runtimeData) {
LogUtils.warn(TAG, `任务不存在: ${taskId}`);
return false;
}
if (runtimeData.status !== TaskStatus.INACTIVE) {
LogUtils.warn(
TAG,
`任务状态不正确,无法激活: ${taskId}, 当前状态: ${runtimeData.status}`,
);
return false;
}
if (!this.canActivateTask(runtimeData.config)) {
LogUtils.warn(TAG, `任务激活条件不满足: ${taskId}`);
return false;
}
runtimeData.status = TaskStatus.ACTIVE;
runtimeData.activatedTime = Date.now();
EventManager.getInstance().emit(
TaskEventType.TASK_ACTIVATED,
taskId,
TimeManager.getInstance().getCorrectedTime(),
);
LogUtils.info(TAG, `任务已激活: ${taskId}`);
return true;
} catch (err) {
LogUtils.error(TAG, `激活任务失败: ${taskId}`, err);
return false;
}
}
/**
* 更新任务进度
*/
public updateTaskProgress(
taskId: string,
targetId: string,
progress: number,
): boolean {
try {
const runtimeData = this.taskRuntimeData.get(taskId);
if (!runtimeData) {
LogUtils.warn(TAG, `任务不存在: ${taskId}`);
return false;
}
if (runtimeData.status !== TaskStatus.ACTIVE) {
LogUtils.debug(
TAG,
`任务状态不正确,无法更新进度: ${taskId}, 当前状态: ${runtimeData.status}`,
);
return false;
}
const target = runtimeData.targets.find((t) => t.id === targetId);
if (!target) {
LogUtils.warn(TAG, `任务目标不存在: ${taskId}, ${targetId}`);
return false;
}
const oldProgress = target.currentCount;
target.currentCount = Math.min(
target.currentCount + progress,
target.targetCount,
);
EventManager.getInstance().emit(
TaskEventType.TASK_PROGRESS_UPDATED,
taskId,
TimeManager.getInstance().getCorrectedTime(),
{
targetId: targetId,
oldProgress: oldProgress,
newProgress: target.currentCount,
targetCount: target.targetCount,
},
);
// 检查任务是否完成
if (this.isTaskCompleted(runtimeData)) {
this.completeTask(taskId);
}
LogUtils.debug(
TAG,
`任务进度已更新: ${taskId}, ${targetId}, ${target.currentCount}/${target.targetCount}`,
);
return true;
} catch (err) {
LogUtils.error(TAG, `更新任务进度失败: ${taskId}`, err);
return false;
}
}
/**
* 完成任务
*/
public completeTask(taskId: string): boolean {
try {
const runtimeData = this.taskRuntimeData.get(taskId);
if (!runtimeData) {
LogUtils.warn(TAG, `任务不存在: ${taskId}`);
return false;
}
if (runtimeData.status !== TaskStatus.ACTIVE) {
LogUtils.warn(
TAG,
`任务状态不正确,无法完成: ${taskId}, 当前状态: ${runtimeData.status}`,
);
return false;
}
if (!this.isTaskCompleted(runtimeData)) {
LogUtils.warn(TAG, `任务完成条件不满足: ${taskId}`);
return false;
}
runtimeData.status = TaskStatus.COMPLETED;
runtimeData.completedTime = Date.now();
EventManager.getInstance().emit(
TaskEventType.TASK_COMPLETED,
taskId,
TimeManager.getInstance().getCorrectedTime(),
);
LogUtils.info(TAG, `任务已完成: ${taskId}`);
return true;
} catch (err) {
LogUtils.error(TAG, `完成任务失败: ${taskId}`, err);
return false;
}
}
/**
* 领取任务奖励
*/
public claimTaskReward(taskId: string): ITaskReward[] | null {
try {
const runtimeData = this.taskRuntimeData.get(taskId);
if (!runtimeData) {
LogUtils.warn(TAG, `任务不存在: ${taskId}`);
return null;
}
if (runtimeData.status !== TaskStatus.COMPLETED) {
LogUtils.warn(
TAG,
`任务状态不正确,无法领取奖励: ${taskId}, 当前状态: ${runtimeData.status}`,
);
return null;
}
runtimeData.status = TaskStatus.CLAIMED;
runtimeData.claimedTime = Date.now();
const rewards = runtimeData.config.rewards;
EventManager.getInstance().emit(
TaskEventType.TASK_REWARD_CLAIMED,
taskId,
TimeManager.getInstance().getCorrectedTime(),
{
rewards: rewards,
},
);
LogUtils.info(
TAG,
`任务奖励已领取: ${taskId}, 奖励数量: ${rewards.length}`,
);
return rewards;
} catch (err) {
LogUtils.error(TAG, `领取任务奖励失败: ${taskId}`, err);
return null;
}
}
/**
* 重置任务
*/
public resetTask(taskId: string): boolean {
try {
const runtimeData = this.taskRuntimeData.get(taskId);
if (!runtimeData) {
LogUtils.warn(TAG, `任务不存在: ${taskId}`);
return false;
}
// 重置任务状态和进度
runtimeData.status = TaskStatus.INACTIVE;
runtimeData.activatedTime = undefined;
runtimeData.completedTime = undefined;
runtimeData.claimedTime = undefined;
runtimeData.resetTime = Date.now();
// 重置目标进度
for (const target of runtimeData.targets) {
target.currentCount = 0;
}
EventManager.getInstance().emit(
TaskEventType.TASK_RESET,
taskId,
TimeManager.getInstance().getCorrectedTime(),
);
LogUtils.info(TAG, `任务已重置: ${taskId}`);
return true;
} catch (err) {
LogUtils.error(TAG, `重置任务失败: ${taskId}`, err);
return false;
}
}
/**
* 使任务过期
*/
public expireTask(taskId: string): boolean {
try {
const runtimeData = this.taskRuntimeData.get(taskId);
if (!runtimeData) {
LogUtils.warn(TAG, `任务不存在: ${taskId}`);
return false;
}
runtimeData.status = TaskStatus.EXPIRED;
runtimeData.expiredTime = Date.now();
EventManager.getInstance().emit(
TaskEventType.TASK_EXPIRED,
taskId,
TimeManager.getInstance().getCorrectedTime(),
);
LogUtils.info(TAG, `任务已过期: ${taskId}`);
return true;
} catch (err) {
LogUtils.error(TAG, `任务过期失败: ${taskId}`, err);
return false;
}
}
/**
* 检查任务是否可以激活
*/
private canActivateTask(config: ITaskConfig): boolean {
// 检查任务是否启用
if (!config.enabled) {
return false;
}
// 检查时间范围
const now = Date.now();
if (config.startTime && now < config.startTime) {
return false;
}
if (config.endTime && now > config.endTime) {
return false;
}
// 检查前置任务
if (config.prerequisites) {
for (const prerequisiteId of config.prerequisites) {
const prerequisiteData =
this.taskRuntimeData.get(prerequisiteId);
if (
!prerequisiteData ||
prerequisiteData.status !== TaskStatus.CLAIMED
) {
return false;
}
}
}
// TODO: 检查解锁条件
return true;
}
/**
* 检查任务是否完成
*/
private isTaskCompleted(runtimeData: ITaskRuntimeData): boolean {
// 检查所有目标是否完成
for (const target of runtimeData.targets) {
if (target.currentCount < target.targetCount) {
return false;
}
}
// TODO: 检查完成条件
return true;
}
/**
* 保存任务数据
*/
public async saveTaskData(): Promise<void> {
try {
// TODO: 保存任务运行时数据到本地存储
LogUtils.info(TAG, "保存任务数据");
} catch (err) {
LogUtils.error(TAG, "保存任务数据失败:", err);
throw err;
}
}
/**
* 销毁任务管理器
*/
public destroy(): void {
this.taskConfigs.clear();
this.taskRuntimeData.clear();
this.initialized = false;
LogUtils.info(TAG, "任务管理器已销毁");
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "02df2ceb-9103-4a43-9c77-3e8c88c6a1d6",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,290 @@
import { Singleton } from "../Singleton";
import LogUtils from "../utils/LogUtils";
import { TaskTypeRegistry } from "./decorators/TaskTypeDecorator";
import {
TaskType,
TaskResetCycle,
ITaskConfig,
GlobalTaskType,
} from "./TaskData";
const TAG = "TaskTypeManager";
/**
* 任务类型处理器接口
*/
export interface ITaskTypeHandler {
/** 验证任务配置 */
validateConfig(config: ITaskConfig): boolean;
/** 检查任务是否应该重置 */
shouldReset(config: ITaskConfig, lastResetTime: number): boolean;
/** 检查任务是否过期 */
isExpired(config: ITaskConfig, activatedTime: number): boolean;
/** 获取下次重置时间 */
getNextResetTime(config: ITaskConfig): number;
/** 获取任务过期时间 */
getExpireTime(config: ITaskConfig, activatedTime: number): number;
}
/**
* 每周任务处理器
*/
export class WeeklyTaskHandler implements ITaskTypeHandler {
getType(): string {
return GlobalTaskType.WEEKLY;
}
public validateConfig(config: ITaskConfig): boolean {
if (config.resetCycle !== TaskResetCycle.WEEKLY) {
LogUtils.warn(TAG, `每周任务重置周期应为 WEEKLY: ${config.id}`);
return false;
}
return true;
}
public shouldReset(config: ITaskConfig, lastResetTime: number): boolean {
const now = Date.now();
const lastResetDate = new Date(lastResetTime);
const currentDate = new Date(now);
// 获取周一的日期
const getMonday = (date: Date): Date => {
const day = date.getDay();
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
const monday = new Date(date);
monday.setDate(diff);
monday.setHours(0, 0, 0, 0);
return monday;
};
const lastMonday = getMonday(lastResetDate);
const currentMonday = getMonday(currentDate);
return lastMonday.getTime() !== currentMonday.getTime();
}
public isExpired(config: ITaskConfig, activatedTime: number): boolean {
const expireTime = this.getExpireTime(config, activatedTime);
return Date.now() > expireTime;
}
public getNextResetTime(config: ITaskConfig): number {
const now = new Date();
const nextMonday = new Date(now);
const day = now.getDay();
const diff = 7 - day + 1;
nextMonday.setDate(now.getDate() + diff);
nextMonday.setHours(0, 0, 0, 0);
return nextMonday.getTime();
}
public getExpireTime(config: ITaskConfig, activatedTime: number): number {
const activatedDate = new Date(activatedTime);
const day = activatedDate.getDay();
const diff = 7 - day + 1;
const expireDate = new Date(activatedDate);
expireDate.setDate(expireDate.getDate() + diff);
expireDate.setHours(0, 0, 0, 0);
return expireDate.getTime();
}
}
/**
* 每月任务处理器
*/
export class MonthlyTaskHandler implements ITaskTypeHandler {
getType(): string {
return GlobalTaskType.MONTHLY;
}
public validateConfig(config: ITaskConfig): boolean {
if (config.resetCycle !== TaskResetCycle.MONTHLY) {
LogUtils.warn(TAG, `每月任务重置周期应为 MONTHLY: ${config.id}`);
return false;
}
return true;
}
public shouldReset(config: ITaskConfig, lastResetTime: number): boolean {
const lastResetDate = new Date(lastResetTime);
const currentDate = new Date();
return (
lastResetDate.getMonth() !== currentDate.getMonth() ||
lastResetDate.getFullYear() !== currentDate.getFullYear()
);
}
public isExpired(config: ITaskConfig, activatedTime: number): boolean {
const expireTime = this.getExpireTime(config, activatedTime);
return Date.now() > expireTime;
}
public getNextResetTime(config: ITaskConfig): number {
const now = new Date();
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
nextMonth.setHours(0, 0, 0, 0);
return nextMonth.getTime();
}
public getExpireTime(config: ITaskConfig, activatedTime: number): number {
const activatedDate = new Date(activatedTime);
const expireDate = new Date(
activatedDate.getFullYear(),
activatedDate.getMonth() + 1,
1,
);
expireDate.setHours(0, 0, 0, 0);
return expireDate.getTime();
}
}
/**
* 赛季任务处理器
*/
export class SeasonalTaskHandler implements ITaskTypeHandler {
getType(): string {
return GlobalTaskType.SEASONAL;
}
public validateConfig(config: ITaskConfig): boolean {
if (config.resetCycle !== TaskResetCycle.SEASONAL) {
LogUtils.warn(TAG, `赛季任务重置周期应为 SEASONAL: ${config.id}`);
return false;
}
if (!config.startTime || !config.endTime) {
LogUtils.warn(TAG, `赛季任务必须设置开始和结束时间: ${config.id}`);
return false;
}
return true;
}
public shouldReset(config: ITaskConfig, lastResetTime: number): boolean {
// 赛季任务通常不自动重置,由配置的开始时间决定
return false;
}
public isExpired(config: ITaskConfig, activatedTime: number): boolean {
return config.endTime ? Date.now() > config.endTime : false;
}
public getNextResetTime(config: ITaskConfig): number {
// 赛季任务的下次重置时间通常是下个赛季的开始时间
return config.endTime || 0;
}
public getExpireTime(config: ITaskConfig, activatedTime: number): number {
return config.endTime || Number.MAX_SAFE_INTEGER;
}
}
/**
* 任务类型管理器
*/
export class TaskTypeManager extends Singleton {
private handlers: Map<TaskType, ITaskTypeHandler> = new Map();
protected async onInit() {
this.registerDefaultHandlers();
}
/**
* 注册默认的任务类型处理器
*/
private registerDefaultHandlers(): void {
const handlers = TaskTypeRegistry.createAllHandlers();
for (const handler of handlers) {
this.registerHandler(handler);
}
LogUtils.info(TAG, `自动注册了 ${handlers.length} 个任务类型处理器`);
}
/**
* 注册任务类型处理器
*/
public registerHandler(handler: ITaskTypeHandler): void {
const type = TaskTypeRegistry.getTypeByHandler(handler);
this.handlers.set(type, handler);
LogUtils.info(TAG, `注册任务类型处理器: ${type}`);
}
/**
* 获取任务类型处理器
*/
public getHandler(type: TaskType): ITaskTypeHandler | undefined {
return this.handlers.get(type);
}
/**
* 验证任务配置
*/
public validateTaskConfig(config: ITaskConfig): boolean {
const handler = this.getHandler(config.type);
if (!handler) {
LogUtils.warn(TAG, `未找到任务类型处理器: ${config.type}`);
return false;
}
return handler.validateConfig(config);
}
/**
* 检查任务是否应该重置
*/
public shouldResetTask(
config: ITaskConfig,
lastResetTime: number,
): boolean {
const handler = this.getHandler(config.type);
if (!handler) {
return false;
}
return handler.shouldReset(config, lastResetTime);
}
/**
* 检查任务是否过期
*/
public isTaskExpired(config: ITaskConfig, activatedTime: number): boolean {
const handler = this.getHandler(config.type);
if (!handler) {
return false;
}
return handler.isExpired(config, activatedTime);
}
/**
* 获取任务下次重置时间
*/
public getTaskNextResetTime(config: ITaskConfig): number {
const handler = this.getHandler(config.type);
if (!handler) {
return 0;
}
return handler.getNextResetTime(config);
}
/**
* 获取任务过期时间
*/
public getTaskExpireTime(
config: ITaskConfig,
activatedTime: number,
): number {
const handler = this.getHandler(config.type);
if (!handler) {
return Number.MAX_SAFE_INTEGER;
}
return handler.getExpireTime(config, activatedTime);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7591243c-cfc5-44dc-a93c-ddba56c9422b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "bc0e3ad3-a16b-48eb-878f-4c2a0e9fa318",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,114 @@
import { TaskType } from "../TaskData";
import { ITaskTypeHandler } from "../TaskTypeManager";
import LogUtils from "../../utils/LogUtils";
import { EDITOR } from "cc/env";
const TAG = "TaskTypeDecorator";
/**
* 任务类型处理器注册表
*/
class TaskTypeRegistry {
private static handlers: Map<TaskType, new () => ITaskTypeHandler> =
new Map();
private static handlerInstances: Map<ITaskTypeHandler, TaskType> =
new Map();
/**
* 注册任务类型处理器
*/
public static register(
type: TaskType,
handlerClass: new () => ITaskTypeHandler,
): void {
if (EDITOR) {
return;
}
this.handlers.set(type, handlerClass);
LogUtils.info(TAG, `注册任务类型处理器类: ${type}`);
}
/**
* 获取所有注册的处理器类
*/
public static getAllHandlerClasses(): Map<
TaskType,
new () => ITaskTypeHandler
> {
return new Map(this.handlers);
}
/**
* 创建所有处理器实例
*/
public static createAllHandlers(): ITaskTypeHandler[] {
const handlers: ITaskTypeHandler[] = [];
for (const [type, HandlerClass] of this.handlers) {
try {
const handler = new HandlerClass();
handlers.push(handler);
// 记录实例与类型的映射关系
this.handlerInstances.set(handler, type);
LogUtils.info(TAG, `创建任务类型处理器实例: ${type}`);
} catch (err) {
LogUtils.error(TAG, `创建任务类型处理器失败: ${type}`, err);
}
}
return handlers;
}
/**
* 通过处理器实例获取对应的任务类型
* @param handler 处理器实例
* @returns 对应的任务类型,如果未找到则返回 undefined
*/
public static getTypeByHandler(
handler: ITaskTypeHandler,
): TaskType | undefined {
// 首先从实例映射中查找
const typeFromMap = this.handlerInstances.get(handler);
if (typeFromMap !== undefined) {
return typeFromMap;
}
// 如果映射中没有,尝试通过构造函数查找
const handlerConstructor =
handler.constructor as new () => ITaskTypeHandler;
for (const [type, HandlerClass] of this.handlers) {
if (HandlerClass === handlerConstructor) {
// 更新实例映射
this.handlerInstances.set(handler, type);
LogUtils.debug(TAG, `通过构造函数找到处理器类型: ${type}`);
return type;
}
}
LogUtils.warn(TAG, "无法找到处理器对应的任务类型");
return undefined;
}
/**
* 清除实例映射缓存
*/
public static clearInstanceCache(): void {
this.handlerInstances.clear();
LogUtils.info(TAG, "已清除处理器实例映射缓存");
}
}
/**
* 任务类型装饰器
* @param type 任务类型
*/
export function TaskTypeHandler(type: TaskType) {
return function <T extends new () => ITaskTypeHandler>(target: T): T {
// 注册处理器类
TaskTypeRegistry.register(type, target);
return target;
};
}
/**
* 导出注册表供 TaskTypeManager 使用
*/
export { TaskTypeRegistry };

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b12123d8-76c4-4593-8d60-bc155c58a14f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "01a41960-3ea0-4285-b02c-fa4dd616bba1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,53 @@
import LogUtils from "../../utils/LogUtils";
import { GlobalTaskType, ITaskConfig, TaskResetCycle } from "../TaskData";
import { ITaskTypeHandler } from "../TaskTypeManager";
import { TaskTypeHandler } from "../decorators/TaskTypeDecorator";
const TAG = "DailyTaskHandler";
/**
* 每日任务处理器
*/
@TaskTypeHandler(GlobalTaskType.DAILY)
export class DailyTaskHandler implements ITaskTypeHandler {
public validateConfig(config: ITaskConfig): boolean {
if (config.resetCycle !== TaskResetCycle.DAILY) {
LogUtils.warn(TAG, `每日任务重置周期应为 DAILY: ${config.id}`);
return false;
}
return true;
}
public shouldReset(config: ITaskConfig, lastResetTime: number): boolean {
const now = Date.now();
const lastResetDate = new Date(lastResetTime);
const currentDate = new Date(now);
// 检查是否跨天
return (
lastResetDate.getDate() !== currentDate.getDate() ||
lastResetDate.getMonth() !== currentDate.getMonth() ||
lastResetDate.getFullYear() !== currentDate.getFullYear()
);
}
public isExpired(config: ITaskConfig, activatedTime: number): boolean {
const expireTime = this.getExpireTime(config, activatedTime);
return Date.now() > expireTime;
}
public getNextResetTime(config: ITaskConfig): number {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
return tomorrow.getTime();
}
public getExpireTime(config: ITaskConfig, activatedTime: number): number {
const activatedDate = new Date(activatedTime);
const expireDate = new Date(activatedDate);
expireDate.setDate(expireDate.getDate() + 1);
expireDate.setHours(0, 0, 0, 0);
return expireDate.getTime();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "68a07db2-c379-4565-aa54-63671afd0bfc",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "9f931516-2af1-4d73-894f-576a5f792662",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,319 @@
import { _decorator, Director, director } from "cc";
import NodeSingleton from "../NodeSingleton";
import LogUtils from "../utils/LogUtils";
const { ccclass } = _decorator;
/**
* 计时器接口
*/
export interface ITimer {
/** 计时器ID */
id: number;
/** 延迟时间(秒) */
delay: number;
/** 重复次数,-1表示无限重复 */
repeat: number;
/** 回调函数 */
callback: () => void;
/** 已执行次数 */
executeCount: number;
/** 累计时间 */
elapsedTime: number;
/** 是否暂停 */
isPaused: boolean;
/** 是否已完成 */
isCompleted: boolean;
}
/**
* 时间管理器
* 提供计时器功能和时间矫正功能
*/
@ccclass("TimeManager")
export default class TimeManager extends NodeSingleton {
private static readonly TAG = "TimeManager";
/** 计时器映射表 */
private _timers: Map<number, ITimer> = new Map();
/** 计时器ID生成器 */
private _timerIdGenerator = 0;
/** 服务器时间偏移量(毫秒) */
private _serverTimeOffset = 0;
/** 是否已进行时间矫正 */
private _isTimeCorrected = false;
/** 上次更新时间 */
private _lastUpdateTime = 0;
protected onLoad(): void {
this._lastUpdateTime = Date.now();
LogUtils.log(TimeManager.TAG, "TimeManager 初始化完成");
}
protected onEnable(): void {
director.on(Director.EVENT_BEFORE_UPDATE, this.update, this);
}
protected onDisable(): void {
director.off(Director.EVENT_BEFORE_UPDATE, this.update, this);
}
/**
* 更新计时器
*/
public update(): void {
const currentTime = Date.now();
const deltaTime = (currentTime - this._lastUpdateTime) / 1000; // 转换为秒
this._lastUpdateTime = currentTime;
// 更新所有计时器
const completedTimers: number[] = [];
for (const [id, timer] of this._timers.entries()) {
if (timer.isPaused || timer.isCompleted) {
continue;
}
timer.elapsedTime += deltaTime;
if (timer.elapsedTime >= timer.delay) {
// 执行回调
try {
timer.callback();
} catch (err) {
LogUtils.error(
TimeManager.TAG,
`计时器回调执行出错: ${err}`,
);
} finally {
timer.executeCount++;
}
// 检查是否需要重复
if (timer.repeat === -1 || timer.executeCount < timer.repeat) {
timer.elapsedTime = 0; // 重置计时
} else {
timer.isCompleted = true;
completedTimers.push(id);
}
}
}
// 清理已完成的计时器
for (const id of completedTimers) {
this._timers.delete(id);
}
}
/**
* 创建计时器
* @param delay 延迟时间(秒)
* @param callback 回调函数
* @param repeat 重复次数默认1次-1表示无限重复
* @returns 计时器ID
*/
public createTimer(
delay: number,
callback: () => void,
repeat: number = 1,
): number {
if (delay <= 0) {
LogUtils.warn(TimeManager.TAG, "计时器延迟时间必须大于0");
return -1;
}
if (!callback) {
LogUtils.warn(TimeManager.TAG, "计时器回调函数不能为空");
return -1;
}
const timerId = ++this._timerIdGenerator;
const timer: ITimer = {
id: timerId,
delay: delay,
repeat: repeat,
callback: callback,
executeCount: 0,
elapsedTime: 0,
isPaused: false,
isCompleted: false,
};
this._timers.set(timerId, timer);
LogUtils.log(
TimeManager.TAG,
`创建计时器: ID=${timerId}, delay=${delay}s, repeat=${repeat}`,
);
return timerId;
}
/**
* 暂停计时器
* @param timerId 计时器ID
*/
public pauseTimer(timerId: number): void {
const timer = this._timers.get(timerId);
if (timer) {
timer.isPaused = true;
LogUtils.log(TimeManager.TAG, `暂停计时器: ID=${timerId}`);
} else {
LogUtils.warn(TimeManager.TAG, `计时器不存在: ID=${timerId}`);
}
}
/**
* 恢复计时器
* @param timerId 计时器ID
*/
public resumeTimer(timerId: number): void {
const timer = this._timers.get(timerId);
if (timer) {
timer.isPaused = false;
LogUtils.log(TimeManager.TAG, `恢复计时器: ID=${timerId}`);
} else {
LogUtils.warn(TimeManager.TAG, `计时器不存在: ID=${timerId}`);
}
}
/**
* 停止并移除计时器
* @param timerId 计时器ID
*/
public removeTimer(timerId: number): void {
if (this._timers.delete(timerId)) {
LogUtils.log(TimeManager.TAG, `移除计时器: ID=${timerId}`);
} else {
LogUtils.warn(TimeManager.TAG, `计时器不存在: ID=${timerId}`);
}
}
/**
* 清除所有计时器
*/
public clearAllTimers(): void {
const count = this._timers.size;
this._timers.clear();
LogUtils.log(TimeManager.TAG, `清除所有计时器,共${count}`);
}
/**
* 获取计时器信息
* @param timerId 计时器ID
* @returns 计时器信息如果不存在返回null
*/
public getTimerInfo(timerId: number): ITimer | null {
return this._timers.get(timerId) || null;
}
/**
* 获取活跃计时器数量
* @returns 活跃计时器数量
*/
public getActiveTimerCount(): number {
let count = 0;
for (const [, timer] of this._timers) {
if (!timer.isCompleted && !timer.isPaused) {
count++;
}
}
return count;
}
/**
* 设置服务器时间矫正
* @param serverTimestamp 服务器时间戳(毫秒)
*/
public correctTime(serverTimestamp: number): void {
const localTime = Date.now();
this._serverTimeOffset = serverTimestamp - localTime;
this._isTimeCorrected = true;
LogUtils.log(
TimeManager.TAG,
`时间矫正完成,偏移量: ${this._serverTimeOffset}ms`,
);
}
/**
* 获取矫正后的当前时间戳(毫秒)
* @returns 矫正后的时间戳
*/
public getCorrectedTime(): number {
return Date.now() + this._serverTimeOffset;
}
/**
* 获取矫正后的当前时间戳(秒)
* @returns 矫正后的时间戳(秒)
*/
public getCorrectedTimeSeconds(): number {
return Math.floor(this.getCorrectedTime() / 1000);
}
/**
* 获取服务器时间偏移量
* @returns 偏移量(毫秒)
*/
public getTimeOffset(): number {
return this._serverTimeOffset;
}
/**
* 是否已进行时间矫正
* @returns 是否已矫正
*/
public isTimeCorrected(): boolean {
return this._isTimeCorrected;
}
/**
* 重置时间矫正
*/
public resetTimeCorrection(): void {
this._serverTimeOffset = 0;
this._isTimeCorrected = false;
LogUtils.log(TimeManager.TAG, "时间矫正已重置");
}
// 便捷方法
/**
* 延迟执行(一次性)
* @param delay 延迟时间(秒)
* @param callback 回调函数
* @returns 计时器ID
*/
public delay(delay: number, callback: () => void): number {
return this.createTimer(delay, callback, 1);
}
/**
* 循环执行
* @param interval 间隔时间(秒)
* @param callback 回调函数
* @returns 计时器ID
*/
public loop(interval: number, callback: () => void): number {
return this.createTimer(interval, callback, -1);
}
/**
* 重复执行指定次数
* @param interval 间隔时间(秒)
* @param callback 回调函数
* @param count 重复次数
* @returns 计时器ID
*/
public repeat(
interval: number,
callback: () => void,
count: number,
): number {
return this.createTimer(interval, callback, count);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d6880bf2-460f-401a-aa43-bc264265a46c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "f26384d6-ae5e-4196-b413-d675dc2de641",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,3 @@
import BaseUI from "./BaseUI";
export default abstract class BaseLayer extends BaseUI {}

Some files were not shown because too many files have changed in this diff Show More