BulletHitEffect.ts 19 KB

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