BulletEngine.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { _decorator, Component, Node, Vec3, Prefab, instantiate, Collider2D, Contact2DType, IPhysics2DContact, math } from 'cc';
  2. import { BulletInitData, WeaponConfig, BulletLifecycleConfig } from './BulletTypes';
  3. import { ConfigLoader } from './ConfigLoader';
  4. import { BulletCount } from './effects/BulletCount';
  5. import { BulletTrajectory as BCBulletTrajectory } from './effects/BulletTrajectory';
  6. import { BulletHitEffect as BCBulletHitEffect } from './effects/BulletHitEffect';
  7. import { BulletLifecycle as BCBulletLifecycle } from './effects/BulletLifecycle';
  8. import { BulletTrailController } from './BulletTrailController';
  9. const { ccclass } = _decorator;
  10. /** 可移植的统一子弹控制器 */
  11. @ccclass('WeaponBulletController')
  12. export class WeaponBulletController extends Component {
  13. private trajectory: BCBulletTrajectory | null = null;
  14. private hitEffect: BCBulletHitEffect | null = null;
  15. private lifecycle: BCBulletLifecycle | null = null;
  16. private trail: BulletTrailController | null = null;
  17. private weaponConfig: WeaponConfig | null = null;
  18. private isInitialized = false;
  19. // 图片“上为前进方向”的偏移修正(度)
  20. private readonly _orientationOffsetByWeapon: Record<string, number> = {
  21. sharp_carrot: -90,
  22. hot_pepper: -90,
  23. okra_missile: -90,
  24. mace_club: -90
  25. };
  26. private getOrientationOffset(): number {
  27. const id = this.weaponConfig?.id;
  28. if (!id) return 0;
  29. return this._orientationOffsetByWeapon[id] ?? 0;
  30. }
  31. public init(initData: BulletInitData) {
  32. // 装配配置
  33. this.weaponConfig = initData.weaponConfig ?? ConfigLoader.getWeaponConfig(initData.weaponId);
  34. if (!this.weaponConfig) {
  35. this.node.destroy();
  36. return;
  37. }
  38. // 计算方向
  39. const direction = (initData.direction?.clone() ?? new Vec3(1, 0, 0)).normalize();
  40. // 弹道
  41. this.trajectory = this.getComponent(BCBulletTrajectory) || this.addComponent(BCBulletTrajectory);
  42. const trajCfg = { ...this.weaponConfig.bulletConfig.trajectory, speed: this.weaponConfig.stats.bulletSpeed };
  43. this.trajectory.init(trajCfg, direction, initData.firePosition, initData.sourceBlock);
  44. // 初始位置略微前移,避免贴边重叠
  45. const spawnOffset = 30;
  46. const pos = this.node.position.clone();
  47. this.node.setPosition(pos.x + direction.x * spawnOffset, pos.y + direction.y * spawnOffset);
  48. // 命中效果
  49. this.hitEffect = this.getComponent(BCBulletHitEffect) || this.addComponent(BCBulletHitEffect);
  50. this.hitEffect.init(this.weaponConfig.bulletConfig.hitEffects);
  51. // 生命周期
  52. this.lifecycle = this.getComponent(BCBulletLifecycle) || this.addComponent(BCBulletLifecycle);
  53. const lc: BulletLifecycleConfig = { ...this.weaponConfig.bulletConfig.lifecycle };
  54. if (lc.type === 'return_trip' && !lc.maxRange && this.weaponConfig.stats.range) {
  55. lc.maxRange = this.weaponConfig.stats.range;
  56. }
  57. // 透传 pierce/ricochet 初始计数(若命中效果中有)
  58. const pierceInit = this.weaponConfig.bulletConfig.hitEffects.find(e => e.type === 'pierce_damage')?.pierceCount ?? lc.penetration ?? 0;
  59. const ricochetInit = this.weaponConfig.bulletConfig.hitEffects.find(e => e.type === 'ricochet_damage')?.ricochetCount ?? lc.ricochetCount ?? 0;
  60. this.lifecycle.init(lc, initData.firePosition, pierceInit, ricochetInit);
  61. // 拖尾(可选)
  62. this.trail = this.getComponent(BulletTrailController) || this.addComponent(BulletTrailController);
  63. // 旋转对齐外观
  64. const deg = math.toDegree(Math.atan2(direction.y, direction.x)) + this.getOrientationOffset();
  65. const shouldRotate = this.weaponConfig?.bulletConfig?.shouldRotate;
  66. const pelletNode = this.node.getChildByName('Pellet');
  67. if (shouldRotate === false && pelletNode) {
  68. pelletNode.angle = deg;
  69. } else {
  70. this.node.angle = deg;
  71. }
  72. // 启用持续外观对齐/自旋(贴合两种模式)
  73. if (this.trajectory) {
  74. this.trajectory.setFacingOptions({
  75. shouldRotate: shouldRotate,
  76. offsetDeg: this.getOrientationOffset(),
  77. targetChildName: pelletNode ? 'Pellet' : undefined,
  78. spinSpeedDeg: 720
  79. });
  80. }
  81. // 碰撞监听(若存在 Collider2D)
  82. const collider = this.getComponent(Collider2D);
  83. if (collider) {
  84. collider.on(Contact2DType.BEGIN_CONTACT, this.onCollision, this);
  85. }
  86. this.isInitialized = true;
  87. }
  88. private onCollision(self: Collider2D, other: Collider2D, contact: IPhysics2DContact | null) {
  89. if (!this.isInitialized || !this.hitEffect || !this.lifecycle) return;
  90. const result = this.hitEffect.applyOnHit();
  91. this.lifecycle.notifyHit(result);
  92. if (this.lifecycle.shouldDestroyNow()) {
  93. this.node.destroy();
  94. return;
  95. }
  96. }
  97. }
  98. /**
  99. * 工具:批量创建子弹(Prefab 或 Node 模板)
  100. */
  101. export class BulletEngine {
  102. static createBullets(initData: BulletInitData, bulletTemplate: Prefab | Node): Node[] {
  103. // 获取配置
  104. const config = initData.weaponConfig ?? ConfigLoader.getWeaponConfig(initData.weaponId);
  105. if (!config) return [];
  106. const dir = (initData.direction?.clone() ?? new Vec3(1, 0, 0)).normalize();
  107. const spawns = BulletCount.calculateSpawns(config.bulletConfig.count, initData.firePosition, dir);
  108. const nodes: Node[] = [];
  109. for (const s of spawns) {
  110. const node = instantiate(bulletTemplate) as Node;
  111. const ctrl = node.getComponent(WeaponBulletController) || node.addComponent(WeaponBulletController);
  112. // 处理延迟:先禁用,延时后启用
  113. if (s.delayMs > 0) {
  114. node.active = false;
  115. ctrl.init({ ...initData, firePosition: s.position, direction: s.direction, weaponConfig: config });
  116. nodes.push(node);
  117. setTimeout(() => { if (node && node.isValid) node.active = true; }, s.delayMs);
  118. } else {
  119. ctrl.init({ ...initData, firePosition: s.position, direction: s.direction, weaponConfig: config });
  120. nodes.push(node);
  121. }
  122. }
  123. return nodes;
  124. }
  125. }