BulletHitEffect.ts 20 KB

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