import { instantiate, Node, Prefab, Vec3 ,v3,resources,SpriteFrame,Sprite,SpriteAtlas, BoxCollider2D, NodePool, tween, Tween} from "cc"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { smc } from "../common/SingletonModuleComp"; import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet"; import { HeroInfo } from "../common/config/heroSet"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; import { MoveComp } from "./MoveComp"; /** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */ @ecs.register(`Monster`) export class Monster extends ecs.Entity { /** 怪物数据组件引用 */ HeroModel!: HeroAttrsComp; /** 怪物表现组件引用 */ HeroView!: HeroViewComp; /** 怪物移动组件引用 */ MonMove!: MoveComp; /** 调试开关,控制生命周期日志输出 */ private debugMode: boolean = false; /** 多键对象池:key 为 prefab 路径,value 为对应节点池 */ static pools: Map = new Map(); /** 单个路径的池容量上限 */ static readonly MAX_POOL_SIZE: number = 12; /** 所有路径合计池容量上限 */ static readonly MAX_POOL_TOTAL: number = 60; /** 计算当前所有对象池节点总量 */ private static totalPoolSize(): number { let total = 0; this.pools.forEach((pool) => { total += pool.size(); }); return total; } /** 从指定路径对象池取可用节点,取不到返回 null */ static getFromPool(path: string): Node | null { if (this.pools.has(path)) { const pool = this.pools.get(path)!; while (pool.size() > 0) { const node = pool.get(); if (node && node.isValid) { return node; } } } return null; } /** 节点回收到对象池,超上限则直接销毁 */ static putToPool(path: string, node: Node) { if (!node || !node.isValid) return; if (!this.pools.has(path)) { this.pools.set(path, new NodePool()); } const pool = this.pools.get(path)!; if (pool.size() >= this.MAX_POOL_SIZE || this.totalPoolSize() >= this.MAX_POOL_TOTAL) { node.destroy(); return; } pool.put(node); } /** 清空所有对象池并销毁池内节点 */ static clearPools() { this.pools.forEach((pool) => { while (pool.size() > 0) { const node = pool.get(); if (node && node.isValid) { node.destroy(); } } pool.clear(); }); this.pools.clear(); } /** 获取对象池统计信息,用于调试与容量监控 */ static getPoolStats() { let total = 0; this.pools.forEach((pool) => { total += pool.size(); }); return { paths: this.pools.size, total, maxPerPath: this.MAX_POOL_SIZE, maxTotal: this.MAX_POOL_TOTAL }; } /** 注册实体必需组件:移动 + 属性 */ protected init() { this.addComponents( MoveComp, HeroAttrsComp, ); } /** 销毁实体:优先回收节点,然后释放组件 */ destroy(): void { // 按英雄路径回收到对象池,提升高频刷怪性能 const model = this.get(HeroAttrsComp); const view = this.get(HeroViewComp); if (model && view && view.node && view.node.isValid) { const path = "game/heros/" + HeroInfo[model.hero_uuid].path; Monster.putToPool(path, view.node); } // 手动移除组件,避免 ecs 引用滞留 this.remove(HeroViewComp); this.remove(HeroAttrsComp); super.destroy(); } /** * 加载并初始化怪物 * 1) 优先对象池复用节点,减少实例化开销 * 2) 初始化表现、属性、技能与阵营 * 3) 播放下落入场并在落地后启用碰撞与移动 */ load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y,mon_lv:number=1) { // 怪物默认朝左,表现缩放固定为负向 scale=-1 // 当前怪物尺寸固定,保留变量便于后续扩展 let size=1 var scene = smc.map.MapView.scene; // 根据配置读取怪物预制体路径 var path = "game/heros/"+HeroInfo[uuid].path; // 优先从对象池取节点,未命中时再实例化 let node = Monster.getFromPool(path); if (!node) { var prefab: Prefab = oops.res.get(path, Prefab)!; node = instantiate(prefab); } // 统一挂到实体显示层 HERO 节点下 node.parent = scene.entityLayer!.node!.getChildByName("HERO")!; var view = node.getComponent(HeroViewComp)!; const collider = node.getComponent(BoxCollider2D); // 入场期间关闭碰撞,防止下落时提前参与战斗 if (collider) { collider.enabled = false; } node.setScale(size*node.scale.x,size*node.scale.y); node.setPosition(pos) const model = this.get(HeroAttrsComp); // 从配置表获取怪物静态数据 let hero = HeroInfo[uuid]; // 视图层参数:朝向与碰撞阵营 view.scale = scale; view.box_group = BoxSet.MONSTER; // 模型层参数:身份、阵营、基础数值 model.hero_uuid = uuid; model.hero_name = hero.name; model.hp = model.hp_max = hero.hp; model.ap = hero.ap; model.speed = hero.speed; model.type = hero.type; model.fac = FacSet.MON; // 标记是否 Boss,非 Boss 默认记作杂兵 model.is_boss =is_boss if(!model.is_boss){ model.is_kalami = true; } // 构建技能表并注入运行时冷却字段 ccd model.skills = {}; for (const key in hero.skills) { const skill = hero.skills[key]; if (!skill) continue; // 最终技能等级 = 初始技能等级 + 怪物等级增量,且下限为 0 model.skills[skill.uuid] = { ...skill, lv: Math.max(0,skill.lv + mon_lv - 2), ccd: 0 }; } // 缓存技能射程等派生数据,减少战斗帧内重复计算 model.updateSkillDistanceCache(); // 注册视图组件并重置对象池复用状态 this.add(view); view.init(); view.as.idle(); // 广播怪物加载事件,供刷怪与战斗系统联动 oops.message.dispatchEvent("monster_load",this) // 初始化移动参数:方向、目标 X、站位基准 Y const move = this.get(MoveComp); move.reset(); move.direction = -1; move.targetX = Math.max(-320, Math.min(320, pos.x)); move.baseY = dropToY; move.moving = false; // 依据下落距离自适应入场时长,确保观感一致 const dropDistance = Math.abs(pos.y - dropToY); const dropDuration = Math.max(0.18, Math.min(0.38, dropDistance / 1200)); // 停止旧动画后执行下落 tween,避免复用节点时动画叠加 Tween.stopAllByTarget(node); tween(node) .to(dropDuration, { position: v3(pos.x, dropToY, 0) }) .call(() => { if (!node || !node.isValid) return; // 落地后锁定最终位置,切换到落地完成状态 node.setPosition(pos.x, dropToY, 0); view.playEnd("down"); move.moving = true; // 落地后启用怪物碰撞分组 if (collider) { collider.enabled = true; collider.group = BoxSet.MONSTER; collider.apply(); } }) .start(); // 维护关卡内怪物数量统计 smc.vmdata.mission_data.mon_num++ } /** 重置入口:复用 destroy 的释放流程 */ reset() { super.destroy(); } }