BulletHitEffect.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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. const damage = params.damage || 0;
  129. this.damageEnemy(hitNode, damage);
  130. this.spawnHitEffect(hitNode.worldPosition);
  131. return damage;
  132. }
  133. /**
  134. * 处理穿透伤害
  135. */
  136. private processPierceDamage(params: PierceParams, hitNode: Node): number {
  137. const damage = params.damage || 0;
  138. this.damageEnemy(hitNode, damage);
  139. this.spawnHitEffect(hitNode.worldPosition);
  140. this.pierceCount++;
  141. return damage;
  142. }
  143. /**
  144. * 处理爆炸效果
  145. */
  146. private processExplosion(params: ExplosionParams, position: Vec3): number {
  147. const scheduleExplosion = () => {
  148. // 生成爆炸特效
  149. this.spawnExplosionEffect(position);
  150. // 对范围内敌人造成伤害
  151. const damage = this.damageEnemiesInRadius(position, params.radius, params.damage);
  152. return damage;
  153. };
  154. if (params.delay > 0) {
  155. this.scheduleOnce(scheduleExplosion, params.delay);
  156. return 0; // 延迟爆炸,当前不造成伤害
  157. } else {
  158. return scheduleExplosion();
  159. }
  160. }
  161. /**
  162. * 处理地面灼烧效果
  163. */
  164. private processGroundBurn(params: BurnParams, position: Vec3) {
  165. // 创建灼烧区域节点
  166. const burnArea = new Node('BurnArea');
  167. const gameArea = find('Canvas/GameLevelUI/GameArea');
  168. if (gameArea) {
  169. gameArea.addChild(burnArea);
  170. // 设置位置
  171. const localPos = gameArea.getComponent(UITransform)?.convertToNodeSpaceAR(position) || new Vec3();
  172. burnArea.position = localPos;
  173. // 添加碰撞体用于检测范围
  174. const collider = burnArea.addComponent(CircleCollider2D);
  175. collider.radius = params.radius;
  176. collider.sensor = true; // 设为传感器
  177. // 生成灼烧特效
  178. this.spawnBurnEffect(burnArea);
  179. // 定时造成伤害
  180. let remainingTime = params.duration;
  181. const damageTimer = () => {
  182. if (remainingTime <= 0 || !burnArea.isValid) {
  183. burnArea.destroy();
  184. return;
  185. }
  186. // 对范围内敌人造成伤害
  187. this.damageEnemiesInRadius(position, params.radius, params.damage);
  188. remainingTime -= params.tickInterval;
  189. this.scheduleOnce(damageTimer, params.tickInterval);
  190. };
  191. this.scheduleOnce(damageTimer, params.tickInterval);
  192. this.activeBurnAreas.push(burnArea);
  193. }
  194. }
  195. /**
  196. * 处理弹射伤害
  197. */
  198. private processRicochetDamage(params: RicochetParams, hitNode: Node): number {
  199. const damage = params.damage || 0;
  200. this.damageEnemy(hitNode, damage);
  201. if (this.ricochetCount < params.ricochetCount) {
  202. this.ricochetCount++;
  203. // 计算弹射方向
  204. this.calculateRicochetDirection(params.ricochetAngle);
  205. }
  206. return damage;
  207. }
  208. /**
  209. * 计算弹射方向
  210. */
  211. private calculateRicochetDirection(maxAngle: number) {
  212. const trajectory = this.getComponent(BulletTrajectory);
  213. if (!trajectory) return;
  214. // 获取当前速度方向
  215. const currentVel = trajectory.getCurrentVelocity();
  216. // 随机弹射角度
  217. const angleRad = (Math.random() - 0.5) * maxAngle * Math.PI / 180;
  218. const cos = Math.cos(angleRad);
  219. const sin = Math.sin(angleRad);
  220. // 应用旋转
  221. const newDirection = new Vec3(
  222. currentVel.x * cos - currentVel.y * sin,
  223. currentVel.x * sin + currentVel.y * cos,
  224. 0
  225. ).normalize();
  226. // 使用弹道组件的changeDirection方法
  227. trajectory.changeDirection(newDirection);
  228. }
  229. /**
  230. * 对单个敌人造成伤害
  231. */
  232. private damageEnemy(enemyNode: Node, damage: number) {
  233. if (!this.isEnemyNode(enemyNode)) return;
  234. // 计算暴击伤害和暴击状态
  235. const damageResult = this.calculateCriticalDamage(damage);
  236. // 尝试调用敌人的受伤方法
  237. const enemyInstance = enemyNode.getComponent('EnemyInstance') as any;
  238. if (enemyInstance) {
  239. if (typeof enemyInstance.takeDamage === 'function') {
  240. enemyInstance.takeDamage(damageResult.damage, damageResult.isCritical);
  241. return;
  242. }
  243. if (typeof enemyInstance.health === 'number') {
  244. enemyInstance.health -= damageResult.damage;
  245. if (enemyInstance.health <= 0) {
  246. enemyNode.destroy();
  247. }
  248. return;
  249. }
  250. }
  251. // 备用方案:通过EnemyController
  252. const enemyController = find('Canvas/GameLevelUI/EnemyController')?.getComponent('EnemyController') as any;
  253. if (enemyController && typeof enemyController.damageEnemy === 'function') {
  254. enemyController.damageEnemy(enemyNode, damageResult.damage, damageResult.isCritical);
  255. }
  256. }
  257. /**
  258. * 计算暴击伤害
  259. */
  260. private calculateCriticalDamage(baseDamage: number): { damage: number, isCritical: boolean } {
  261. // 从WeaponBullet获取暴击相关数值
  262. const weaponBullet = this.getComponent('WeaponBullet') as any;
  263. if (!weaponBullet) {
  264. // 如果没有WeaponBullet组件,使用基础暴击率10%
  265. const critChance = 0.1;
  266. const isCritical = Math.random() < critChance;
  267. if (isCritical) {
  268. const critDamage = baseDamage * 2; // 默认暴击倍数2倍
  269. this.showCriticalHitEffect();
  270. console.log(`[BulletHitEffect] 暴击!基础伤害: ${baseDamage}, 暴击伤害: ${critDamage}, 暴击率: ${(critChance * 100).toFixed(1)}%`);
  271. return { damage: critDamage, isCritical: true };
  272. }
  273. return { damage: baseDamage, isCritical: false };
  274. }
  275. // 获取暴击率和暴击伤害
  276. const critChance = weaponBullet.getCritChance();
  277. const critDamage = weaponBullet.getFinalCritDamage();
  278. // 检查是否触发暴击
  279. const isCritical = Math.random() < critChance;
  280. if (isCritical) {
  281. // 显示暴击特效
  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. * 显示暴击特效
  290. */
  291. private showCriticalHitEffect() {
  292. // 这里可以添加暴击特效,比如特殊的粒子效果或音效
  293. // 暂时只在控制台输出,后续可以扩展
  294. console.log('[BulletHitEffect] 暴击特效触发!');
  295. }
  296. /**
  297. * 对范围内敌人造成伤害
  298. */
  299. private damageEnemiesInRadius(center: Vec3, radius: number, damage: number): number {
  300. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  301. if (!enemyContainer) return 0;
  302. let totalDamage = 0;
  303. const enemies = enemyContainer.children.filter(child =>
  304. child.active && this.isEnemyNode(child)
  305. );
  306. // 获取WeaponBullet组件以使用最终伤害值
  307. const weaponBullet = this.getComponent('WeaponBullet') as any;
  308. const baseDamage = weaponBullet ? weaponBullet.getFinalDamage() : damage;
  309. for (const enemy of enemies) {
  310. const distance = Vec3.distance(center, enemy.worldPosition);
  311. if (distance <= radius) {
  312. // 每个敌人独立计算暴击
  313. const damageResult = this.calculateCriticalDamage(baseDamage);
  314. // 直接处理伤害,避免重复计算暴击
  315. this.applyDamageToEnemy(enemy, damageResult.damage, damageResult.isCritical);
  316. totalDamage += damageResult.damage;
  317. }
  318. }
  319. return totalDamage;
  320. }
  321. /**
  322. * 直接对敌人应用伤害(不进行暴击计算)
  323. */
  324. private applyDamageToEnemy(enemyNode: Node, damage: number, isCritical: boolean = false) {
  325. if (!this.isEnemyNode(enemyNode)) return;
  326. // 尝试调用敌人的受伤方法
  327. const enemyInstance = enemyNode.getComponent('EnemyInstance') as any;
  328. if (enemyInstance) {
  329. if (typeof enemyInstance.takeDamage === 'function') {
  330. enemyInstance.takeDamage(damage, isCritical);
  331. return;
  332. }
  333. if (typeof enemyInstance.health === 'number') {
  334. enemyInstance.health -= damage;
  335. if (enemyInstance.health <= 0) {
  336. enemyNode.destroy();
  337. }
  338. return;
  339. }
  340. }
  341. // 备用方案:通过EnemyController
  342. const enemyController = find('Canvas/GameLevelUI/EnemyController')?.getComponent('EnemyController') as any;
  343. if (enemyController && typeof enemyController.damageEnemy === 'function') {
  344. enemyController.damageEnemy(enemyNode, damage, isCritical);
  345. }
  346. }
  347. /**
  348. * 判断是否为敌人节点
  349. */
  350. private isEnemyNode(node: Node): boolean {
  351. const name = node.name.toLowerCase();
  352. return name.includes('enemy') ||
  353. name.includes('敌人') ||
  354. node.getComponent('EnemyInstance') !== null;
  355. }
  356. /**
  357. * 生成爆炸特效
  358. */
  359. private spawnExplosionEffect(position: Vec3) {
  360. const path = this.defaultHitEffectPath || 'Animation/tx/tx0004';
  361. this.spawnEffect(path, position, false);
  362. }
  363. /**
  364. * 生成灼烧特效
  365. */
  366. private spawnBurnEffect(parent: Node) {
  367. const path = this.defaultBurnEffectPath || this.defaultTrailEffectPath || 'Animation/tx/tx0006';
  368. this.spawnEffect(path, new Vec3(), true, parent);
  369. }
  370. /**
  371. * 生成特效
  372. */
  373. private spawnEffect(path: string, worldPos: Vec3, loop = false, parent?: Node) {
  374. if (!path) return;
  375. const spawnWithData = (skData: sp.SkeletonData) => {
  376. const effectNode = new Node('Effect');
  377. const skeletonComp: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
  378. skeletonComp.skeletonData = skData;
  379. skeletonComp.setAnimation(0, 'animation', loop);
  380. const targetParent = parent || find('Canvas/GameLevelUI/GameArea') || find('Canvas');
  381. if (targetParent) {
  382. targetParent.addChild(effectNode);
  383. if (parent) {
  384. effectNode.setPosition(0, 0, 0);
  385. } else {
  386. const parentTrans = targetParent.getComponent(UITransform);
  387. if (parentTrans) {
  388. const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
  389. effectNode.position = localPos;
  390. }
  391. }
  392. }
  393. if (!loop) {
  394. skeletonComp.setCompleteListener(() => {
  395. effectNode.destroy();
  396. });
  397. }
  398. };
  399. // 先尝试直接加载给定路径
  400. resources.load(path, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
  401. if (err) {
  402. console.warn('加载特效失败:', path, err);
  403. return;
  404. }
  405. spawnWithData(skData);
  406. });
  407. }
  408. /**
  409. * 清理资源
  410. */
  411. onDestroy() {
  412. // 清理激活的灼烧区域
  413. for (const burnArea of this.activeBurnAreas) {
  414. if (burnArea && burnArea.isValid) {
  415. burnArea.destroy();
  416. }
  417. }
  418. this.activeBurnAreas = [];
  419. }
  420. /**
  421. * 获取命中统计
  422. */
  423. public getHitStats() {
  424. return {
  425. hitCount: this.hitCount,
  426. pierceCount: this.pierceCount,
  427. ricochetCount: this.ricochetCount
  428. };
  429. }
  430. /**
  431. * 验证配置
  432. */
  433. public static validateConfig(effects: HitEffectConfig[]): boolean {
  434. if (!Array.isArray(effects) || effects.length === 0) return false;
  435. for (const effect of effects) {
  436. if (!effect.type || effect.priority < 0) return false;
  437. if (!effect.params) return false;
  438. }
  439. return true;
  440. }
  441. private spawnHitEffect(worldPos: Vec3) {
  442. const path = this.defaultHitEffectPath;
  443. if (path) {
  444. this.spawnEffect(path, worldPos, false);
  445. }
  446. }
  447. }