BulletHitEffect.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. import { _decorator, Component, Node, Vec3, find, instantiate, Prefab, UITransform, resources, sp, CircleCollider2D, Contact2DType, Collider2D, IPhysics2DContact } from 'cc';
  2. import { BulletTrajectory } from './BulletTrajectory';
  3. import { HitEffectConfig } from '../../Core/ConfigManager';
  4. import { PersistentSkillManager } from '../../FourUI/SkillSystem/PersistentSkillManager';
  5. import { BurnEffect } from './BurnEffect';
  6. import { GroundBurnArea } from './GroundBurnArea';
  7. import { GroundBurnAreaManager } from './GroundBurnAreaManager';
  8. import { WeaponBullet } from '../WeaponBullet';
  9. import EventBus, { GameEvents } from '../../Core/EventBus';
  10. import { Audio } from '../../AudioManager/AudioManager';
  11. const { ccclass, property } = _decorator;
  12. /**
  13. * 命中效果控制器
  14. * 负责处理可叠加的命中效果
  15. */
  16. // 接口定义已移至ConfigManager.ts中的HitEffectConfig
  17. export interface HitResult {
  18. shouldDestroy: boolean; // 是否应该销毁子弹
  19. shouldContinue: boolean; // 是否应该继续飞行
  20. shouldRicochet: boolean; // 是否应该弹射
  21. damageDealt: number; // 造成的伤害
  22. }
  23. @ccclass('BulletHitEffect')
  24. export class BulletHitEffect extends Component {
  25. private hitEffects: HitEffectConfig[] = [];
  26. private hitCount: number = 0;
  27. private pierceCount: number = 0;
  28. private ricochetCount: number = 0;
  29. private activeBurnAreas: Node[] = [];
  30. // 检测范围相关
  31. private detectionCollider: CircleCollider2D | null = null;
  32. private detectedEnemies: Set<Node> = new Set();
  33. // 锯齿草子弹状态管理
  34. private hasFirstHit: boolean = false; // 是否已经首次击中敌人
  35. private currentTargetEnemy: Node | null = null; // 当前击中的目标敌人
  36. // 默认特效路径(从WeaponBullet传入)
  37. private defaultHitEffectPath: string = '';
  38. private defaultTrailEffectPath: string = '';
  39. private defaultBurnEffectPath: string = '';
  40. public setDefaultEffects(hit: string | null, trail?: string | null, burn?: string | null) {
  41. if (hit) this.defaultHitEffectPath = hit;
  42. if (trail) this.defaultTrailEffectPath = trail;
  43. if (burn) this.defaultBurnEffectPath = burn;
  44. }
  45. /**
  46. * 初始化命中效果
  47. */
  48. public init(effects: HitEffectConfig[]) {
  49. // 按优先级排序
  50. this.hitEffects = [...effects].sort((a, b) => a.priority - b.priority);
  51. // 重置锯齿草状态
  52. this.resetSawGrassState();
  53. // 检查是否有手动添加的检测范围碰撞器
  54. this.setupDetectionColliderEvents();
  55. }
  56. /**
  57. * 处理命中事件
  58. */
  59. public processHit(hitNode: Node, contactPos: Vec3): HitResult {
  60. // 检查是否是弹射后的命中
  61. const isRicochetHit = this.ricochetCount > 0;
  62. const hitType = isRicochetHit ? '弹射命中' : '直接命中';
  63. // 锯齿草子弹首次击中逻辑
  64. const weaponBullet = this.getComponent(WeaponBullet);
  65. const weaponInfo = weaponBullet ? weaponBullet.getWeaponInfo() : null;
  66. const weaponId = weaponInfo ? weaponInfo.getWeaponId() : null;
  67. if (weaponId === 'saw_grass' && !this.hasFirstHit && this.isEnemyNode(hitNode)) {
  68. this.hasFirstHit = true;
  69. this.currentTargetEnemy = hitNode;
  70. }
  71. this.hitCount++;
  72. const result: HitResult = {
  73. shouldDestroy: false,
  74. shouldContinue: false,
  75. shouldRicochet: false,
  76. damageDealt: 0
  77. };
  78. // 按优先级处理所有效果
  79. for (const effect of this.hitEffects) {
  80. const effectResult = this.processEffect(effect, hitNode, contactPos);
  81. // 累积伤害
  82. result.damageDealt += effectResult.damageDealt;
  83. // 处理控制逻辑(OR逻辑,任何一个效果要求的行为都会执行)
  84. if (effectResult.shouldDestroy) result.shouldDestroy = true;
  85. if (effectResult.shouldContinue) result.shouldContinue = true;
  86. if (effectResult.shouldRicochet) result.shouldRicochet = true;
  87. }
  88. // 逻辑优先级:销毁 > 弹射 > 继续
  89. if (result.shouldDestroy) {
  90. result.shouldContinue = false;
  91. result.shouldRicochet = false;
  92. } else if (result.shouldRicochet) {
  93. result.shouldContinue = false;
  94. }
  95. return result;
  96. }
  97. /**
  98. * 处理单个效果
  99. */
  100. private processEffect(effect: HitEffectConfig, hitNode: Node, contactPos: Vec3): HitResult {
  101. const result: HitResult = {
  102. shouldDestroy: false,
  103. shouldContinue: false,
  104. shouldRicochet: false,
  105. damageDealt: 0
  106. };
  107. switch (effect.type) {
  108. case 'normal_damage':
  109. result.damageDealt = this.processNormalDamage(effect, hitNode);
  110. result.shouldDestroy = true;
  111. break;
  112. case 'pierce_damage':
  113. result.damageDealt = this.processPierceDamage(effect, hitNode);
  114. break;
  115. case 'explosion':
  116. result.damageDealt = this.processExplosion(effect, contactPos);
  117. result.shouldDestroy = true;
  118. break;
  119. case 'ground_burn':
  120. this.processGroundBurn(effect, hitNode);
  121. break;
  122. case 'ricochet_damage':
  123. // 先判断是否可以弹射(在递增ricochetCount之前)
  124. const canRicochet = this.ricochetCount < (effect.ricochetCount || 0);
  125. result.damageDealt = this.processRicochetDamage(effect, hitNode);
  126. result.shouldRicochet = canRicochet;
  127. break;
  128. }
  129. return result;
  130. }
  131. /**
  132. * 处理普通伤害
  133. */
  134. private processNormalDamage(effect: any, hitNode: Node): number {
  135. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  136. const weaponBullet = this.getComponent(WeaponBullet);
  137. const damage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  138. this.damageEnemy(hitNode, damage);
  139. this.spawnHitEffect(hitNode.worldPosition);
  140. return damage;
  141. }
  142. /**
  143. * 处理穿透伤害
  144. */
  145. private processPierceDamage(effect: HitEffectConfig, hitNode: Node): number {
  146. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  147. const weaponBullet = this.getComponent(WeaponBullet);
  148. const damage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  149. this.damageEnemy(hitNode, damage);
  150. this.spawnHitEffect(hitNode.worldPosition);
  151. this.pierceCount++;
  152. return damage;
  153. }
  154. /**
  155. * 处理爆炸效果
  156. */
  157. private processExplosion(effect: HitEffectConfig, position: Vec3): number {
  158. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  159. const weaponBullet = this.getComponent(WeaponBullet);
  160. const explosionDamage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  161. const scheduleExplosion = () => {
  162. // 播放爆炸音效
  163. this.playAttackSound();
  164. // 生成爆炸特效
  165. this.spawnExplosionEffect(position);
  166. // 对范围内敌人造成伤害
  167. const damage = this.damageEnemiesInRadius(position, effect.radius, explosionDamage);
  168. return damage;
  169. };
  170. // 立即爆炸,不使用任何延迟
  171. return scheduleExplosion();
  172. }
  173. /**
  174. * 处理地面燃烧区域效果
  175. */
  176. private processGroundBurn(effect: HitEffectConfig, hitNode: Node) {
  177. // 获取地面燃烧区域管理器
  178. const burnAreaManager = GroundBurnAreaManager.getInstance();
  179. if (!burnAreaManager) {
  180. console.error('[BulletHitEffect] 无法获取GroundBurnAreaManager实例');
  181. return;
  182. }
  183. // 获取子弹的世界位置作为燃烧区域中心
  184. const burnPosition = this.node.worldPosition.clone();
  185. // 如果命中的是敌人,使用敌人的位置
  186. if (this.isEnemyNode(hitNode)) {
  187. burnPosition.set(hitNode.worldPosition);
  188. }
  189. try {
  190. // 获取武器子弹组件
  191. const weaponBullet = this.getComponent(WeaponBullet);
  192. // 通过管理器创建燃烧区域
  193. const burnAreaNode = burnAreaManager.createGroundBurnArea(
  194. burnPosition,
  195. effect,
  196. weaponBullet,
  197. this.defaultBurnEffectPath || this.defaultTrailEffectPath
  198. );
  199. if (burnAreaNode) {
  200. // 将燃烧区域添加到活跃列表中
  201. this.activeBurnAreas.push(burnAreaNode);
  202. }
  203. } catch (error) {
  204. console.error('[BulletHitEffect] 通过管理器创建地面燃烧区域失败:', error);
  205. }
  206. }
  207. /**
  208. * 处理弹射伤害
  209. */
  210. private processRicochetDamage(effect: HitEffectConfig, hitNode: Node): number {
  211. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  212. const weaponBullet = this.getComponent(WeaponBullet);
  213. const damage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  214. this.damageEnemy(hitNode, damage);
  215. // 检查是否还能继续弹射
  216. if (this.ricochetCount < effect.ricochetCount) {
  217. this.ricochetCount++;
  218. // 计算弹射方向
  219. this.calculateRicochetDirection(effect.ricochetAngle);
  220. } else {
  221. //console.log(`[BulletHitEffect] 弹射次数已达上限,不再弹射`);
  222. }
  223. return damage;
  224. }
  225. /**
  226. * 计算弹射方向
  227. */
  228. private calculateRicochetDirection(maxAngle: number) {
  229. const trajectory = this.getComponent(BulletTrajectory);
  230. if (!trajectory) return;
  231. // 获取武器子弹组件来确定检测范围
  232. const weaponBullet = this.getComponent(WeaponBullet);
  233. const weaponInfo = weaponBullet ? weaponBullet.getWeaponInfo() : null;
  234. const weaponId = weaponInfo ? weaponInfo.getWeaponId() : null;
  235. const detectionRange = weaponId ? this.getDetectionRange(weaponId) : 500;
  236. let nearestEnemy: Node | null = null;
  237. // 锯齿草武器使用专门的CircleCollider2D实时追踪逻辑
  238. if (weaponId === 'saw_grass') {
  239. nearestEnemy = this.findNearestEnemyForSawGrass(detectionRange);
  240. } else {
  241. // 其他武器使用原有的实时计算方法
  242. nearestEnemy = this.findNearestEnemy(detectionRange);
  243. }
  244. let newDirection: Vec3;
  245. if (nearestEnemy) {
  246. // 如果找到敌人,计算朝向敌人的方向
  247. const bulletPos = this.node.worldPosition;
  248. const enemyPos = nearestEnemy.worldPosition;
  249. // 计算基础方向(朝向敌人)
  250. const baseDirection = enemyPos.clone().subtract(bulletPos).normalize();
  251. // 添加随机偏移角度,让弹射不那么精确
  252. const angleRad = (Math.random() - 0.5) * maxAngle * Math.PI / 180;
  253. const cos = Math.cos(angleRad);
  254. const sin = Math.sin(angleRad);
  255. // 应用旋转偏移
  256. newDirection = new Vec3(
  257. baseDirection.x * cos - baseDirection.y * sin,
  258. baseDirection.x * sin + baseDirection.y * cos,
  259. 0
  260. ).normalize();
  261. const weaponType = weaponId === 'saw_grass' ? '锯齿草CircleCollider2D' : '实时计算';
  262. } else {
  263. // 如果没有找到敌人,使用随机方向(保持原有逻辑)
  264. const currentVel = trajectory.getCurrentVelocity();
  265. const angleRad = (Math.random() - 0.5) * maxAngle * Math.PI / 180;
  266. const cos = Math.cos(angleRad);
  267. const sin = Math.sin(angleRad);
  268. newDirection = new Vec3(
  269. currentVel.x * cos - currentVel.y * sin,
  270. currentVel.x * sin + currentVel.y * cos,
  271. 0
  272. ).normalize();
  273. const weaponType = weaponId === 'saw_grass' ? '锯齿草CircleCollider2D' : '实时计算';
  274. }
  275. // 使用弹道组件的changeDirection方法
  276. trajectory.changeDirection(newDirection);
  277. }
  278. /**
  279. * 对单个敌人造成伤害
  280. */
  281. private damageEnemy(enemyNode: Node, damage: number) {
  282. if (!this.isEnemyNode(enemyNode)) return;
  283. // 播放攻击音效
  284. this.playAttackSound();
  285. // 计算暴击伤害和暴击状态
  286. const damageResult = this.calculateCriticalDamage(damage);
  287. // 使用applyDamageToEnemy方法,通过EventBus发送伤害事件
  288. this.applyDamageToEnemy(enemyNode, damageResult.damage, damageResult.isCritical);
  289. }
  290. /**
  291. * 计算暴击伤害
  292. */
  293. private calculateCriticalDamage(baseDamage: number): { damage: number, isCritical: boolean } {
  294. // 获取武器子弹组件来计算暴击
  295. const weaponBullet = this.getComponent(WeaponBullet);
  296. if (!weaponBullet) {
  297. // 如果没有WeaponBullet组件,使用基础暴击率10%
  298. const critChance = 0.1;
  299. const isCritical = Math.random() < critChance;
  300. if (isCritical) {
  301. // 暴击伤害 = 基础伤害 × (1 + 暴击伤害倍率),默认暴击倍率100%
  302. const critDamage = Math.round(baseDamage * (1 + 1.0)); // 四舍五入为整数
  303. this.showCriticalHitEffect();
  304. return { damage: critDamage, isCritical: true };
  305. }
  306. return { damage: baseDamage, isCritical: false };
  307. }
  308. // 获取暴击率
  309. const critChance = weaponBullet.getCritChance();
  310. // 检查是否触发暴击
  311. const isCritical = Math.random() < critChance;
  312. if (isCritical) {
  313. // 获取暴击伤害倍率(从技能系统)
  314. const critDamageMultiplier = this.getCritDamageMultiplier();
  315. // 暴击伤害 = 基础伤害 × (1 + 暴击伤害倍率)
  316. const critDamage = Math.round(baseDamage * (1 + critDamageMultiplier)); // 四舍五入为整数
  317. // 显示暴击特效
  318. this.showCriticalHitEffect();
  319. return { damage: critDamage, isCritical: true };
  320. }
  321. return { damage: baseDamage, isCritical: false };
  322. }
  323. /**
  324. * 获取暴击伤害倍率
  325. */
  326. private getCritDamageMultiplier(): number {
  327. // 从局外技能系统获取暴击伤害加成
  328. const persistentSkillManager = PersistentSkillManager.getInstance();
  329. if (persistentSkillManager) {
  330. const bonuses = persistentSkillManager.getSkillBonuses();
  331. // critDamageBonus是百分比值,需要转换为倍率
  332. // 暴击伤害倍率 = 基础100% + 技能加成百分比
  333. return 1.0 + (bonuses.critDamageBonus || 0) / 100;
  334. }
  335. // 默认暴击倍率100%(即2倍伤害)
  336. return 1.0;
  337. }
  338. /**
  339. * 播放攻击音效
  340. */
  341. private playAttackSound() {
  342. try {
  343. // 获取武器子弹组件
  344. const weaponBullet = this.getComponent(WeaponBullet);
  345. if (!weaponBullet) {
  346. console.log('[BulletHitEffect] 未找到WeaponBullet组件,无法播放攻击音效');
  347. return;
  348. }
  349. // 获取武器配置
  350. const weaponConfig = weaponBullet.getWeaponConfig();
  351. if (!weaponConfig || !weaponConfig.visualConfig || !weaponConfig.visualConfig.attackSound) {
  352. console.log('[BulletHitEffect] 武器配置中未找到attackSound,跳过音效播放');
  353. return;
  354. }
  355. // 播放攻击音效
  356. const attackSoundPath = weaponConfig.visualConfig.attackSound;
  357. Audio.playWeaponSound(attackSoundPath);
  358. } catch (error) {
  359. console.error('[BulletHitEffect] 播放攻击音效时出错:', error);
  360. }
  361. }
  362. /**
  363. * 显示暴击特效
  364. */
  365. private showCriticalHitEffect() {
  366. // 这里可以添加暴击特效,比如特殊的粒子效果或音效
  367. // 暂时只在控制台输出,后续可以扩展
  368. console.log('[BulletHitEffect] 暴击特效触发!');
  369. }
  370. /**
  371. * 对范围内敌人造成伤害
  372. */
  373. private damageEnemiesInRadius(center: Vec3, radius: number, damage: number): number {
  374. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  375. if (!enemyContainer) return 0;
  376. let totalDamage = 0;
  377. const enemies = enemyContainer.children.filter(child =>
  378. child.active && this.isEnemyNode(child)
  379. );
  380. // 对于持续伤害(如地面灼烧),直接使用传入的伤害值
  381. // 对于爆炸伤害,使用WeaponBullet的最终伤害值
  382. const baseDamage = damage;
  383. console.log(`[BulletHitEffect] 范围伤害 - 中心位置: (${center.x.toFixed(1)}, ${center.y.toFixed(1)}), 半径: ${radius}, 基础伤害: ${baseDamage}`);
  384. for (const enemy of enemies) {
  385. const distance = Vec3.distance(center, enemy.worldPosition);
  386. if (distance <= radius) {
  387. // 每个敌人独立计算暴击
  388. const damageResult = this.calculateCriticalDamage(baseDamage);
  389. console.log(`[BulletHitEffect] 敌人受到范围伤害 - 距离: ${distance.toFixed(1)}, 伤害: ${damageResult.damage}, 暴击: ${damageResult.isCritical}`);
  390. // 直接处理伤害,避免重复计算暴击
  391. this.applyDamageToEnemy(enemy, damageResult.damage, damageResult.isCritical);
  392. totalDamage += damageResult.damage;
  393. }
  394. }
  395. return totalDamage;
  396. }
  397. /**
  398. * 直接对敌人应用伤害(不进行暴击计算)
  399. */
  400. private applyDamageToEnemy(enemyNode: Node, damage: number, isCritical: boolean = false) {
  401. console.log(`[BulletHitEffect] 通过EventBus发送伤害事件: ${damage}, 暴击: ${isCritical}, 敌人节点: ${enemyNode.name}`);
  402. if (!this.isEnemyNode(enemyNode)) {
  403. console.log(`[BulletHitEffect] 节点不是敌人,跳过伤害`);
  404. return;
  405. }
  406. // 通过EventBus发送伤害事件
  407. const eventBus = EventBus.getInstance();
  408. const damageData = {
  409. enemyNode: enemyNode,
  410. damage: damage,
  411. isCritical: isCritical,
  412. source: 'BulletHitEffect'
  413. };
  414. console.log(`[BulletHitEffect] 发送APPLY_DAMAGE_TO_ENEMY事件`, damageData);
  415. eventBus.emit(GameEvents.APPLY_DAMAGE_TO_ENEMY, damageData);
  416. }
  417. /**
  418. * 判断是否为敌人节点
  419. */
  420. private isEnemyNode(node: Node): boolean {
  421. if (!node || !node.isValid) {
  422. return false;
  423. }
  424. // 检查是否为EnemySprite子节点
  425. if (node.name === 'EnemySprite' && node.parent) {
  426. return node.parent.getComponent('EnemyInstance') !== null;
  427. }
  428. // 兼容旧的敌人检测逻辑
  429. const name = node.name.toLowerCase();
  430. return name.includes('enemy') ||
  431. name.includes('敌人');
  432. }
  433. /**
  434. * 获取武器的敌人检测范围
  435. */
  436. private getDetectionRange(weaponId: string): number {
  437. // 根据武器类型设置不同的检测范围
  438. const detectionRanges: { [key: string]: number } = {
  439. 'saw_grass': 500, // 锯齿草:500范围智能弹射
  440. 'sharp_carrot': 300, // 尖胡萝卜:较短范围
  441. 'pea_shooter': 200, // 毛豆射手:基础范围
  442. // 可以根据需要添加更多武器的检测范围
  443. };
  444. return detectionRanges[weaponId] || 300; // 默认300范围
  445. }
  446. /**
  447. * 寻找最近的敌人(在指定范围内)
  448. */
  449. /**
  450. * 锯齿草武器专用的敌人查找方法 - 基于CircleCollider2D检测并实时追踪
  451. */
  452. private findNearestEnemyForSawGrass(maxRange: number = 500): Node | null {
  453. // 如果还没有首次击中敌人,不使用CircleCollider检测到的敌人
  454. if (!this.hasFirstHit) {
  455. console.log(`⏳ [锯齿草弹射] 尚未首次击中敌人,暂不启用CircleCollider追踪`);
  456. return null;
  457. }
  458. // 清理无效的敌人节点
  459. const invalidEnemies = new Set<Node>();
  460. for (const enemy of this.detectedEnemies) {
  461. if (!enemy || !enemy.isValid || !enemy.active) {
  462. invalidEnemies.add(enemy);
  463. }
  464. }
  465. // 移除无效敌人
  466. for (const invalidEnemy of invalidEnemies) {
  467. this.detectedEnemies.delete(invalidEnemy);
  468. }
  469. if (this.detectedEnemies.size === 0) {
  470. return null;
  471. }
  472. const currentPos = this.node.worldPosition;
  473. let nearestEnemy: Node | null = null;
  474. let nearestDistance = Infinity;
  475. // 实时计算每个检测到敌人的当前位置,排除当前目标敌人
  476. for (const enemy of this.detectedEnemies) {
  477. if (enemy && enemy.isValid && enemy.active) {
  478. // 排除当前击中的目标敌人
  479. if (this.currentTargetEnemy && enemy === this.currentTargetEnemy) {
  480. continue;
  481. }
  482. // 获取敌人的实时位置
  483. const enemyCurrentPos = enemy.worldPosition;
  484. const distance = Vec3.distance(currentPos, enemyCurrentPos);
  485. // 检查是否在范围内且是最近的
  486. if (distance <= maxRange && distance < nearestDistance) {
  487. nearestDistance = distance;
  488. nearestEnemy = enemy;
  489. }
  490. }
  491. }
  492. if (nearestEnemy) {
  493. } else {
  494. console.log(`❌ [锯齿草弹射] 检测到的敌人都超出范围或无效,或都是当前目标敌人`);
  495. }
  496. return nearestEnemy;
  497. }
  498. /**
  499. * 通用的敌人查找方法 - 用于非锯齿草武器
  500. */
  501. private findNearestEnemy(maxRange: number = 500): Node | null {
  502. // 回退到原有的实时计算方法(用于非锯齿草武器)
  503. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  504. if (!enemyContainer) return null;
  505. const enemies = enemyContainer.children.filter(child => {
  506. if (!child.active) return false;
  507. const nameLower = child.name.toLowerCase();
  508. if (nameLower.includes('enemy') || nameLower.includes('敌人')) return true;
  509. if (child.getComponent('EnemyInstance')) return true;
  510. return false;
  511. });
  512. if (enemies.length === 0) return null;
  513. let nearest: Node = null;
  514. let nearestDist = Infinity;
  515. const bulletPos = this.node.worldPosition;
  516. for (const enemy of enemies) {
  517. const dist = Vec3.distance(bulletPos, enemy.worldPosition);
  518. // 只考虑在检测范围内的敌人
  519. if (dist <= maxRange && dist < nearestDist) {
  520. nearestDist = dist;
  521. nearest = enemy;
  522. }
  523. }
  524. return nearest;
  525. }
  526. /**
  527. * 生成爆炸特效
  528. */
  529. private spawnExplosionEffect(position: Vec3) {
  530. const path = this.defaultHitEffectPath || 'Animation/WeaponTx/tx0004/tx0004';
  531. this.spawnEffect(path, position, false);
  532. }
  533. /**
  534. * 生成灼烧特效
  535. */
  536. private spawnBurnEffect(parent: Node) {
  537. const path = this.defaultBurnEffectPath || this.defaultTrailEffectPath || 'Animation/WeaponTx/tx0006/tx0006';
  538. // 使用回调函数处理异步创建的特效节点
  539. this.spawnEffect(path, new Vec3(), true, parent, (effectNode) => {
  540. // 将特效节点传递给 BurnEffect 组件,以便在停止时清理
  541. const burnEffect = parent.getComponent(BurnEffect);
  542. if (burnEffect && effectNode) {
  543. burnEffect.setBurnEffectNode(effectNode);
  544. }
  545. });
  546. }
  547. /**
  548. * 生成地面燃烧特效
  549. */
  550. private spawnGroundBurnEffect(parent: Node) {
  551. const path = this.defaultBurnEffectPath || this.defaultTrailEffectPath || 'Animation/WeaponTx/tx0006/tx0006';
  552. // 使用回调函数处理异步创建的特效节点
  553. this.spawnEffect(path, new Vec3(), true, parent, (effectNode) => {
  554. // 将特效节点传递给 GroundBurnArea 组件,以便在停止时清理
  555. const groundBurnArea = parent.getComponent(GroundBurnArea);
  556. if (groundBurnArea && effectNode) {
  557. groundBurnArea.setBurnEffectNode(effectNode);
  558. }
  559. });
  560. }
  561. /**
  562. * 生成特效
  563. */
  564. private spawnEffect(path: string, worldPos: Vec3, loop = false, parent?: Node, onCreated?: (effectNode: Node) => void): void {
  565. if (!path) return;
  566. const spawnWithData = (skData: sp.SkeletonData) => {
  567. const effectNode = new Node('Effect');
  568. const skeletonComp: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
  569. skeletonComp.skeletonData = skData;
  570. skeletonComp.setAnimation(0, 'animation', loop);
  571. const targetParent = parent || find('Canvas/GameLevelUI/enemyContainer') || find('Canvas');
  572. if (targetParent) {
  573. targetParent.addChild(effectNode);
  574. if (parent) {
  575. effectNode.setPosition(0, 0, 0);
  576. } else {
  577. const parentTrans = targetParent.getComponent(UITransform);
  578. if (parentTrans) {
  579. const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
  580. effectNode.position = localPos;
  581. }
  582. }
  583. }
  584. if (!loop) {
  585. skeletonComp.setCompleteListener(() => {
  586. effectNode.destroy();
  587. });
  588. }
  589. // 调用回调函数,传递创建的特效节点
  590. if (onCreated) {
  591. onCreated(effectNode);
  592. }
  593. };
  594. // 先尝试直接加载给定路径
  595. resources.load(path, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
  596. if (err) {
  597. console.warn('加载特效失败:', path, err);
  598. return;
  599. }
  600. spawnWithData(skData);
  601. });
  602. }
  603. /**
  604. * 清理资源
  605. */
  606. onDestroy() {
  607. // 不再强制销毁燃烧区域,让它们按照自己的持续时间自然销毁
  608. // 燃烧区域现在由GroundBurnAreaManager统一管理,有自己的生命周期
  609. // 只清空引用,不销毁燃烧区域节点
  610. this.activeBurnAreas = [];
  611. // 清理检测碰撞器事件监听
  612. if (this.detectionCollider) {
  613. this.detectionCollider.off(Contact2DType.BEGIN_CONTACT, this.onDetectionEnter, this);
  614. this.detectionCollider.off(Contact2DType.END_CONTACT, this.onDetectionExit, this);
  615. this.detectionCollider = null;
  616. }
  617. // 清理检测到的敌人集合
  618. this.detectedEnemies.clear();
  619. // 重置锯齿草状态
  620. this.resetSawGrassState();
  621. }
  622. /**
  623. * 设置检测范围碰撞器事件监听(用于手动添加的CircleCollider2D)
  624. */
  625. private setupDetectionColliderEvents() {
  626. // 首先在当前节点查找CircleCollider2D组件
  627. let colliders = this.getComponents(CircleCollider2D);
  628. // 如果当前节点没有找到,则在子节点中递归查找
  629. if (colliders.length === 0) {
  630. colliders = this.getComponentsInChildren(CircleCollider2D);
  631. }
  632. // 找到用作检测范围的CircleCollider2D(通常是sensor模式的)
  633. for (const collider of colliders) {
  634. if (collider.sensor) { // 只要是sensor模式的CircleCollider2D就认为是检测范围碰撞器
  635. this.detectionCollider = collider;
  636. // 启用碰撞监听 - Cocos Creator 3.8.6正确的事件类型
  637. this.detectionCollider.on(Contact2DType.BEGIN_CONTACT, this.onDetectionEnter, this);
  638. this.detectionCollider.on(Contact2DType.END_CONTACT, this.onDetectionExit, this);
  639. break;
  640. }
  641. }
  642. if (!this.detectionCollider) {
  643. console.log(`[BulletHitEffect] 未找到检测范围碰撞器,将使用实时计算方式`);
  644. }
  645. }
  646. /**
  647. * 检测范围进入事件
  648. */
  649. private onDetectionEnter(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  650. const otherNode = otherCollider.node;
  651. if (this.isEnemyNode(otherNode)) {
  652. // 获取武器信息,确认是否为锯齿草武器
  653. const weaponBullet = this.getComponent(WeaponBullet);
  654. const weaponInfo = weaponBullet ? weaponBullet.getWeaponInfo() : null;
  655. const weaponId = weaponInfo ? weaponInfo.getWeaponId() : null;
  656. // 对于锯齿草武器,记录所有检测到的敌人,但在弹射时会根据hasFirstHit状态进行过滤
  657. this.detectedEnemies.add(otherNode);
  658. if (weaponId === 'saw_grass') {
  659. const hitStatus = this.hasFirstHit ? '已首次击中' : '尚未首次击中';
  660. }
  661. }
  662. }
  663. /**
  664. * 检测范围退出事件
  665. */
  666. private onDetectionExit(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  667. const otherNode = otherCollider.node;
  668. if (this.isEnemyNode(otherNode)) {
  669. this.detectedEnemies.delete(otherNode);
  670. // 获取武器信息,确认是否为锯齿草武器
  671. const weaponBullet = this.getComponent(WeaponBullet);
  672. const weaponInfo = weaponBullet ? weaponBullet.getWeaponInfo() : null;
  673. const weaponId = weaponInfo ? weaponInfo.getWeaponId() : null;
  674. if (weaponId === 'saw_grass') {
  675. console.log(`🌿 [锯齿草检测] 锯齿草武器敌人离开: ${otherNode.name}`);
  676. }
  677. }
  678. }
  679. /**
  680. * 获取命中统计
  681. */
  682. public getHitStats() {
  683. return {
  684. hitCount: this.hitCount,
  685. pierceCount: this.pierceCount,
  686. ricochetCount: this.ricochetCount
  687. };
  688. }
  689. /**
  690. * 验证配置
  691. */
  692. public static validateConfig(effects: HitEffectConfig[]): boolean {
  693. if (!Array.isArray(effects) || effects.length === 0) return false;
  694. for (const effect of effects) {
  695. if (!effect.type || effect.priority < 0) return false;
  696. if (!effect.params) return false;
  697. }
  698. return true;
  699. }
  700. private spawnHitEffect(worldPos: Vec3) {
  701. const path = this.defaultHitEffectPath;
  702. if (path) {
  703. this.spawnEffect(path, worldPos, false);
  704. }
  705. }
  706. /**
  707. * 重置锯齿草状态
  708. */
  709. private resetSawGrassState() {
  710. this.hasFirstHit = false;
  711. this.currentTargetEnemy = null;
  712. }
  713. }