|
|
@@ -0,0 +1,281 @@
|
|
|
+import { _decorator, Node, Component, find } from 'cc';
|
|
|
+import EventBus, { GameEvents } from '../Core/EventBus';
|
|
|
+
|
|
|
+const { ccclass, property } = _decorator;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 敌人攻击状态管理器
|
|
|
+ * 使用字典结构快速查询敌人是否可以被攻击
|
|
|
+ * 采用标准单例模式,不依赖BaseSingleton
|
|
|
+ */
|
|
|
+@ccclass('EnemyAttackStateManager')
|
|
|
+export class EnemyAttackStateManager extends Component {
|
|
|
+ private static _instance: EnemyAttackStateManager | null = null;
|
|
|
+
|
|
|
+ // 敌人攻击状态字典:key为敌人节点的uuid,value为是否可攻击
|
|
|
+ private enemyAttackableStates: Map<string, boolean> = new Map();
|
|
|
+
|
|
|
+ // 敌人节点引用字典:key为敌人节点的uuid,value为敌人节点
|
|
|
+ private enemyNodeReferences: Map<string, Node> = new Map();
|
|
|
+
|
|
|
+ // Debug(属性检查器展示)
|
|
|
+ @property({ tooltip: '是否更新调试信息(属性检查器)' })
|
|
|
+ public showDebug: boolean = true;
|
|
|
+
|
|
|
+ @property({ tooltip: '调试信息刷新间隔(秒)' })
|
|
|
+ public debugUpdateInterval: number = 0.5;
|
|
|
+
|
|
|
+ // 以下字段通过装饰器在属性检查器中显示(只读调试)
|
|
|
+ @property({ tooltip: '敌人总数(只读)' })
|
|
|
+ public totalEnemies: number = 0;
|
|
|
+
|
|
|
+ @property({ tooltip: '可攻击敌人数(只读)' })
|
|
|
+ public attackableCount: number = 0;
|
|
|
+
|
|
|
+ @property({ type: [String], tooltip: '示例敌人名称(最多10个)' })
|
|
|
+ public enemyNameSamples: string[] = [];
|
|
|
+
|
|
|
+ @property({ tooltip: '字典状态预览(JSON,只读)' })
|
|
|
+ public statesPreviewJson: string = '';
|
|
|
+
|
|
|
+ // 组件加载时设置单例并注册事件
|
|
|
+ onLoad() {
|
|
|
+ EnemyAttackStateManager._instance = this;
|
|
|
+ this.setupEventListeners();
|
|
|
+ if (this.showDebug) {
|
|
|
+ this.schedule(this.refreshInspectorDebug.bind(this), Math.max(0.1, this.debugUpdateInterval));
|
|
|
+ }
|
|
|
+ this.refreshInspectorDebug();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static getInstance(): EnemyAttackStateManager {
|
|
|
+ // 若已有实例则直接返回
|
|
|
+ if (EnemyAttackStateManager._instance) {
|
|
|
+ return EnemyAttackStateManager._instance;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试从场景中获取或挂载到 Canvas/GameLevelUI/enemyContainer
|
|
|
+ const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
|
|
|
+ if (enemyContainer) {
|
|
|
+ let comp = enemyContainer.getComponent(EnemyAttackStateManager) as EnemyAttackStateManager;
|
|
|
+ if (!comp) {
|
|
|
+ comp = enemyContainer.addComponent(EnemyAttackStateManager) as EnemyAttackStateManager;
|
|
|
+ }
|
|
|
+ EnemyAttackStateManager._instance = comp;
|
|
|
+ return comp;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果未找到节点,返回一个占位的组件实例(不建议,但保证调用不崩溃)
|
|
|
+ // 注意:占位实例无法可视化显示,仅用于避免空指针
|
|
|
+ const placeholder = new EnemyAttackStateManager();
|
|
|
+ EnemyAttackStateManager._instance = placeholder as any;
|
|
|
+ return EnemyAttackStateManager._instance;
|
|
|
+ }
|
|
|
+
|
|
|
+ private setupEventListeners() {
|
|
|
+ const eventBus = EventBus.getInstance();
|
|
|
+
|
|
|
+ // 监听敌人隐身开始事件
|
|
|
+ eventBus.on(GameEvents.ENEMY_STEALTH_START, this.onEnemyStealthStart, this);
|
|
|
+
|
|
|
+ // 监听敌人隐身结束事件
|
|
|
+ eventBus.on(GameEvents.ENEMY_STEALTH_END, this.onEnemyStealthEnd, this);
|
|
|
+
|
|
|
+ // 监听敌人死亡事件
|
|
|
+ eventBus.on('ENEMY_KILLED', this.onEnemyKilled, this);
|
|
|
+
|
|
|
+ // 监听清除所有敌人事件
|
|
|
+ eventBus.on(GameEvents.CLEAR_ALL_ENEMIES, this.onClearAllEnemies, this);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注册敌人到攻击状态管理器
|
|
|
+ * @param enemyNode 敌人节点
|
|
|
+ * @param isAttackable 初始攻击状态,默认为true
|
|
|
+ */
|
|
|
+ public registerEnemy(enemyNode: Node, isAttackable: boolean = true) {
|
|
|
+ if (!enemyNode || !enemyNode.isValid) return;
|
|
|
+
|
|
|
+ const enemyId = enemyNode.uuid;
|
|
|
+ this.enemyAttackableStates.set(enemyId, isAttackable);
|
|
|
+ this.enemyNodeReferences.set(enemyId, enemyNode);
|
|
|
+
|
|
|
+ console.log(`[EnemyAttackStateManager] 注册敌人: ${enemyNode.name} (${enemyId}), 可攻击: ${isAttackable}`);
|
|
|
+ this.refreshInspectorDebug();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从攻击状态管理器中移除敌人
|
|
|
+ * @param enemyNode 敌人节点
|
|
|
+ */
|
|
|
+ public unregisterEnemy(enemyNode: Node) {
|
|
|
+ if (!enemyNode) return;
|
|
|
+
|
|
|
+ const enemyId = enemyNode.uuid;
|
|
|
+ this.enemyAttackableStates.delete(enemyId);
|
|
|
+ this.enemyNodeReferences.delete(enemyId);
|
|
|
+
|
|
|
+ console.log(`[EnemyAttackStateManager] 移除敌人: ${enemyNode.name} (${enemyId})`);
|
|
|
+ this.refreshInspectorDebug();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置敌人的攻击状态
|
|
|
+ * @param enemyNode 敌人节点
|
|
|
+ * @param attackable 是否可攻击
|
|
|
+ */
|
|
|
+ public setEnemyAttackable(enemyNode: Node, attackable: boolean): void {
|
|
|
+ if (!enemyNode || !enemyNode.isValid) return;
|
|
|
+
|
|
|
+ const enemyId = enemyNode.uuid;
|
|
|
+ const previousState = this.enemyAttackableStates.get(enemyId);
|
|
|
+
|
|
|
+ this.enemyAttackableStates.set(enemyId, attackable);
|
|
|
+
|
|
|
+ // 添加状态变化日志
|
|
|
+ if (previousState !== attackable) {
|
|
|
+ const stateText = attackable ? '可攻击' : '不可攻击';
|
|
|
+ const reasonText = attackable ? '(退出隐身)' : '(进入隐身)';
|
|
|
+ console.log(`[EnemyAttackStateManager] 敌人状态变化: ${enemyNode.name} (${enemyId.substring(0, 8)}...) -> ${stateText} ${reasonText}`);
|
|
|
+ }
|
|
|
+ this.refreshInspectorDebug();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查敌人是否可以被攻击
|
|
|
+ * @param enemyNode 敌人节点
|
|
|
+ * @returns 是否可攻击
|
|
|
+ */
|
|
|
+ public isEnemyAttackable(enemyNode: Node): boolean {
|
|
|
+ if (!enemyNode || !enemyNode.isValid) return false;
|
|
|
+
|
|
|
+ const enemyId = enemyNode.uuid;
|
|
|
+ return this.enemyAttackableStates.get(enemyId) ?? false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取所有可攻击的敌人节点
|
|
|
+ * @returns 可攻击的敌人节点数组
|
|
|
+ */
|
|
|
+ public getAttackableEnemies(): Node[] {
|
|
|
+ const attackableEnemies: Node[] = [];
|
|
|
+
|
|
|
+ for (const [enemyId, isAttackable] of this.enemyAttackableStates.entries()) {
|
|
|
+ if (isAttackable) {
|
|
|
+ const enemyNode = this.enemyNodeReferences.get(enemyId);
|
|
|
+ if (enemyNode && enemyNode.isValid) {
|
|
|
+ attackableEnemies.push(enemyNode);
|
|
|
+ } else {
|
|
|
+ // 清理无效的引用
|
|
|
+ this.enemyAttackableStates.delete(enemyId);
|
|
|
+ this.enemyNodeReferences.delete(enemyId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return attackableEnemies;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理所有敌人状态
|
|
|
+ */
|
|
|
+ public clearAllEnemies() {
|
|
|
+ this.enemyAttackableStates.clear();
|
|
|
+ this.enemyNodeReferences.clear();
|
|
|
+ console.log(`[EnemyAttackStateManager] 清理所有敌人状态`);
|
|
|
+ this.refreshInspectorDebug();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前管理的敌人数量
|
|
|
+ */
|
|
|
+ public getEnemyCount(): number {
|
|
|
+ return this.enemyAttackableStates.size;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取可攻击敌人数量
|
|
|
+ */
|
|
|
+ public getAttackableEnemyCount(): number {
|
|
|
+ let count = 0;
|
|
|
+ for (const isAttackable of this.enemyAttackableStates.values()) {
|
|
|
+ if (isAttackable) count++;
|
|
|
+ }
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 事件处理方法
|
|
|
+ private onEnemyStealthStart(data: { enemy: any, duration?: number }) {
|
|
|
+ if (data.enemy && data.enemy.node) {
|
|
|
+ this.setEnemyAttackable(data.enemy.node, false);
|
|
|
+ console.log(`[EnemyAttackStateManager] 敌人进入隐身状态: ${data.enemy.node.name}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private onEnemyStealthEnd(data: { enemy: any }) {
|
|
|
+ if (data.enemy && data.enemy.node) {
|
|
|
+ this.setEnemyAttackable(data.enemy.node, true);
|
|
|
+ console.log(`[EnemyAttackStateManager] 敌人退出隐身状态: ${data.enemy.node.name}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private onEnemyKilled() {
|
|
|
+ // 注意:敌人节点的移除已经在EnemyInstance.onDestroy()中通过unregisterEnemy()处理
|
|
|
+ // 这个事件处理器主要用于其他需要响应敌人死亡的逻辑
|
|
|
+ console.log(`[EnemyAttackStateManager] 接收到敌人死亡事件`);
|
|
|
+ }
|
|
|
+
|
|
|
+ private onClearAllEnemies() {
|
|
|
+ this.clearAllEnemies();
|
|
|
+ }
|
|
|
+
|
|
|
+ onDestroy() {
|
|
|
+ const eventBus = EventBus.getInstance();
|
|
|
+
|
|
|
+ // 移除所有事件监听器
|
|
|
+ eventBus.off(GameEvents.ENEMY_STEALTH_START, this.onEnemyStealthStart, this);
|
|
|
+ eventBus.off(GameEvents.ENEMY_STEALTH_END, this.onEnemyStealthEnd, this);
|
|
|
+ eventBus.off('ENEMY_KILLED', this.onEnemyKilled, this);
|
|
|
+ eventBus.off(GameEvents.CLEAR_ALL_ENEMIES, this.onClearAllEnemies, this);
|
|
|
+
|
|
|
+ // 清空所有数据
|
|
|
+ this.enemyAttackableStates.clear();
|
|
|
+ this.enemyNodeReferences.clear();
|
|
|
+
|
|
|
+ // 清空单例实例
|
|
|
+ EnemyAttackStateManager._instance = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // === Debug(属性检查器) ===
|
|
|
+ private refreshInspectorDebug() {
|
|
|
+ if (!this.showDebug) return;
|
|
|
+
|
|
|
+ // 概览统计
|
|
|
+ this.totalEnemies = this.getEnemyCount();
|
|
|
+ this.attackableCount = this.getAttackableEnemyCount();
|
|
|
+
|
|
|
+ // 示例名称(最多10个)
|
|
|
+ const samples: string[] = [];
|
|
|
+ let i = 0;
|
|
|
+ for (const [, enemyNode] of this.enemyNodeReferences.entries()) {
|
|
|
+ if (enemyNode && enemyNode.isValid) {
|
|
|
+ samples.push(enemyNode.name);
|
|
|
+ i++;
|
|
|
+ if (i >= 10) break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.enemyNameSamples = samples;
|
|
|
+
|
|
|
+ // 预览字典(最多20条,JSON字符串)
|
|
|
+ const previewItems: Array<{ name: string; id: string; attackable: boolean }> = [];
|
|
|
+ i = 0;
|
|
|
+ for (const [enemyId, isAttackable] of this.enemyAttackableStates.entries()) {
|
|
|
+ const enemyNode = this.enemyNodeReferences.get(enemyId);
|
|
|
+ const name = enemyNode && enemyNode.isValid ? enemyNode.name : '(invalid)';
|
|
|
+ previewItems.push({ name, id: enemyId.substring(0, 8), attackable: !!isAttackable });
|
|
|
+ i++;
|
|
|
+ if (i >= 20) break;
|
|
|
+ }
|
|
|
+ this.statesPreviewJson = JSON.stringify(previewItems, null, 2);
|
|
|
+ }
|
|
|
+}
|