BulletHitEffect.ts 36 KB

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