import { _decorator, Component, find, isValid, Node, Vec3 } from 'cc'; import { GEvent } from 'db://assets/mx/module/event/GEvent'; import { MonsterState } from './MonsterState'; import { Monster } from './Monster'; import { tetriMap } from '../../tetriMap/tetriMap'; import { jiujiuwoMap } from '../../jiujiuwoMap/jiujiuwoMap'; import { BattleType } from '../../manager/ChapterDataManager'; const { ccclass, property } = _decorator; @ccclass('MapAgent') export class MapAgent extends Component { /** 全局点位分配:确保同一 wallTarget 的怪尽量分散到不同 point */ private static _wallPointPool: Map = new Map(); private static parsePointIndex(name: string, prefix: string): number | null { if (!name) return null; if (!name.startsWith(prefix)) return null; const n = parseInt(name.slice(prefix.length), 10); return Number.isFinite(n) ? n : null; } private getMonsterComp(): { state: MonsterState; getAtkTarget: () => void } | null { const c1 = this.node.getComponent(Monster) as unknown as { state: MonsterState; getAtkTarget: () => void } | null; if (c1) return c1; // 兜底:老版本可能依赖字符串查找 return this.node.getComponent('Monster') as unknown as { state: MonsterState; getAtkTarget: () => void } | null; } @property showDebug: boolean = false; curMap: tetriMap|jiujiuwoMap = null; /** tetriMap 终点节点路径(相对 curMap.node) */ @property tetraEndPath: string = '目标点/wallTarget1'; /** 从 wallTarget1 下的 points(point1..point10)随机取终点 */ @property useWallTargetPoints: boolean = true; /** 只匹配以这个前缀开头的子节点名 */ @property wallTargetPointPrefix: string = 'point'; /** 到达点的半径(像素),小于该值就停下攻击 */ @property arriveRadiusToPoint: number = 18; private _directTargetWp: Vec3 | null = null; private _hasPickedPointTarget: boolean = false; /** 出生点编号(用于决定走左/右 point 分组)。0 表示未知,回退用坐标判断 */ private _spawnPointId: number = 0; /**是否正在移动 */ isMoving: boolean = false; /**本对象是否可移动 */ CanMove: boolean = true; /**移动速度 */ MoveSpeed: number = 100; /**是否需要面向目标 */ NeedFaceTarget: boolean = true; /**当前左右朝向(移动时要改变) */ private _moveDir = 1; /**朝向更新的最小横向位移阈值,避免竖直路段抖动 */ private _faceEpsilonX = 1e-3; /**编辑前的位置(复用向量,避免 enter/exit 编辑时 clone) */ private _editPos: Vec3 = new Vec3(); private _moveCompleteCallback: Function = null; private _pendingPathRefresh = false; /** move / 剩余路径距离 复用,避免每帧 new Vec3 */ private _v3Current = new Vec3(); private _v3Target = new Vec3(); private _v3Dir = new Vec3(); private _v3LastWp = new Vec3(); /**设置寻路目标 */ setTarget(target: Vec3 | Node | any, moveCompleteCallback: Function = null) { this._moveCompleteCallback = moveCompleteCallback; this._directTargetWp = null; this._hasPickedPointTarget = false; if (target instanceof Vec3) { this._directTargetWp = target.clone(); } else if (target instanceof Node) { this._directTargetWp = target.worldPosition.clone(); } else if (target && target.node && target.node.worldPosition) { // 兼容旧接口传入“带 node 的对象” this._directTargetWp = target.node.worldPosition.clone(); } this.isMoving = !!this._directTargetWp; } /**由 spawner 注入出生点编号,避免对象池复用/瞬时坐标导致左右分组判断错误 */ setSpawnPointId(spawnPointId: number) { this._spawnPointId = spawnPointId || 0; // 重新挑选终点 this._directTargetWp = null; this._hasPickedPointTarget = false; } protected onEnable(): void { GEvent.Ins.on(GEvent.EnterEdit, this.onEnterEdit, this) GEvent.Ins.on(GEvent.ExitEdit, this.onExitEdit, this) } protected onDisable(): void { GEvent.Ins.off(GEvent.EnterEdit, this.onEnterEdit, this) GEvent.Ins.off(GEvent.ExitEdit, this.onExitEdit, this) } update(deltaTime: number) { if (!gg.game.isBattleContextActive()) return; if(!gg.game.CurentBattle.canUpdateFrame()){ return; } if (this.CanMove && gg.game.CurentBattle.CanMove) { // 如果外部没 setTarget,则尝试从 tetriMap 自动解析终点 if (!this._directTargetWp) { this.tryAutoSetTargetFromTetraMap(); } this.move(deltaTime); } } /**尝试从curMap自动解析终点 */ tryAutoSetTargetFromTetraMap() { // stopAndAttack() 会清空 _directTargetWp,但某些复用流程可能没重置 _hasPickedPointTarget, // 导致这里一直 return,怪物无法重新 pick 目标点。 if (this._hasPickedPointTarget && !this._directTargetWp) { this._hasPickedPointTarget = false; } if(this._hasPickedPointTarget){ return; } // 兼容对象池/重开局:MapAgent 可能先于 CurentBattle.curMap 初始化, // 导致 curMap 引用为空而怪物一直拿不到终点。 if (!this.curMap || !isValid(this.curMap as any)) { const cur = gg.game.CurentBattle?.curMap; if (cur && isValid(cur)) { this.curMap = cur; } } const root = this.curMap?.node; if (!root || !root.isValid) return; // 用 find 解析路径(兼容 3.x) let endNode: Node | null = find(this.tetraEndPath, root); if (!endNode) endNode = find('wallTarget1', root); if (!endNode) return; if (!this.useWallTargetPoints) { // 兜底:不使用 points 时,仍可直接走到 wallTarget1 this.setTarget(endNode); return; } const prefix = this.wallTargetPointPrefix || 'point'; const allPts = endNode.children.filter(c => c && c.isValid && c.name && c.name.startsWith(prefix)); if (allPts.length <= 0) { // 没配置 points 时兜底走到 wallTarget1 this.setTarget(endNode); return; } // point1..point10 分组:左怪用 1..5,右怪用 6..10 // 优先按出生点编号判定(spawnPoint_1..3 走左,spawnPoint_4..6 走右),避免复用/瞬时坐标误判 // 未注入出生点时回退用“当前位置相对 wallTarget 的左右”判断 let isLeftMonster: boolean; if (this._spawnPointId >= 1 && this._spawnPointId <= 3) { isLeftMonster = true; } else if (this._spawnPointId >= 4 && this._spawnPointId <= 6) { isLeftMonster = false; } else { const selfX = this.node.worldPosition.x; const endX = endNode.worldPosition.x; isLeftMonster = selfX < endX; } if(gg.game.CurentBattle.BattleType == BattleType.OrangeWeaponMode_Ya){ isLeftMonster = false } const pts = allPts.filter(p => { const idx = MapAgent.parsePointIndex(p.name, prefix); if (idx == null) return false; return isLeftMonster ? idx >= 1 && idx <= 5 : idx >= 6 && idx <= 10; }); // 兜底:如果场景没配齐 1..10 或命名不规范,回退用全部 points const usablePts = pts.length > 0 ? pts : allPts; // 共享洗牌 + 轮询分配,避免大量怪随机撞到同一个点 const key = `${endNode.uuid || endNode.name}:${isLeftMonster ? 'L' : 'R'}`; let pool = MapAgent._wallPointPool.get(key); if (!pool || pool.points.length !== usablePts.length) { const order = Array.from({ length: usablePts.length }, (_, i) => i); // Fisher–Yates shuffle for (let i = order.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const tmp = order[i]; order[i] = order[j]; order[j] = tmp; } pool = { points: usablePts, order, cursor: 0 }; MapAgent._wallPointPool.set(key, pool); } else { // points 可能是新数组对象,但节点引用一致时替换一下 pool.points = usablePts; } const idx = pool.order[pool.cursor % pool.order.length]; pool.cursor = (pool.cursor + 1) % pool.order.length; const pick = pool.points[idx]; this.setTarget(new Vec3(pick.worldPosition.x, pick.worldPosition.y, this.node.worldPosition.z)); this._hasPickedPointTarget = true; } private stopAndAttack() { this._directTargetWp = null; this.isMoving = false; const comp = this.getMonsterComp(); if (comp) { comp.state = MonsterState.ATTACKING; } else { console.log('怪物组件不存在', this.node.name); } this._moveCompleteCallback?.(); } /**朝向当前移动路点移动 */ move(dt: number) { if (!this._directTargetWp) { this.isMoving = false; return; } // 到达随机 point 后停下进入攻击 if (this.useWallTargetPoints && this._hasPickedPointTarget) { const wp = this.node.worldPosition; const dx = this._directTargetWp.x - wp.x; const dy = this._directTargetWp.y - wp.y; const r = Math.max(2, this.arriveRadiusToPoint); if (dx * dx + dy * dy <= r * r) { this.stopAndAttack(); return; } } this.isMoving = true; Vec3.copy(this._v3Current, this.node.worldPosition); Vec3.copy(this._v3Target, this._directTargetWp); // 仅在横向位移足够大时才切换朝向,竖直路点保持当前朝向,避免 x 误差导致乱跳 const dxFace = this._v3Target.x - this._v3Current.x; if (this.NeedFaceTarget && Math.abs(dxFace) > this._faceEpsilonX) { const Xdir = dxFace > 0 ? -1 : 1; if (Xdir != this._moveDir) { this._moveDir = Xdir; let n = this.node.find("skin"); if (!n) n = this.node; n.scale_x = Math.abs(n.scale_x) * Xdir; } } Vec3.subtract(this._v3Dir, this._v3Target, this._v3Current); const distance = this._v3Dir.length(); const moveDistance = this.MoveSpeed * gg.game.CurentBattle.TimeScale * dt; if (distance < 1e-4) { this.node.setWorldPosition(this._v3Target); this.stopAndAttack(); return; } if (moveDistance > distance) { this.node.setWorldPosition(this._v3Target); this.stopAndAttack(); return; } Vec3.normalize(this._v3Dir, this._v3Dir); Vec3.scaleAndAdd(this._v3Current, this._v3Current, this._v3Dir, moveDistance); this.node.setWorldPosition(this._v3Current); } onEnterEdit() { Vec3.copy(this._editPos, this.node.worldPosition); } onExitEdit(save: boolean) { if (!save) { this.node.setWorldPosition(this._editPos); } this.getMonsterComp()?.getAtkTarget(); } /**获取剩余路径距离(简化:当前点到目标点的直线距离,供排序/估算使用) */ getRemainPathDistance(): number { if (!this.node?.isValid) return 0; if (!this._directTargetWp) { this.tryAutoSetTargetFromTetraMap(); } if (!this._directTargetWp) return 0; return Vec3.distance(this.node.worldPosition, this._directTargetWp); } /**传入武器节点,返回当前怪物到武器的距离 */ getNearestMonster(weaponNode: Node): number { if (!weaponNode || !isValid(weaponNode) || !this.node?.isValid) { return Infinity; } const monsterComp = this.node.getComponent(Monster); if (monsterComp?.getIsDead()) { return Infinity; } return Vec3.distance(weaponNode.worldPosition, this.node.worldPosition); } /** * 进入大厅/主界面时清空静态出怪点轮询表。 * Key 含 wallTarget 节点 uuid,多局战斗会不断累积,需在回 home 时释放。 */ public static clearStaticWallPointPool(): void { MapAgent._wallPointPool.clear(); } }