BulletTrajectory.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  1. import { _decorator, Component, Node, Vec2, Vec3, RigidBody2D, find } from 'cc';
  2. import { EnemyAttackStateManager } from '../EnemyAttackStateManager';
  3. import { BulletTrajectoryConfig } from '../../Core/ConfigManager';
  4. const { ccclass, property } = _decorator;
  5. /**
  6. * 弹道控制器
  7. * 负责控制子弹的运动轨迹
  8. */
  9. export interface TrajectoryState {
  10. initialVelocity: Vec3; // 初始速度
  11. currentVelocity: Vec3; // 当前速度
  12. startPosition: Vec3; // 起始位置
  13. targetPosition?: Vec3; // 目标位置(追踪用)
  14. elapsedTime: number; // 经过时间
  15. phase: 'launch' | 'homing' | 'return'; // 运动阶段
  16. // 路径记录相关
  17. flightPath: Vec3[]; // 飞行路径记录
  18. pathRecordInterval: number; // 路径记录间隔(毫秒)
  19. lastRecordTime: number; // 上次记录时间
  20. returnPathIndex: number; // 返回路径索引
  21. // 发射源方块节点
  22. sourceBlock?: Node; // 发射源方块节点(用于动态获取返回位置)
  23. }
  24. @ccclass('BulletTrajectory')
  25. export class BulletTrajectory extends Component {
  26. private config: BulletTrajectoryConfig = null;
  27. private state: TrajectoryState = null;
  28. private rigidBody: RigidBody2D = null;
  29. private targetNode: Node = null;
  30. private homingTimer: number = 0;
  31. // === Arc Trajectory ===
  32. private arcDir: Vec3 = null; // 当前方向
  33. private arcTargetDir: Vec3 = null; // 目标方向(最终方向)
  34. // === Guided Trajectory (更强自动导航,可拐弯并切换目标) ===
  35. private guidedDir: Vec3 = null; // 当前引导方向
  36. private guidedTargetDir: Vec3 = null; // 目标方向(平滑后的)
  37. /**
  38. * 初始化弹道
  39. */
  40. public init(config: BulletTrajectoryConfig, direction: Vec3, startPos: Vec3, sourceBlock?: Node) {
  41. this.config = { ...config };
  42. this.rigidBody = this.getComponent(RigidBody2D);
  43. if (!this.rigidBody && config.type !== 'arc') {
  44. return;
  45. }
  46. // 初始化状态
  47. this.state = {
  48. initialVelocity: direction.clone().multiplyScalar(config.speed),
  49. currentVelocity: direction.clone().multiplyScalar(config.speed),
  50. startPosition: startPos.clone(),
  51. elapsedTime: 0,
  52. phase: 'launch',
  53. // 路径记录初始化
  54. flightPath: [startPos.clone()],
  55. pathRecordInterval: 50, // 每50毫秒记录一次位置
  56. lastRecordTime: 0,
  57. returnPathIndex: -1,
  58. // 存储发射源方块节点
  59. sourceBlock: sourceBlock
  60. };
  61. this.homingTimer = config.homingDelay;
  62. // 设置初始速度
  63. this.applyInitialVelocity();
  64. // 寻找目标(用于追踪弹道)
  65. if (config.type === 'homing') {
  66. this.findTarget();
  67. }
  68. }
  69. /**
  70. * 设置初始速度
  71. */
  72. private applyInitialVelocity() {
  73. switch (this.config.type) {
  74. case 'straight':
  75. this.rigidBody.linearVelocity = new Vec2(
  76. this.state.initialVelocity.x,
  77. this.state.initialVelocity.y
  78. );
  79. break;
  80. case 'homing':
  81. this.rigidBody.linearVelocity = new Vec2(
  82. this.state.initialVelocity.x,
  83. this.state.initialVelocity.y
  84. );
  85. break;
  86. case 'guided': {
  87. // 参考 arc:初始方向加入随机偏移,但拐弯能力更强
  88. const baseDir = this.state.initialVelocity.clone().normalize();
  89. const sign = Math.random() < 0.5 ? 1 : -1;
  90. const rad = 35 * Math.PI / 180 * sign; // 比 arc 略小的初始偏移,便于后续强导航
  91. const c = Math.cos(rad);
  92. const s = Math.sin(rad);
  93. const offsetDir = new Vec3(
  94. baseDir.x * c - baseDir.y * s,
  95. baseDir.x * s + baseDir.y * c,
  96. 0
  97. ).normalize();
  98. this.guidedDir = offsetDir.clone();
  99. this.guidedTargetDir = baseDir.clone();
  100. // 设置初速度
  101. if (this.rigidBody) {
  102. this.rigidBody.linearVelocity = new Vec2(offsetDir.x * this.config.speed, offsetDir.y * this.config.speed);
  103. }
  104. this.state.currentVelocity.set(offsetDir.x * this.config.speed, offsetDir.y * this.config.speed, 0);
  105. break;
  106. }
  107. case 'arc': {
  108. // 计算带 45° 随机偏移的初始方向
  109. const baseDir = this.state.initialVelocity.clone().normalize();
  110. const signArc = Math.random() < 0.5 ? 1 : -1;
  111. const radArc = 45 * Math.PI / 180 * signArc;
  112. const cosArc = Math.cos(radArc);
  113. const sinArc = Math.sin(radArc);
  114. const offsetDir = new Vec3(
  115. baseDir.x * cosArc - baseDir.y * sinArc,
  116. baseDir.x * sinArc + baseDir.y * cosArc,
  117. 0
  118. ).normalize();
  119. this.arcDir = offsetDir;
  120. this.arcTargetDir = baseDir;
  121. // 如果有物理组件,则设置线速度;否则后续 updateArcTrajectory 将直接操作位移
  122. if (this.rigidBody) {
  123. this.rigidBody.linearVelocity = new Vec2(offsetDir.x * this.config.speed, offsetDir.y * this.config.speed);
  124. }
  125. // 保存当前速度
  126. this.state.currentVelocity.set(offsetDir.x * this.config.speed, offsetDir.y * this.config.speed, 0);
  127. break;
  128. }
  129. }
  130. }
  131. /**
  132. * 寻找追踪目标
  133. */
  134. private findTarget() {
  135. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  136. if (!enemyContainer) return;
  137. const enemies = enemyContainer.children.filter(child =>
  138. child.active && this.isEnemyNode(child)
  139. );
  140. if (enemies.length === 0) return;
  141. // 寻找最近的敌人
  142. let nearestEnemy: Node = null;
  143. let nearestDistance = Infinity;
  144. const bulletPos = this.node.worldPosition;
  145. for (const enemy of enemies) {
  146. const distance = Vec3.distance(bulletPos, enemy.worldPosition);
  147. if (distance < nearestDistance) {
  148. nearestDistance = distance;
  149. nearestEnemy = enemy;
  150. }
  151. }
  152. if (nearestEnemy) {
  153. this.targetNode = nearestEnemy;
  154. this.state.targetPosition = nearestEnemy.worldPosition.clone();
  155. }
  156. }
  157. /**
  158. * 判断是否为敌人节点
  159. */
  160. private isEnemyNode(node: Node): boolean {
  161. // 检查是否为EnemySprite子节点
  162. if (node.name === 'EnemySprite' && node.parent) {
  163. return node.parent.getComponent('EnemyInstance') !== null;
  164. }
  165. // 兼容旧的敌人检测逻辑
  166. const name = node.name.toLowerCase();
  167. return name.includes('enemy') ||
  168. name.includes('敌人') ||
  169. node.getComponent('EnemyInstance') !== null;
  170. }
  171. /**
  172. * 检查场上是否仍有“真实存在”的敌人
  173. * 不受隐身影响:隐身敌人仍算存在;只有全部死亡/销毁才返回false
  174. */
  175. private hasAliveEnemies(): boolean {
  176. // 优先使用攻击状态管理器的敌人总数(包含隐身敌人)
  177. const mgr = EnemyAttackStateManager.getInstance();
  178. if (mgr && typeof mgr.getEnemyCount === 'function') {
  179. if (mgr.getEnemyCount() > 0) return true;
  180. }
  181. // 回退:扫描场景节点
  182. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  183. if (!enemyContainer) return false;
  184. const enemies = enemyContainer.children.filter(child => child.isValid && child.active && this.isEnemyNode(child));
  185. return enemies.length > 0;
  186. }
  187. update(dt: number) {
  188. if (!this.config || !this.state) return;
  189. this.state.elapsedTime += dt;
  190. // 记录路径(仅对回旋镖)
  191. this.recordFlightPath(dt);
  192. switch (this.config.type) {
  193. case 'straight':
  194. this.updateStraightTrajectory(dt);
  195. break;
  196. case 'homing':
  197. this.updateHomingTrajectory(dt);
  198. break;
  199. case 'guided':
  200. this.updateGuidedTrajectory(dt);
  201. break;
  202. case 'arc':
  203. this.updateArcTrajectory(dt);
  204. break;
  205. }
  206. }
  207. /**
  208. * 更新直线弹道
  209. */
  210. private updateStraightTrajectory(dt: number) {
  211. // 直线弹道保持恒定速度,主要由物理引擎处理
  212. const currentVel = this.rigidBody.linearVelocity;
  213. const targetSpeed = this.config.speed;
  214. // 确保速度保持恒定
  215. const currentSpeed = Math.sqrt(currentVel.x * currentVel.x + currentVel.y * currentVel.y);
  216. if (Math.abs(currentSpeed - targetSpeed) > 1) {
  217. const direction = new Vec2(currentVel.x, currentVel.y).normalize();
  218. this.rigidBody.linearVelocity = direction.multiplyScalar(targetSpeed);
  219. }
  220. }
  221. /**
  222. * 更新追踪弹道
  223. */
  224. private updateHomingTrajectory(dt: number) {
  225. // 追踪延迟后开始追踪
  226. if (this.homingTimer > 0) {
  227. this.homingTimer -= dt;
  228. return;
  229. }
  230. // 确定追踪目标位置(敌人节点 or 返回点)
  231. let targetPos: Vec3 = null;
  232. if (this.state.phase === 'return' && this.state.targetPosition) {
  233. // 回程阶段,使用存储的返回坐标
  234. targetPos = this.state.targetPosition;
  235. } else {
  236. // 主动追踪敌人
  237. if (!this.targetNode || !this.targetNode.isValid) {
  238. this.findTarget();
  239. }
  240. if (this.targetNode && this.targetNode.isValid) {
  241. targetPos = this.targetNode.worldPosition;
  242. }
  243. }
  244. // 若依旧没有目标,直接退出
  245. if (!targetPos) {
  246. return;
  247. }
  248. // 计算追踪力
  249. const currentPos = this.node.worldPosition;
  250. const direction = targetPos.clone().subtract(currentPos).normalize();
  251. // 获取当前速度
  252. const currentVel = this.rigidBody.linearVelocity;
  253. const currentVelVec3 = new Vec3(currentVel.x, currentVel.y, 0);
  254. // 线性插值追踪
  255. const homingForce = this.config.homingStrength * dt * 10;
  256. const targetVelocity = direction.multiplyScalar(this.config.speed);
  257. const newVelocity = currentVelVec3.lerp(targetVelocity, homingForce);
  258. this.rigidBody.linearVelocity = new Vec2(newVelocity.x, newVelocity.y);
  259. this.state.currentVelocity.set(newVelocity);
  260. }
  261. /**
  262. * 更新弧线弹道
  263. */
  264. private updateArcTrajectory(dt: number) {
  265. if (!this.arcDir || !this.arcTargetDir) return;
  266. // === 动态追踪目标 ===
  267. // 若当前没有有效目标,则尝试重新寻找
  268. if (!this.targetNode || !this.targetNode.isValid) {
  269. this.findTarget();
  270. // 若仍无目标且场上没有任何存活敌人,则立刻结束生命周期
  271. if ((!this.targetNode || !this.targetNode.isValid) && !this.hasAliveEnemies()) {
  272. const lifecycle = this.getComponent('BulletLifecycle') as any;
  273. if (this.rigidBody) {
  274. this.rigidBody.linearVelocity = new Vec2(0, 0);
  275. }
  276. if (lifecycle && typeof lifecycle.forceDestroy === 'function') {
  277. lifecycle.forceDestroy();
  278. } else {
  279. // 兜底:销毁节点
  280. if (this.node && this.node.isValid) this.node.destroy();
  281. }
  282. return;
  283. }
  284. }
  285. // 检查是否到达目标位置(爆炸武器和回旋镖都适用,但处理方式不同)
  286. const lifecycle = this.getComponent('BulletLifecycle') as any;
  287. const isBoomerang = lifecycle && lifecycle.getState && lifecycle.getState().phase !== undefined;
  288. if (this.state.targetPosition) {
  289. const currentPos = this.node.worldPosition;
  290. const distanceToTarget = Vec3.distance(currentPos, this.state.targetPosition);
  291. // 如果到达目标位置附近(50像素内)
  292. if (distanceToTarget <= 50) {
  293. if (isBoomerang) {
  294. // 回旋镖:触发命中效果但不爆炸,开始返回
  295. const hitEffect = this.getComponent('BulletHitEffect') as any;
  296. if (hitEffect && typeof hitEffect.processHit === 'function') {
  297. // 对目标位置附近的敌人造成伤害
  298. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  299. if (enemyContainer) {
  300. const enemies = enemyContainer.children.filter(child =>
  301. child.active && this.isEnemyNode(child)
  302. );
  303. // 寻找目标位置附近的敌人
  304. for (const enemy of enemies) {
  305. const distanceToEnemy = Vec3.distance(currentPos, enemy.worldPosition);
  306. if (distanceToEnemy <= 50) {
  307. hitEffect.processHit(enemy, currentPos);
  308. break; // 只命中一个敌人
  309. }
  310. }
  311. }
  312. }
  313. // 通知生命周期组件处理命中(回旋镖会开始返回而不是销毁)
  314. if (lifecycle && typeof lifecycle.onHit === 'function') {
  315. lifecycle.onHit(this.targetNode || this.node);
  316. }
  317. } else {
  318. // 爆炸武器:停止运动并触发爆炸
  319. if (this.rigidBody) {
  320. this.rigidBody.linearVelocity = new Vec2(0, 0);
  321. }
  322. // 先触发命中效果(包括音效播放)
  323. const hitEffect = this.getComponent('BulletHitEffect') as any;
  324. if (hitEffect && typeof hitEffect.processHit === 'function') {
  325. // 使用当前位置作为命中位置,传入子弹节点作为命中目标
  326. hitEffect.processHit(this.node, currentPos);
  327. }
  328. // 然后通知生命周期组件触发爆炸
  329. if (lifecycle && typeof lifecycle.onHit === 'function') {
  330. lifecycle.onHit(this.node); // 触发爆炸逻辑
  331. }
  332. return;
  333. }
  334. }
  335. }
  336. // 根据回旋镖状态决定目标方向
  337. if (isBoomerang && lifecycle.getState().phase === 'returning') {
  338. // 返回阶段:直接朝向目标位置(方块当前位置)
  339. if (this.state.targetPosition) {
  340. const pos = this.node.worldPosition;
  341. const dirToTarget = this.state.targetPosition.clone().subtract(pos).normalize();
  342. this.arcTargetDir.set(dirToTarget);
  343. // 检查是否到达目标位置
  344. const distanceToTarget = Vec3.distance(pos, this.state.targetPosition);
  345. if (distanceToTarget <= 30) {
  346. // 到达目标,销毁子弹
  347. if (lifecycle && typeof lifecycle.forceDestroy === 'function') {
  348. lifecycle.forceDestroy();
  349. }
  350. return;
  351. }
  352. }
  353. } else {
  354. // 主动追踪阶段:朝向敌人
  355. if (this.targetNode && this.targetNode.isValid) {
  356. const pos = this.node.worldPosition;
  357. const dirToTarget = this.targetNode.worldPosition.clone().subtract(pos).normalize();
  358. this.arcTargetDir.set(dirToTarget);
  359. // 对于回旋镖,检查是否足够接近敌人来触发命中
  360. if (isBoomerang) {
  361. const distanceToEnemy = Vec3.distance(pos, this.targetNode.worldPosition);
  362. if (distanceToEnemy <= 30) { // 30像素内算命中
  363. // 触发命中效果
  364. const hitEffect = this.getComponent('BulletHitEffect') as any;
  365. if (hitEffect && typeof hitEffect.processHit === 'function') {
  366. hitEffect.processHit(this.targetNode, pos);
  367. }
  368. // 通知生命周期组件处理命中
  369. if (lifecycle && typeof lifecycle.onHit === 'function') {
  370. lifecycle.onHit(this.targetNode);
  371. }
  372. }
  373. }
  374. // 更新目标位置为当前敌人位置
  375. this.state.targetPosition = this.targetNode.worldPosition.clone();
  376. } else if (!this.state.targetPosition) {
  377. // 如果没有目标且没有记录的目标位置,设置一个默认的前方位置
  378. const defaultTarget = this.node.worldPosition.clone().add(this.arcDir.clone().multiplyScalar(200));
  379. this.state.targetPosition = defaultTarget;
  380. }
  381. }
  382. // 根据与目标距离动态调整转向速率:越近转得越快,避免打圈
  383. let rotateFactor = (this.config.rotateSpeed ?? 0.5) * dt;
  384. if (isBoomerang && lifecycle.getState().phase === 'returning') {
  385. // 回旋镖返回阶段:根据距离调整转向速率,确保平滑返回
  386. if (this.state.targetPosition) {
  387. const distanceToTarget = Vec3.distance(this.node.worldPosition, this.state.targetPosition);
  388. // 根据距离动态调整转向速率,距离越近转向越快
  389. if (distanceToTarget < 50) {
  390. rotateFactor *= 4.0; // 非常接近目标点时快速转向
  391. } else if (distanceToTarget < 100) {
  392. rotateFactor *= 2.5; // 接近目标点时加快转向
  393. } else if (distanceToTarget < 200) {
  394. rotateFactor *= 1.8; // 中等距离时适度加快转向
  395. } else {
  396. rotateFactor *= 1.2; // 远距离时使用基础转向速率
  397. }
  398. }
  399. } else if (this.targetNode && this.targetNode.isValid) {
  400. // 主动追踪阶段:根据距离调整转向速率
  401. const distance = Vec3.distance(this.node.worldPosition, this.targetNode.worldPosition);
  402. rotateFactor *= (2000 / Math.max(distance, 50));
  403. }
  404. const newDir = new Vec3();
  405. Vec3.slerp(newDir, this.arcDir, this.arcTargetDir, Math.min(1, rotateFactor));
  406. this.arcDir.set(newDir);
  407. // 更新位移 / 速度
  408. if (this.rigidBody) {
  409. this.rigidBody.linearVelocity = new Vec2(this.arcDir.x * this.config.speed, this.arcDir.y * this.config.speed);
  410. } else {
  411. const displacement = this.arcDir.clone().multiplyScalar(this.config.speed * dt);
  412. this.node.worldPosition = this.node.worldPosition.add(displacement);
  413. }
  414. // 更新状态中的当前速度
  415. this.state.currentVelocity.set(this.arcDir.x * this.config.speed, this.arcDir.y * this.config.speed, 0);
  416. }
  417. /**
  418. * 更新 guided 轨迹:强导航、顺滑拐弯与目标切换
  419. */
  420. private updateGuidedTrajectory(dt: number) {
  421. if (!this.guidedDir || !this.guidedTargetDir) return;
  422. // 动态追踪与目标切换:优先当前锁定;失效或消失则寻找最近目标
  423. if (!this.targetNode || !this.targetNode.isValid) {
  424. this.findTarget();
  425. // 若仍无目标且场上没有任何存活敌人,则立刻结束生命周期
  426. if ((!this.targetNode || !this.targetNode.isValid) && !this.hasAliveEnemies()) {
  427. const lifecycle = this.getComponent('BulletLifecycle') as any;
  428. if (this.rigidBody) {
  429. this.rigidBody.linearVelocity = new Vec2(0, 0);
  430. }
  431. if (lifecycle && typeof lifecycle.forceDestroy === 'function') {
  432. lifecycle.forceDestroy();
  433. } else {
  434. if (this.node && this.node.isValid) this.node.destroy();
  435. }
  436. return;
  437. }
  438. }
  439. const currentPos = this.node.worldPosition;
  440. let targetPos: Vec3 | null = null;
  441. if (this.targetNode && this.targetNode.isValid) {
  442. targetPos = this.targetNode.worldPosition;
  443. } else if (this.state.targetPosition) {
  444. targetPos = this.state.targetPosition;
  445. }
  446. // 若仍无目标,保持原方向匀速飞行
  447. if (!targetPos) {
  448. if (this.rigidBody) {
  449. this.rigidBody.linearVelocity = new Vec2(this.guidedDir.x * this.config.speed, this.guidedDir.y * this.config.speed);
  450. }
  451. return;
  452. }
  453. // 期望方向(纯追踪)
  454. const dirToTarget = targetPos.clone().subtract(currentPos).normalize();
  455. const dist = Vec3.distance(currentPos, targetPos);
  456. // 动态平滑:远距离更平滑,近距离响应更快
  457. const t = this.state.elapsedTime;
  458. let smooth: number;
  459. if (dist > 350) {
  460. smooth = t < 0.6 ? 0.15 : 0.18; // 前期更柔和
  461. } else if (dist > 180) {
  462. smooth = 0.24; // 中段略增强响应
  463. } else if (dist > 90) {
  464. smooth = 0.36; // 接近时明显加快
  465. } else {
  466. smooth = 0.52; // 临近目标快速贴合
  467. }
  468. const blended = new Vec3(
  469. this.guidedTargetDir.x + (dirToTarget.x - this.guidedTargetDir.x) * smooth,
  470. this.guidedTargetDir.y + (dirToTarget.y - this.guidedTargetDir.y) * smooth,
  471. 0
  472. ).normalize();
  473. this.guidedTargetDir.set(blended);
  474. // 根据角误差与距离动态提升转向速率(构造“导弹投掷”前平滑、末段急拐”手感)
  475. const dot = Math.max(-1, Math.min(1, this.guidedDir.x * this.guidedTargetDir.x + this.guidedDir.y * this.guidedTargetDir.y));
  476. const angleErr = Math.acos(dot); // [0, π]
  477. const baseRotate = (this.config.rotateSpeed ?? 0.8) * dt;
  478. let turnScale: number;
  479. if (dist > 350) {
  480. turnScale = 0.35; // 远处很平滑
  481. } else if (dist > 180) {
  482. turnScale = 0.6; // 中段加速但仍平滑
  483. } else if (dist > 90) {
  484. turnScale = 0.9; // 接近目标时显著加快
  485. } else {
  486. turnScale = 1.5; // 末段急拐,形成导弹俯冲感觉
  487. }
  488. const angleScale = 0.8 + (angleErr / Math.PI) * 1.4;
  489. // 末段大角误差额外加成,增强“急转”质感
  490. const lateTurnBonus = (dist < 70 && angleErr > (50 * Math.PI / 180)) ? 1.6 : 1.0;
  491. let rotateFactor = baseRotate * turnScale * angleScale * lateTurnBonus;
  492. // 限制范围,确保数值稳定
  493. rotateFactor = Math.min(0.95, Math.max(0.015, rotateFactor));
  494. // 使用带符号角度的旋转以避免过度插值导致的打圈
  495. // 计算带符号角误差 [-π, π]
  496. const dotClamped = Math.max(-1, Math.min(1, this.guidedDir.x * this.guidedTargetDir.x + this.guidedDir.y * this.guidedTargetDir.y));
  497. const crossZ = this.guidedDir.x * this.guidedTargetDir.y - this.guidedDir.y * this.guidedTargetDir.x;
  498. const signedAngle = Math.atan2(crossZ, dotClamped);
  499. // 以 rotateFactor 作为“最大步进”,限制每帧转向幅度,避免来回过冲产生自转
  500. const maxTurn = Math.min(0.95, Math.max(0.01, rotateFactor)) * Math.PI; // 将插值因子映射到弧度步进
  501. const clampedTurn = Math.max(-maxTurn, Math.min(maxTurn, signedAngle));
  502. // 近距稳定锁定:很近且角误差很小,直接对准避免绕圈
  503. if (dist < 50 && Math.abs(signedAngle) < (12 * Math.PI / 180)) {
  504. this.guidedDir.set(this.guidedTargetDir);
  505. } else {
  506. const c = Math.cos(clampedTurn);
  507. const s = Math.sin(clampedTurn);
  508. const rotated = new Vec3(
  509. this.guidedDir.x * c - this.guidedDir.y * s,
  510. this.guidedDir.x * s + this.guidedDir.y * c,
  511. 0
  512. ).normalize();
  513. this.guidedDir.set(rotated);
  514. }
  515. // 更新速度
  516. if (this.rigidBody) {
  517. this.rigidBody.linearVelocity = new Vec2(this.guidedDir.x * this.config.speed, this.guidedDir.y * this.config.speed);
  518. } else {
  519. const displacement = this.guidedDir.clone().multiplyScalar(this.config.speed * dt);
  520. this.node.worldPosition = this.node.worldPosition.add(displacement);
  521. }
  522. this.state.currentVelocity.set(this.guidedDir.x * this.config.speed, this.guidedDir.y * this.config.speed, 0);
  523. // 近距命中(对齐爆炸型的处理)
  524. const lifecycle = this.getComponent('BulletLifecycle') as any;
  525. const hitEffect = this.getComponent('BulletHitEffect') as any;
  526. const isExplosion = hitEffect && typeof (hitEffect as any).hasExplosionEffect === 'function'
  527. ? (hitEffect as any).hasExplosionEffect()
  528. : false;
  529. if (this.targetNode && this.targetNode.isValid) {
  530. const hitRadius = 45;
  531. const d = Vec3.distance(currentPos, this.targetNode.worldPosition);
  532. if (d <= hitRadius) {
  533. if (this.rigidBody) this.rigidBody.linearVelocity = new Vec2(0, 0);
  534. if (isExplosion) {
  535. if (hitEffect && typeof hitEffect.processHit === 'function') {
  536. hitEffect.processHit(this.node, currentPos);
  537. }
  538. if (lifecycle && typeof lifecycle.onHit === 'function') {
  539. lifecycle.onHit(this.node);
  540. }
  541. } else {
  542. if (hitEffect && typeof hitEffect.processHit === 'function') {
  543. hitEffect.processHit(this.targetNode, currentPos);
  544. }
  545. if (lifecycle && typeof lifecycle.onHit === 'function') {
  546. lifecycle.onHit(this.targetNode);
  547. }
  548. }
  549. return;
  550. }
  551. } else {
  552. // 目标消失:立刻尝试找新目标并继续引导
  553. this.findTarget();
  554. }
  555. }
  556. /**
  557. * 获取当前速度
  558. */
  559. public getCurrentVelocity(): Vec3 {
  560. if (this.config?.type === 'arc') {
  561. return this.arcDir ? this.arcDir.clone().multiplyScalar(this.config.speed) : new Vec3();
  562. }
  563. const vel = this.rigidBody?.linearVelocity;
  564. if (!vel) return new Vec3();
  565. return new Vec3(vel.x, vel.y, 0);
  566. }
  567. /**
  568. * 获取弹道状态
  569. */
  570. public getState(): TrajectoryState {
  571. return this.state;
  572. }
  573. /**
  574. * 设置新的目标位置(用于回旋镖等)
  575. */
  576. public setTargetPosition(target: Vec3) {
  577. this.state.targetPosition = target.clone();
  578. }
  579. /**
  580. * 反转方向(用于回旋镖)
  581. */
  582. public reverseDirection() {
  583. const currentVel = this.rigidBody.linearVelocity;
  584. this.rigidBody.linearVelocity = new Vec2(-currentVel.x, -currentVel.y);
  585. this.state.currentVelocity.multiplyScalar(-1);
  586. this.state.phase = 'return';
  587. }
  588. /**
  589. * 改变方向(用于弹射)
  590. */
  591. public changeDirection(newDirection: Vec3) {
  592. // 归一化新方向
  593. const normalizedDir = newDirection.clone().normalize();
  594. // 保持当前速度大小
  595. const currentSpeed = this.config.speed;
  596. // 更新速度
  597. this.rigidBody.linearVelocity = new Vec2(
  598. normalizedDir.x * currentSpeed,
  599. normalizedDir.y * currentSpeed
  600. );
  601. // 更新状态
  602. this.state.currentVelocity.set(normalizedDir.x * currentSpeed, normalizedDir.y * currentSpeed, 0);
  603. }
  604. /**
  605. * 设置重力
  606. */
  607. public setGravity(gravity: number) {
  608. this.config.gravity = gravity;
  609. }
  610. /**
  611. * 记录飞行路径(仅对回旋镖)
  612. */
  613. private recordFlightPath(dt: number) {
  614. // 检查是否为回旋镖
  615. const lifecycle = this.getComponent('BulletLifecycle') as any;
  616. const isBoomerang = lifecycle && lifecycle.getState && lifecycle.getState().phase !== undefined;
  617. if (!isBoomerang) return;
  618. // 只在非返回阶段记录路径
  619. if (lifecycle.getState().phase === 'returning') return;
  620. // 检查是否到了记录时间
  621. this.state.lastRecordTime += dt * 1000; // 转换为毫秒
  622. if (this.state.lastRecordTime >= this.state.pathRecordInterval) {
  623. const currentPos = this.node.worldPosition.clone();
  624. // 避免记录重复位置
  625. const lastPos = this.state.flightPath[this.state.flightPath.length - 1];
  626. const distance = Vec3.distance(currentPos, lastPos);
  627. if (distance > 10) { // 只有移动距离超过10像素才记录
  628. this.state.flightPath.push(currentPos);
  629. this.state.lastRecordTime = 0;
  630. }
  631. }
  632. }
  633. /**
  634. * 获取返回路径中的下一个目标点
  635. */
  636. private getNextReturnTarget(): Vec3 | null {
  637. if (this.state.flightPath.length === 0) return null;
  638. // 如果还没有开始返回路径,初始化索引
  639. if (this.state.returnPathIndex === -1) {
  640. this.state.returnPathIndex = this.state.flightPath.length - 1;
  641. }
  642. // 检查当前位置是否接近目标点
  643. if (this.state.returnPathIndex >= 0) {
  644. const targetPos = this.state.flightPath[this.state.returnPathIndex];
  645. const currentPos = this.node.worldPosition;
  646. const distance = Vec3.distance(currentPos, targetPos);
  647. // 动态调整接近距离:路径点越少,接近距离越大,避免过于严格的路径跟随
  648. const approachDistance = Math.max(25, Math.min(50, 200 / this.state.flightPath.length));
  649. // 如果接近当前目标点,移动到下一个点
  650. if (distance <= approachDistance) {
  651. this.state.returnPathIndex--;
  652. }
  653. // 返回当前目标点,如果有多个路径点则进行平滑插值
  654. if (this.state.returnPathIndex >= 0) {
  655. const currentTarget = this.state.flightPath[this.state.returnPathIndex];
  656. // 如果还有下一个路径点,进行平滑插值
  657. if (this.state.returnPathIndex > 0) {
  658. const nextTarget = this.state.flightPath[this.state.returnPathIndex - 1];
  659. const progress = Math.min(1, (approachDistance - distance) / approachDistance);
  660. if (progress > 0) {
  661. // 在当前目标点和下一个目标点之间进行插值
  662. const smoothTarget = new Vec3();
  663. Vec3.lerp(smoothTarget, currentTarget, nextTarget, progress * 0.3);
  664. return smoothTarget;
  665. }
  666. }
  667. return currentTarget;
  668. }
  669. }
  670. // 如果已经遍历完所有路径点,返回发射源方块的当前位置
  671. if (this.state.sourceBlock && this.state.sourceBlock.isValid) {
  672. // 动态获取方块的当前世界位置
  673. return this.state.sourceBlock.worldPosition.clone();
  674. }
  675. // 如果方块节点无效,回退到起始位置
  676. return this.state.startPosition;
  677. }
  678. /**
  679. * 获取动态返回目标位置(方块当前位置)
  680. */
  681. public getDynamicReturnTarget(): Vec3 | null {
  682. if (this.state.sourceBlock && this.state.sourceBlock.isValid) {
  683. return this.state.sourceBlock.worldPosition.clone();
  684. }
  685. return null;
  686. }
  687. /**
  688. * 为返回阶段重新初始化轨道
  689. * @param currentPos 当前位置(新的发射点)
  690. * @param targetPos 目标位置(方块位置)
  691. */
  692. public reinitializeForReturn(currentPos: Vec3, targetPos: Vec3) {
  693. if (!this.config || !this.state) return;
  694. // 清除当前所有速度和运动
  695. if (this.rigidBody) {
  696. this.rigidBody.linearVelocity = new Vec2(0, 0);
  697. this.rigidBody.angularVelocity = 0;
  698. }
  699. // 计算从当前位置到目标位置的方向
  700. const direction = targetPos.clone().subtract(currentPos).normalize();
  701. // 重新设置状态
  702. this.state.startPosition = currentPos.clone();
  703. this.state.targetPosition = targetPos.clone();
  704. this.state.initialVelocity = direction.clone().multiplyScalar(this.config.speed);
  705. this.state.currentVelocity = direction.clone().multiplyScalar(this.config.speed);
  706. this.state.elapsedTime = 0;
  707. // 重新初始化弧线轨道参数
  708. if (this.config.type === 'arc') {
  709. this.arcDir = direction.clone();
  710. this.arcTargetDir = direction.clone();
  711. }
  712. // 清除路径记录,开始新的返回轨道
  713. this.state.flightPath = [currentPos.clone()];
  714. this.state.returnPathIndex = -1;
  715. this.state.lastRecordTime = 0;
  716. console.log(`[BulletTrajectory] 重新初始化返回轨道: ${currentPos} -> ${targetPos}`);
  717. }
  718. /**
  719. * 验证配置
  720. */
  721. public static validateConfig(config: BulletTrajectoryConfig): boolean {
  722. if (!config) return false;
  723. if (config.speed <= 0) return false;
  724. if (config.gravity < 0) return false;
  725. if (config.homingStrength < 0 || config.homingStrength > 1) return false;
  726. if (config.homingDelay < 0) return false;
  727. if (config.rotateSpeed !== undefined && (config.rotateSpeed < 0 || config.rotateSpeed > 1)) return false;
  728. return true;
  729. }
  730. }