import { _decorator, Component, Node, Vec2, Vec3, RigidBody2D, Collider2D, Contact2DType, IPhysics2DContact, find, Prefab, instantiate, UITransform, resources, sp } from 'cc'; const { ccclass, property } = _decorator; /** * WeaponBullet 负责两种子弹行为: * 1) explosive – 击中敌人立即销毁并播放 hitEffect。 * 2) piercing – 击中敌人不销毁,持续飞行;若设置 trailEffect 则在尾部循环播放。 * * 使用方式: * const wb = bullet.addComponent(WeaponBullet); * wb.init({ * behavior : 'explosive' | 'piercing', * speed : number, * damage : number, * hitEffect: 'Animation/tx/tx000X', * trailEffect?: 'Animation/tx/tx000X' * }); */ export type BulletBehaviour = 'explosive' | 'piercing' | 'ricochet' | 'boomerang' | 'area_burn' | 'homing' | 'normal'; export interface BulletInitData { behavior: BulletBehaviour; speed: number; damage: number; hitEffect?: string; // resources 路径 trailEffect?: string; // resources 路径 lifetime?: number; // piercing 子弹有效时间 ricochetCount?: number; // ricochet 弹射次数 ricochetAngle?: number; // 每次弹射随机角度 (deg) arcHeight?: number; // boomerang returnDelay?: number; burnDuration?: number; // area burn homingDelay?: number; // homing missile homingStrength?: number; } @ccclass('WeaponBullet') export class WeaponBullet extends Component { private behavior: BulletBehaviour = 'explosive'; private speed = 300; private damage = 1; private hitEffectPath = ''; private trailEffectPath = ''; private lifetime = 5; private ricochetLeft = 0; private ricochetAngle = 30; // deg // boomerang private boomerangTimer = 0; private boomerangReturnDelay = 1; private initialDir: Vec3 = new Vec3(); // area burn private burnDuration = 5; // homing private homingDelay = 0.3; private homingStrength = 0.8; private homingTimer = 0; private dir: Vec3 = new Vec3(1,0,0); private rb: RigidBody2D = null; private timer = 0; public init(cfg: BulletInitData){ this.behavior = cfg.behavior; this.speed = cfg.speed; this.damage = cfg.damage; this.hitEffectPath = cfg.hitEffect || ''; this.trailEffectPath = cfg.trailEffect || ''; if(cfg.lifetime) this.lifetime = cfg.lifetime; if(cfg.ricochetCount!==undefined) this.ricochetLeft = cfg.ricochetCount; if(cfg.ricochetAngle) this.ricochetAngle = cfg.ricochetAngle; if(this.behavior==='normal') this.behavior='explosive'; if(cfg.arcHeight){ /* placeholder visual only */ } if(cfg.returnDelay){ this.boomerangReturnDelay = cfg.returnDelay; } if(cfg.burnDuration){ this.burnDuration = cfg.burnDuration; } if(cfg.homingDelay){ this.homingDelay = cfg.homingDelay; this.homingTimer = cfg.homingDelay; } if(cfg.homingStrength){ this.homingStrength = cfg.homingStrength; } } onLoad(){ this.rb = this.getComponent(RigidBody2D); const col = this.getComponent(Collider2D); if(col){ col.on(Contact2DType.BEGIN_CONTACT,this.onHit,this); } // 添加拖尾 if(this.behavior==='piercing' && this.trailEffectPath){ this.attachTrail(); } } start(){ if(this.rb){ const v = new Vec2(this.dir.x*this.speed, this.dir.y*this.speed); this.rb.linearVelocity = v; } this.timer = this.lifetime; this.initialDir.set(this.dir); } private onHit(self:Collider2D, other:Collider2D, contact:IPhysics2DContact|null){ // 仅处理敌人 const node = other.node; const isEnemy = node.getComponent('EnemyInstance')||node.getComponent('EnemyComponent'); if(!isEnemy) return; // 伤害 const ei = node.getComponent('EnemyInstance') as any; if(ei && typeof ei.takeDamage==='function') ei.takeDamage(this.damage); // 播放击中特效 this.spawnEffect(this.hitEffectPath, node.worldPosition); switch(this.behavior){ case 'explosive': this.node.destroy(); break; case 'piercing': // 继续飞行 break; case 'ricochet': if(this.ricochetLeft>0){ this.ricochetLeft--; this.bounceDirection(); }else{ this.node.destroy(); } break; case 'boomerang': // 命中后照样返程,由 boomerangTimer 处理,不销毁 break; case 'area_burn': // 在地面生成灼烧特效 this.spawnEffect(this.trailEffectPath, node.worldPosition, true); this.scheduleOnce(()=>{/* burn area ends */}, this.burnDuration); this.node.destroy(); break; case 'homing': this.node.destroy(); break; } } update(dt:number){ if(this.behavior==='piercing'){ this.timer -= dt; if(this.timer<=0){ this.node.destroy(); return; } } // 超界销毁 const canvas = find('Canvas'); if(!canvas) return; const halfW = canvas.getComponent(UITransform).width/2; const halfH = canvas.getComponent(UITransform).height/2; const pos = this.node.worldPosition; if(Math.abs(pos.x) > halfW+200 || Math.abs(pos.y) > halfH+200){ this.node.destroy(); } // boomerang返回 if(this.behavior==='boomerang'){ this.boomerangTimer += dt; if(this.boomerangTimer>=this.boomerangReturnDelay){ this.dir.multiplyScalar(-1); if(this.rb){ this.rb.linearVelocity = new Vec2(this.dir.x*this.speed, this.dir.y*this.speed); } this.behavior = 'piercing'; // 之后按穿透逻辑处理 } } // homing 导弹 if(this.behavior==='homing'){ if(this.homingTimer>0){ this.homingTimer-=dt; } else{ const enemyContainer = find('Canvas/GameLevelUI/enemyContainer'); if(enemyContainer && enemyContainer.children.length>0){ let nearest:Node=null; let dist=1e9; for(const e of enemyContainer.children){ if(!e.active) continue; const d = Vec3.distance(this.node.worldPosition, e.worldPosition); if(d{ if(err) {console.warn('load effect fail', skPath); return;} const n = new Node('Fx'); const s = n.addComponent(sp.Skeleton); s.skeletonData = sk; s.setAnimation(0,'animation',loop); const targetParent = parent||find('Canvas/GameLevelUI/GameArea')||find('Canvas'); if(targetParent){ targetParent.addChild(n); if(parent){ n.setPosition(0,0,0);}else{ const trans = targetParent.getComponent(UITransform); n.position = trans?trans.convertToNodeSpaceAR(worldPos):worldPos; } } if(!loop){ s.setCompleteListener(()=>n.destroy()); } }); } private bounceDirection(){ // 简单随机旋转 ricochetAngle const sign = Math.random() < 0.5 ? -1 : 1; const rad = this.ricochetAngle * (Math.PI/180) * sign; const cos = Math.cos(rad), sin = Math.sin(rad); const nx = this.dir.x * cos - this.dir.y * sin; const ny = this.dir.x * sin + this.dir.y * cos; this.dir.set(nx, ny, 0); if(this.rb){ this.rb.linearVelocity = new Vec2(this.dir.x*this.speed, this.dir.y*this.speed); } } }