You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
13 KiB
330 lines
13 KiB
|
1 week ago
|
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<string, { points: Node[]; order: number[]; cursor: number }> = 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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|