BulletHitEffect.ts 19 KB

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