BulletHitEffect.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. import { _decorator, Component, Node, Vec3, find, instantiate, Prefab, UITransform, resources, sp, CircleCollider2D } 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. const { ccclass, property } = _decorator;
  11. /**
  12. * 命中效果控制器
  13. * 负责处理可叠加的命中效果
  14. */
  15. // 接口定义已移至ConfigManager.ts中的HitEffectConfig
  16. export interface HitResult {
  17. shouldDestroy: boolean; // 是否应该销毁子弹
  18. shouldContinue: boolean; // 是否应该继续飞行
  19. shouldRicochet: boolean; // 是否应该弹射
  20. damageDealt: number; // 造成的伤害
  21. }
  22. @ccclass('BulletHitEffect')
  23. export class BulletHitEffect extends Component {
  24. private hitEffects: HitEffectConfig[] = [];
  25. private hitCount: number = 0;
  26. private pierceCount: number = 0;
  27. private ricochetCount: number = 0;
  28. private activeBurnAreas: Node[] = [];
  29. // 默认特效路径(从WeaponBullet传入)
  30. private defaultHitEffectPath: string = '';
  31. private defaultTrailEffectPath: string = '';
  32. private defaultBurnEffectPath: string = '';
  33. public setDefaultEffects(hit: string | null, trail?: string | null, burn?: string | null) {
  34. if (hit) this.defaultHitEffectPath = hit;
  35. if (trail) this.defaultTrailEffectPath = trail;
  36. if (burn) this.defaultBurnEffectPath = burn;
  37. }
  38. /**
  39. * 初始化命中效果
  40. */
  41. public init(effects: HitEffectConfig[]) {
  42. // 按优先级排序
  43. this.hitEffects = [...effects].sort((a, b) => a.priority - b.priority);
  44. }
  45. /**
  46. * 处理命中事件
  47. */
  48. public processHit(hitNode: Node, contactPos: Vec3): HitResult {
  49. console.log(`[BulletHitEffect] 处理命中 - 目标: ${hitNode.name}, 位置: ${contactPos}, 效果数量: ${this.hitEffects.length}`);
  50. this.hitCount++;
  51. const result: HitResult = {
  52. shouldDestroy: false,
  53. shouldContinue: false,
  54. shouldRicochet: false,
  55. damageDealt: 0
  56. };
  57. // 按优先级处理所有效果
  58. for (const effect of this.hitEffects) {
  59. const effectResult = this.processEffect(effect, hitNode, contactPos);
  60. // 累积伤害
  61. result.damageDealt += effectResult.damageDealt;
  62. // 处理控制逻辑(OR逻辑,任何一个效果要求的行为都会执行)
  63. if (effectResult.shouldDestroy) result.shouldDestroy = true;
  64. if (effectResult.shouldContinue) result.shouldContinue = true;
  65. if (effectResult.shouldRicochet) result.shouldRicochet = true;
  66. }
  67. // 逻辑优先级:销毁 > 弹射 > 继续
  68. if (result.shouldDestroy) {
  69. result.shouldContinue = false;
  70. result.shouldRicochet = false;
  71. } else if (result.shouldRicochet) {
  72. result.shouldContinue = false;
  73. }
  74. return result;
  75. }
  76. /**
  77. * 处理单个效果
  78. */
  79. private processEffect(effect: HitEffectConfig, hitNode: Node, contactPos: Vec3): HitResult {
  80. const result: HitResult = {
  81. shouldDestroy: false,
  82. shouldContinue: false,
  83. shouldRicochet: false,
  84. damageDealt: 0
  85. };
  86. switch (effect.type) {
  87. case 'normal_damage':
  88. result.damageDealt = this.processNormalDamage(effect, hitNode);
  89. result.shouldDestroy = true;
  90. break;
  91. case 'pierce_damage':
  92. result.damageDealt = this.processPierceDamage(effect, hitNode);
  93. break;
  94. case 'explosion':
  95. result.damageDealt = this.processExplosion(effect, contactPos);
  96. result.shouldDestroy = true;
  97. break;
  98. case 'ground_burn':
  99. console.log(`[BulletHitEffect] 触发灼烧效果处理`);
  100. this.processGroundBurn(effect, hitNode);
  101. break;
  102. case 'ricochet_damage':
  103. result.damageDealt = this.processRicochetDamage(effect, hitNode);
  104. result.shouldRicochet = this.ricochetCount < (effect.ricochetCount || 0);
  105. break;
  106. }
  107. return result;
  108. }
  109. /**
  110. * 处理普通伤害
  111. */
  112. private processNormalDamage(effect: any, hitNode: Node): number {
  113. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  114. const weaponBullet = this.getComponent(WeaponBullet);
  115. const damage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  116. console.log(`[BulletHitEffect] 普通伤害处理 - 配置伤害: ${effect.damage || 0}, 最终伤害: ${damage}`);
  117. this.damageEnemy(hitNode, damage);
  118. this.spawnHitEffect(hitNode.worldPosition);
  119. return damage;
  120. }
  121. /**
  122. * 处理穿透伤害
  123. */
  124. private processPierceDamage(effect: HitEffectConfig, hitNode: Node): number {
  125. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  126. const weaponBullet = this.getComponent(WeaponBullet);
  127. const damage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  128. console.log(`[BulletHitEffect] 穿透伤害处理 - 配置伤害: ${effect.damage || 0}, 最终伤害: ${damage}`);
  129. this.damageEnemy(hitNode, damage);
  130. this.spawnHitEffect(hitNode.worldPosition);
  131. this.pierceCount++;
  132. return damage;
  133. }
  134. /**
  135. * 处理爆炸效果
  136. */
  137. private processExplosion(effect: HitEffectConfig, position: Vec3): number {
  138. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  139. const weaponBullet = this.getComponent(WeaponBullet);
  140. const explosionDamage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  141. console.log(`[BulletHitEffect] 爆炸伤害处理 - 配置伤害: ${effect.damage || 0}, 最终伤害: ${explosionDamage}`);
  142. const scheduleExplosion = () => {
  143. // 生成爆炸特效
  144. this.spawnExplosionEffect(position);
  145. // 对范围内敌人造成伤害
  146. const damage = this.damageEnemiesInRadius(position, effect.radius, explosionDamage);
  147. return damage;
  148. };
  149. if (effect.delay > 0) {
  150. this.scheduleOnce(scheduleExplosion, effect.delay);
  151. return 0; // 延迟爆炸,当前不造成伤害
  152. } else {
  153. return scheduleExplosion();
  154. }
  155. }
  156. /**
  157. * 处理地面燃烧区域效果
  158. */
  159. private processGroundBurn(effect: HitEffectConfig, hitNode: Node) {
  160. console.log(`[BulletHitEffect] processGroundBurn 被调用 - 创建地面燃烧区域`);
  161. // 获取地面燃烧区域管理器
  162. const burnAreaManager = GroundBurnAreaManager.getInstance();
  163. if (!burnAreaManager) {
  164. console.error('[BulletHitEffect] 无法获取GroundBurnAreaManager实例');
  165. return;
  166. }
  167. // 获取子弹的世界位置作为燃烧区域中心
  168. const burnPosition = this.node.worldPosition.clone();
  169. // 如果命中的是敌人,使用敌人的位置
  170. if (this.isEnemyNode(hitNode)) {
  171. burnPosition.set(hitNode.worldPosition);
  172. }
  173. console.log(`[BulletHitEffect] 在位置 (${burnPosition.x.toFixed(1)}, ${burnPosition.y.toFixed(1)}) 创建地面燃烧区域 - 持续时间: ${effect.duration}秒, 伤害: ${effect.damage}, 间隔: ${effect.tickInterval}秒`);
  174. try {
  175. // 获取武器子弹组件
  176. const weaponBullet = this.getComponent(WeaponBullet);
  177. // 通过管理器创建燃烧区域
  178. const burnAreaNode = burnAreaManager.createGroundBurnArea(
  179. burnPosition,
  180. effect,
  181. weaponBullet,
  182. this.defaultBurnEffectPath || this.defaultTrailEffectPath
  183. );
  184. if (burnAreaNode) {
  185. // 将燃烧区域添加到活跃列表中
  186. this.activeBurnAreas.push(burnAreaNode);
  187. console.log(`[BulletHitEffect] 地面燃烧区域已通过管理器创建`);
  188. }
  189. } catch (error) {
  190. console.error('[BulletHitEffect] 通过管理器创建地面燃烧区域失败:', error);
  191. }
  192. }
  193. /**
  194. * 处理弹射伤害
  195. */
  196. private processRicochetDamage(effect: HitEffectConfig, hitNode: Node): number {
  197. // 使用WeaponBullet的最终伤害值而不是配置中的基础伤害
  198. const weaponBullet = this.getComponent(WeaponBullet);
  199. const damage = weaponBullet ? weaponBullet.getFinalDamage() : (effect.damage || 0);
  200. console.log(`[BulletHitEffect] 弹射伤害处理 - 配置伤害: ${effect.damage || 0}, 最终伤害: ${damage}`);
  201. this.damageEnemy(hitNode, damage);
  202. if (this.ricochetCount < effect.ricochetCount) {
  203. this.ricochetCount++;
  204. // 计算弹射方向
  205. this.calculateRicochetDirection(effect.ricochetAngle);
  206. }
  207. return damage;
  208. }
  209. /**
  210. * 计算弹射方向
  211. */
  212. private calculateRicochetDirection(maxAngle: number) {
  213. const trajectory = this.getComponent(BulletTrajectory);
  214. if (!trajectory) return;
  215. // 获取当前速度方向
  216. const currentVel = trajectory.getCurrentVelocity();
  217. // 随机弹射角度
  218. const angleRad = (Math.random() - 0.5) * maxAngle * Math.PI / 180;
  219. const cos = Math.cos(angleRad);
  220. const sin = Math.sin(angleRad);
  221. // 应用旋转
  222. const newDirection = new Vec3(
  223. currentVel.x * cos - currentVel.y * sin,
  224. currentVel.x * sin + currentVel.y * cos,
  225. 0
  226. ).normalize();
  227. // 使用弹道组件的changeDirection方法
  228. trajectory.changeDirection(newDirection);
  229. }
  230. /**
  231. * 对单个敌人造成伤害
  232. */
  233. private damageEnemy(enemyNode: Node, damage: number) {
  234. if (!this.isEnemyNode(enemyNode)) return;
  235. // 计算暴击伤害和暴击状态
  236. const damageResult = this.calculateCriticalDamage(damage);
  237. // 使用applyDamageToEnemy方法,通过EventBus发送伤害事件
  238. this.applyDamageToEnemy(enemyNode, damageResult.damage, damageResult.isCritical);
  239. }
  240. /**
  241. * 计算暴击伤害
  242. */
  243. private calculateCriticalDamage(baseDamage: number): { damage: number, isCritical: boolean } {
  244. // 获取武器子弹组件来计算暴击
  245. const weaponBullet = this.getComponent(WeaponBullet);
  246. if (!weaponBullet) {
  247. // 如果没有WeaponBullet组件,使用基础暴击率10%
  248. const critChance = 0.1;
  249. const isCritical = Math.random() < critChance;
  250. if (isCritical) {
  251. // 暴击伤害 = 基础伤害 × (1 + 暴击伤害倍率),默认暴击倍率100%
  252. const critDamage = Math.ceil((baseDamage * (1 + 1.0)) * 10) / 10; // 向上取整到一位小数
  253. this.showCriticalHitEffect();
  254. console.log(`[BulletHitEffect] 暴击!基础伤害: ${baseDamage}, 暴击伤害: ${critDamage}, 暴击率: ${(critChance * 100).toFixed(1)}%`);
  255. return { damage: critDamage, isCritical: true };
  256. }
  257. return { damage: baseDamage, isCritical: false };
  258. }
  259. // 获取暴击率
  260. const critChance = weaponBullet.getCritChance();
  261. // 检查是否触发暴击
  262. const isCritical = Math.random() < critChance;
  263. if (isCritical) {
  264. // 获取暴击伤害倍率(从技能系统)
  265. const critDamageMultiplier = this.getCritDamageMultiplier();
  266. // 暴击伤害 = 基础伤害 × (1 + 暴击伤害倍率)
  267. const critDamage = Math.ceil((baseDamage * (1 + critDamageMultiplier)) * 10) / 10; // 向上取整到一位小数
  268. // 显示暴击特效
  269. this.showCriticalHitEffect();
  270. console.log(`[BulletHitEffect] 暴击!基础伤害: ${baseDamage}, 暴击倍率: ${(critDamageMultiplier * 100).toFixed(1)}%, 暴击伤害: ${critDamage}, 暴击率: ${(critChance * 100).toFixed(1)}%`);
  271. return { damage: critDamage, isCritical: true };
  272. }
  273. return { damage: baseDamage, isCritical: false };
  274. }
  275. /**
  276. * 获取暴击伤害倍率
  277. */
  278. private getCritDamageMultiplier(): number {
  279. // 从局外技能系统获取暴击伤害加成
  280. const persistentSkillManager = PersistentSkillManager.getInstance();
  281. if (persistentSkillManager) {
  282. const bonuses = persistentSkillManager.getSkillBonuses();
  283. // critDamageBonus是百分比值,需要转换为倍率
  284. // 暴击伤害倍率 = 基础100% + 技能加成百分比
  285. return 1.0 + (bonuses.critDamageBonus || 0) / 100;
  286. }
  287. // 默认暴击倍率100%(即2倍伤害)
  288. return 1.0;
  289. }
  290. /**
  291. * 显示暴击特效
  292. */
  293. private showCriticalHitEffect() {
  294. // 这里可以添加暴击特效,比如特殊的粒子效果或音效
  295. // 暂时只在控制台输出,后续可以扩展
  296. console.log('[BulletHitEffect] 暴击特效触发!');
  297. }
  298. /**
  299. * 对范围内敌人造成伤害
  300. */
  301. private damageEnemiesInRadius(center: Vec3, radius: number, damage: number): number {
  302. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  303. if (!enemyContainer) return 0;
  304. let totalDamage = 0;
  305. const enemies = enemyContainer.children.filter(child =>
  306. child.active && this.isEnemyNode(child)
  307. );
  308. // 对于持续伤害(如地面灼烧),直接使用传入的伤害值
  309. // 对于爆炸伤害,使用WeaponBullet的最终伤害值
  310. const baseDamage = damage;
  311. console.log(`[BulletHitEffect] 范围伤害 - 中心位置: (${center.x.toFixed(1)}, ${center.y.toFixed(1)}), 半径: ${radius}, 基础伤害: ${baseDamage}`);
  312. for (const enemy of enemies) {
  313. const distance = Vec3.distance(center, enemy.worldPosition);
  314. if (distance <= radius) {
  315. // 每个敌人独立计算暴击
  316. const damageResult = this.calculateCriticalDamage(baseDamage);
  317. console.log(`[BulletHitEffect] 敌人受到范围伤害 - 距离: ${distance.toFixed(1)}, 伤害: ${damageResult.damage}, 暴击: ${damageResult.isCritical}`);
  318. // 直接处理伤害,避免重复计算暴击
  319. this.applyDamageToEnemy(enemy, damageResult.damage, damageResult.isCritical);
  320. totalDamage += damageResult.damage;
  321. }
  322. }
  323. return totalDamage;
  324. }
  325. /**
  326. * 直接对敌人应用伤害(不进行暴击计算)
  327. */
  328. private applyDamageToEnemy(enemyNode: Node, damage: number, isCritical: boolean = false) {
  329. console.log(`[BulletHitEffect] 通过EventBus发送伤害事件: ${damage}, 暴击: ${isCritical}, 敌人节点: ${enemyNode.name}`);
  330. if (!this.isEnemyNode(enemyNode)) {
  331. console.log(`[BulletHitEffect] 节点不是敌人,跳过伤害`);
  332. return;
  333. }
  334. // 通过EventBus发送伤害事件
  335. const eventBus = EventBus.getInstance();
  336. const damageData = {
  337. enemyNode: enemyNode,
  338. damage: damage,
  339. isCritical: isCritical,
  340. source: 'BulletHitEffect'
  341. };
  342. console.log(`[BulletHitEffect] 发送APPLY_DAMAGE_TO_ENEMY事件`, damageData);
  343. eventBus.emit(GameEvents.APPLY_DAMAGE_TO_ENEMY, damageData);
  344. }
  345. /**
  346. * 判断是否为敌人节点
  347. */
  348. private isEnemyNode(node: Node): boolean {
  349. if (!node || !node.isValid) {
  350. return false;
  351. }
  352. const name = node.name.toLowerCase();
  353. return name.includes('enemy') ||
  354. name.includes('敌人');
  355. }
  356. /**
  357. * 生成爆炸特效
  358. */
  359. private spawnExplosionEffect(position: Vec3) {
  360. const path = this.defaultHitEffectPath || 'Animation/WeaponTx/tx0004/tx0004';
  361. this.spawnEffect(path, position, false);
  362. }
  363. /**
  364. * 生成灼烧特效
  365. */
  366. private spawnBurnEffect(parent: Node) {
  367. const path = this.defaultBurnEffectPath || this.defaultTrailEffectPath || 'Animation/WeaponTx/tx0006/tx0006';
  368. // 使用回调函数处理异步创建的特效节点
  369. this.spawnEffect(path, new Vec3(), true, parent, (effectNode) => {
  370. // 将特效节点传递给 BurnEffect 组件,以便在停止时清理
  371. const burnEffect = parent.getComponent(BurnEffect);
  372. if (burnEffect && effectNode) {
  373. burnEffect.setBurnEffectNode(effectNode);
  374. }
  375. });
  376. }
  377. /**
  378. * 生成地面燃烧特效
  379. */
  380. private spawnGroundBurnEffect(parent: Node) {
  381. const path = this.defaultBurnEffectPath || this.defaultTrailEffectPath || 'Animation/WeaponTx/tx0006/tx0006';
  382. // 使用回调函数处理异步创建的特效节点
  383. this.spawnEffect(path, new Vec3(), true, parent, (effectNode) => {
  384. // 将特效节点传递给 GroundBurnArea 组件,以便在停止时清理
  385. const groundBurnArea = parent.getComponent(GroundBurnArea);
  386. if (groundBurnArea && effectNode) {
  387. groundBurnArea.setBurnEffectNode(effectNode);
  388. }
  389. });
  390. }
  391. /**
  392. * 生成特效
  393. */
  394. private spawnEffect(path: string, worldPos: Vec3, loop = false, parent?: Node, onCreated?: (effectNode: Node) => void): void {
  395. if (!path) return;
  396. const spawnWithData = (skData: sp.SkeletonData) => {
  397. const effectNode = new Node('Effect');
  398. const skeletonComp: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
  399. skeletonComp.skeletonData = skData;
  400. skeletonComp.setAnimation(0, 'animation', loop);
  401. const targetParent = parent || find('Canvas/GameLevelUI/GameArea') || find('Canvas');
  402. if (targetParent) {
  403. targetParent.addChild(effectNode);
  404. if (parent) {
  405. effectNode.setPosition(0, 0, 0);
  406. } else {
  407. const parentTrans = targetParent.getComponent(UITransform);
  408. if (parentTrans) {
  409. const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
  410. effectNode.position = localPos;
  411. }
  412. }
  413. }
  414. if (!loop) {
  415. skeletonComp.setCompleteListener(() => {
  416. effectNode.destroy();
  417. });
  418. }
  419. // 调用回调函数,传递创建的特效节点
  420. if (onCreated) {
  421. onCreated(effectNode);
  422. }
  423. };
  424. // 先尝试直接加载给定路径
  425. resources.load(path, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
  426. if (err) {
  427. console.warn('加载特效失败:', path, err);
  428. return;
  429. }
  430. spawnWithData(skData);
  431. });
  432. }
  433. /**
  434. * 清理资源
  435. */
  436. onDestroy() {
  437. // 不再强制销毁燃烧区域,让它们按照自己的持续时间自然销毁
  438. // 燃烧区域现在由GroundBurnAreaManager统一管理,有自己的生命周期
  439. console.log(`[BulletHitEffect] 子弹销毁,但不会影响已创建的${this.activeBurnAreas.length}个燃烧区域`);
  440. // 只清空引用,不销毁燃烧区域节点
  441. this.activeBurnAreas = [];
  442. }
  443. /**
  444. * 获取命中统计
  445. */
  446. public getHitStats() {
  447. return {
  448. hitCount: this.hitCount,
  449. pierceCount: this.pierceCount,
  450. ricochetCount: this.ricochetCount
  451. };
  452. }
  453. /**
  454. * 验证配置
  455. */
  456. public static validateConfig(effects: HitEffectConfig[]): boolean {
  457. if (!Array.isArray(effects) || effects.length === 0) return false;
  458. for (const effect of effects) {
  459. if (!effect.type || effect.priority < 0) return false;
  460. if (!effect.params) return false;
  461. }
  462. return true;
  463. }
  464. private spawnHitEffect(worldPos: Vec3) {
  465. const path = this.defaultHitEffectPath;
  466. if (path) {
  467. this.spawnEffect(path, worldPos, false);
  468. }
  469. }
  470. }