BulletHitEffect.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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 enemyInstance = enemyNode.getComponent('EnemyInstance') as any;
  236. if (enemyInstance) {
  237. if (typeof enemyInstance.takeDamage === 'function') {
  238. enemyInstance.takeDamage(damage);
  239. return;
  240. }
  241. if (typeof enemyInstance.health === 'number') {
  242. enemyInstance.health -= damage;
  243. if (enemyInstance.health <= 0) {
  244. enemyNode.destroy();
  245. }
  246. return;
  247. }
  248. }
  249. // 备用方案:通过EnemyController
  250. const enemyController = find('Canvas/GameLevelUI/EnemyController')?.getComponent('EnemyController') as any;
  251. if (enemyController && typeof enemyController.damageEnemy === 'function') {
  252. enemyController.damageEnemy(enemyNode, damage);
  253. }
  254. }
  255. /**
  256. * 对范围内敌人造成伤害
  257. */
  258. private damageEnemiesInRadius(center: Vec3, radius: number, damage: number): number {
  259. const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
  260. if (!enemyContainer) return 0;
  261. let totalDamage = 0;
  262. const enemies = enemyContainer.children.filter(child =>
  263. child.active && this.isEnemyNode(child)
  264. );
  265. for (const enemy of enemies) {
  266. const distance = Vec3.distance(center, enemy.worldPosition);
  267. if (distance <= radius) {
  268. this.damageEnemy(enemy, damage);
  269. totalDamage += damage;
  270. }
  271. }
  272. return totalDamage;
  273. }
  274. /**
  275. * 判断是否为敌人节点
  276. */
  277. private isEnemyNode(node: Node): boolean {
  278. const name = node.name.toLowerCase();
  279. return name.includes('enemy') ||
  280. name.includes('敌人') ||
  281. node.getComponent('EnemyInstance') !== null;
  282. }
  283. /**
  284. * 生成爆炸特效
  285. */
  286. private spawnExplosionEffect(position: Vec3) {
  287. const path = this.defaultHitEffectPath || 'Animation/tx/tx0004';
  288. this.spawnEffect(path, position, false);
  289. }
  290. /**
  291. * 生成灼烧特效
  292. */
  293. private spawnBurnEffect(parent: Node) {
  294. const path = this.defaultBurnEffectPath || this.defaultTrailEffectPath || 'Animation/tx/tx0006';
  295. this.spawnEffect(path, new Vec3(), true, parent);
  296. }
  297. /**
  298. * 生成特效
  299. */
  300. private spawnEffect(path: string, worldPos: Vec3, loop = false, parent?: Node) {
  301. if (!path) return;
  302. const spawnWithData = (skData: sp.SkeletonData) => {
  303. const effectNode = new Node('Effect');
  304. const skeletonComp: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
  305. skeletonComp.skeletonData = skData;
  306. skeletonComp.setAnimation(0, 'animation', loop);
  307. const targetParent = parent || find('Canvas/GameLevelUI/GameArea') || find('Canvas');
  308. if (targetParent) {
  309. targetParent.addChild(effectNode);
  310. if (parent) {
  311. effectNode.setPosition(0, 0, 0);
  312. } else {
  313. const parentTrans = targetParent.getComponent(UITransform);
  314. if (parentTrans) {
  315. const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
  316. effectNode.position = localPos;
  317. }
  318. }
  319. }
  320. if (!loop) {
  321. skeletonComp.setCompleteListener(() => {
  322. effectNode.destroy();
  323. });
  324. }
  325. };
  326. // 先尝试直接加载给定路径
  327. resources.load(path, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
  328. if (err) {
  329. console.warn('加载特效失败:', path, err);
  330. return;
  331. }
  332. spawnWithData(skData);
  333. });
  334. }
  335. /**
  336. * 清理资源
  337. */
  338. onDestroy() {
  339. // 清理激活的灼烧区域
  340. for (const burnArea of this.activeBurnAreas) {
  341. if (burnArea && burnArea.isValid) {
  342. burnArea.destroy();
  343. }
  344. }
  345. this.activeBurnAreas = [];
  346. }
  347. /**
  348. * 获取命中统计
  349. */
  350. public getHitStats() {
  351. return {
  352. hitCount: this.hitCount,
  353. pierceCount: this.pierceCount,
  354. ricochetCount: this.ricochetCount
  355. };
  356. }
  357. /**
  358. * 验证配置
  359. */
  360. public static validateConfig(effects: HitEffectConfig[]): boolean {
  361. if (!Array.isArray(effects) || effects.length === 0) return false;
  362. for (const effect of effects) {
  363. if (!effect.type || effect.priority < 0) return false;
  364. if (!effect.params) return false;
  365. }
  366. return true;
  367. }
  368. private spawnHitEffect(worldPos: Vec3) {
  369. const path = this.defaultHitEffectPath;
  370. if (path) {
  371. this.spawnEffect(path, worldPos, false);
  372. }
  373. }
  374. }