| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- 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<dist){ dist=d; nearest=e; }
- }
- if(nearest){
- const dir = nearest.worldPosition.clone().subtract(this.node.worldPosition).normalize();
- // 线性插值方向
- this.dir.lerp(dir, this.homingStrength*dt);
- if(this.rb){
- this.rb.linearVelocity = new Vec2(this.dir.x*this.speed, this.dir.y*this.speed);
- }
- }
- }
- }
- }
- }
- private attachTrail(){
- this.spawnEffect(this.trailEffectPath, new Vec3(), true, this.node);
- }
- private spawnEffect(path:string, worldPos:Vec3, loop=false, parent?:Node){
- if(!path) return;
- const m = path.match(/tx\/(tx\d{4})/);
- const code = m? m[1]:'';
- const skPath = code?`Animation/WeaponTx/${code}/${code}`:path;
- resources.load(skPath, sp.SkeletonData, (err,sk:sp.SkeletonData)=>{
- 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);
- }
- }
- }
|