BulletHitEffect.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. console.log(`💥 命中效果初始化: ${this.hitEffects.length}个效果`);
  62. }
  63. /**
  64. * 处理命中事件
  65. */
  66. public processHit(hitNode: Node, contactPos: Vec3): HitResult {
  67. console.log(`💥 处理命中: ${hitNode.name}`);
  68. const result: HitResult = {
  69. shouldDestroy: false,
  70. shouldContinue: false,
  71. shouldRicochet: false,
  72. damageDealt: 0
  73. };
  74. this.hitCount++;
  75. // 按优先级处理所有效果
  76. for (const effect of this.hitEffects) {
  77. const effectResult = this.processEffect(effect, hitNode, contactPos);
  78. // 累积伤害
  79. result.damageDealt += effectResult.damageDealt;
  80. // 处理控制逻辑(OR逻辑,任何一个效果要求的行为都会执行)
  81. if (effectResult.shouldDestroy) result.shouldDestroy = true;
  82. if (effectResult.shouldContinue) result.shouldContinue = true;
  83. if (effectResult.shouldRicochet) result.shouldRicochet = true;
  84. }
  85. // 逻辑优先级:销毁 > 弹射 > 继续
  86. if (result.shouldDestroy) {
  87. result.shouldContinue = false;
  88. result.shouldRicochet = false;
  89. } else if (result.shouldRicochet) {
  90. result.shouldContinue = false;
  91. }
  92. return result;
  93. }
  94. /**
  95. * 处理单个效果
  96. */
  97. private processEffect(effect: HitEffectConfig, hitNode: Node, contactPos: Vec3): HitResult {
  98. const result: HitResult = {
  99. shouldDestroy: false,
  100. shouldContinue: false,
  101. shouldRicochet: false,
  102. damageDealt: 0
  103. };
  104. switch (effect.type) {
  105. case 'normal_damage':
  106. result.damageDealt = this.processNormalDamage(effect.params, hitNode);
  107. result.shouldDestroy = true;
  108. break;
  109. case 'pierce_damage':
  110. result.damageDealt = this.processPierceDamage(effect.params as PierceParams, hitNode);
  111. break;
  112. case 'explosion':
  113. result.damageDealt = this.processExplosion(effect.params as ExplosionParams, contactPos);
  114. result.shouldDestroy = true;
  115. break;
  116. case 'ground_burn':
  117. this.processGroundBurn(effect.params as BurnParams, contactPos);
  118. break;
  119. case 'ricochet_damage':
  120. result.damageDealt = this.processRicochetDamage(effect.params as RicochetParams, hitNode);
  121. result.shouldRicochet = this.ricochetCount < (effect.params as RicochetParams).ricochetCount;
  122. break;
  123. }
  124. return result;
  125. }
  126. /**
  127. * 处理普通伤害
  128. */
  129. private processNormalDamage(params: any, hitNode: Node): number {
  130. const damage = params.damage || 0;
  131. this.damageEnemy(hitNode, damage);
  132. this.spawnHitEffect(hitNode.worldPosition);
  133. console.log(`⚔️ 普通伤害: ${damage}`);
  134. return damage;
  135. }
  136. /**
  137. * 处理穿透伤害
  138. */
  139. private processPierceDamage(params: PierceParams, hitNode: Node): number {
  140. const damage = params.damage || 0;
  141. this.damageEnemy(hitNode, damage);
  142. this.spawnHitEffect(hitNode.worldPosition);
  143. this.pierceCount++;
  144. console.log(`🏹 穿透伤害: ${damage}, 穿透次数: ${this.pierceCount}/${params.pierceCount}`);
  145. return damage;
  146. }
  147. /**
  148. * 处理爆炸效果
  149. */
  150. private processExplosion(params: ExplosionParams, position: Vec3): number {
  151. console.log(`💣 爆炸效果: 伤害${params.damage}, 半径${params.radius}`);
  152. const scheduleExplosion = () => {
  153. // 生成爆炸特效
  154. this.spawnExplosionEffect(position);
  155. // 对范围内敌人造成伤害
  156. const damage = this.damageEnemiesInRadius(position, params.radius, params.damage);
  157. return damage;
  158. };
  159. if (params.delay > 0) {
  160. this.scheduleOnce(scheduleExplosion, params.delay);
  161. return 0; // 延迟爆炸,当前不造成伤害
  162. } else {
  163. return scheduleExplosion();
  164. }
  165. }
  166. /**
  167. * 处理地面灼烧效果
  168. */
  169. private processGroundBurn(params: BurnParams, position: Vec3) {
  170. console.log(`🔥 地面灼烧: 伤害${params.damage}, 持续${params.duration}秒`);
  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. const damage = params.damage || 0;
  206. this.damageEnemy(hitNode, damage);
  207. if (this.ricochetCount < params.ricochetCount) {
  208. this.ricochetCount++;
  209. // 计算弹射方向
  210. this.calculateRicochetDirection(params.ricochetAngle);
  211. console.log(`🎾 弹射伤害: ${damage}, 弹射次数: ${this.ricochetCount}/${params.ricochetCount}`);
  212. }
  213. return damage;
  214. }
  215. /**
  216. * 计算弹射方向
  217. */
  218. private calculateRicochetDirection(maxAngle: number) {
  219. const trajectory = this.getComponent(BulletTrajectory);
  220. if (!trajectory) return;
  221. // 获取当前速度方向
  222. const currentVel = trajectory.getCurrentVelocity();
  223. // 随机弹射角度
  224. const angleRad = (Math.random() - 0.5) * maxAngle * Math.PI / 180;
  225. const cos = Math.cos(angleRad);
  226. const sin = Math.sin(angleRad);
  227. // 应用旋转
  228. const newDirection = new Vec3(
  229. currentVel.x * cos - currentVel.y * sin,
  230. currentVel.x * sin + currentVel.y * cos,
  231. 0
  232. ).normalize();
  233. // 使用弹道组件的changeDirection方法
  234. trajectory.changeDirection(newDirection);
  235. console.log('🎾 弹射方向已计算并应用');
  236. }
  237. /**
  238. * 对单个敌人造成伤害
  239. */
  240. private damageEnemy(enemyNode: Node, damage: number) {
  241. if (!this.isEnemyNode(enemyNode)) return;
  242. console.log(`⚔️ 对敌人 ${enemyNode.name} 造成 ${damage} 伤害`);
  243. // 尝试调用敌人的受伤方法
  244. const enemyInstance = enemyNode.getComponent('EnemyInstance') as any;
  245. if (enemyInstance) {
  246. if (typeof enemyInstance.takeDamage === 'function') {
  247. enemyInstance.takeDamage(damage);
  248. return;
  249. }
  250. if (typeof enemyInstance.health === 'number') {
  251. enemyInstance.health -= damage;
  252. if (enemyInstance.health <= 0) {
  253. enemyNode.destroy();
  254. }
  255. return;
  256. }
  257. }
  258. // 备用方案:通过EnemyController
  259. const enemyController = find('Canvas/GameLevelUI/EnemyController')?.getComponent('EnemyController') as any;
  260. if (enemyController && typeof enemyController.damageEnemy === 'function') {
  261. enemyController.damageEnemy(enemyNode, damage);
  262. }
  263. }
  264. /**
  265. * 对范围内敌人造成伤害
  266. */
  267. private damageEnemiesInRadius(center: Vec3, radius: number, damage: number): number {
  268. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  269. if (!enemyContainer) return 0;
  270. let totalDamage = 0;
  271. const enemies = enemyContainer.children.filter(child =>
  272. child.active && this.isEnemyNode(child)
  273. );
  274. for (const enemy of enemies) {
  275. const distance = Vec3.distance(center, enemy.worldPosition);
  276. if (distance <= radius) {
  277. this.damageEnemy(enemy, damage);
  278. totalDamage += damage;
  279. }
  280. }
  281. console.log(`💥 范围伤害: 影响 ${enemies.length} 个敌人, 总伤害 ${totalDamage}`);
  282. return totalDamage;
  283. }
  284. /**
  285. * 判断是否为敌人节点
  286. */
  287. private isEnemyNode(node: Node): boolean {
  288. const name = node.name.toLowerCase();
  289. return name.includes('enemy') ||
  290. name.includes('敌人') ||
  291. node.getComponent('EnemyInstance') !== null;
  292. }
  293. /**
  294. * 生成爆炸特效
  295. */
  296. private spawnExplosionEffect(position: Vec3) {
  297. const path = this.defaultHitEffectPath || 'Animation/tx/tx0004';
  298. this.spawnEffect(path, position, false);
  299. }
  300. /**
  301. * 生成灼烧特效
  302. */
  303. private spawnBurnEffect(parent: Node) {
  304. const path = this.defaultBurnEffectPath || this.defaultTrailEffectPath || 'Animation/tx/tx0006';
  305. this.spawnEffect(path, new Vec3(), true, parent);
  306. }
  307. /**
  308. * 生成特效
  309. */
  310. private spawnEffect(path: string, worldPos: Vec3, loop = false, parent?: Node) {
  311. if (!path) return;
  312. const spawnWithData = (skData: sp.SkeletonData) => {
  313. const effectNode = new Node('Effect');
  314. const skeletonComp: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
  315. skeletonComp.skeletonData = skData;
  316. skeletonComp.setAnimation(0, 'animation', loop);
  317. const targetParent = parent || find('Canvas/GameLevelUI/GameArea') || find('Canvas');
  318. if (targetParent) {
  319. targetParent.addChild(effectNode);
  320. if (parent) {
  321. effectNode.setPosition(0, 0, 0);
  322. } else {
  323. const parentTrans = targetParent.getComponent(UITransform);
  324. if (parentTrans) {
  325. const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
  326. effectNode.position = localPos;
  327. }
  328. }
  329. }
  330. if (!loop) {
  331. skeletonComp.setCompleteListener(() => {
  332. effectNode.destroy();
  333. });
  334. }
  335. };
  336. // 先尝试直接加载给定路径
  337. resources.load(path, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
  338. if (err) {
  339. console.warn('加载特效失败:', path, err);
  340. return;
  341. }
  342. spawnWithData(skData);
  343. });
  344. }
  345. /**
  346. * 清理资源
  347. */
  348. onDestroy() {
  349. // 清理激活的灼烧区域
  350. for (const burnArea of this.activeBurnAreas) {
  351. if (burnArea && burnArea.isValid) {
  352. burnArea.destroy();
  353. }
  354. }
  355. this.activeBurnAreas = [];
  356. }
  357. /**
  358. * 获取命中统计
  359. */
  360. public getHitStats() {
  361. return {
  362. hitCount: this.hitCount,
  363. pierceCount: this.pierceCount,
  364. ricochetCount: this.ricochetCount
  365. };
  366. }
  367. /**
  368. * 验证配置
  369. */
  370. public static validateConfig(effects: HitEffectConfig[]): boolean {
  371. if (!Array.isArray(effects) || effects.length === 0) return false;
  372. for (const effect of effects) {
  373. if (!effect.type || effect.priority < 0) return false;
  374. if (!effect.params) return false;
  375. }
  376. return true;
  377. }
  378. private spawnHitEffect(worldPos: Vec3) {
  379. const path = this.defaultHitEffectPath;
  380. if (path) {
  381. this.spawnEffect(path, worldPos, false);
  382. }
  383. }
  384. }