|
|
@@ -1,20 +1,24 @@
|
|
|
import { _decorator, Component, Node, Vec2, Vec3, RigidBody2D, Collider2D, Contact2DType, IPhysics2DContact, find, Prefab, instantiate, UITransform, resources, sp } from 'cc';
|
|
|
+import { WeaponBlockExample } from './WeaponBlockExample';
|
|
|
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'
|
|
|
- * });
|
|
|
+ * WeaponBullet - 统一的子弹控制器
|
|
|
+ *
|
|
|
+ * 整合了原 BulletController 的所有功能:
|
|
|
+ * 1. 自动瞄准最近敌人
|
|
|
+ * 2. 物理属性设置
|
|
|
+ * 3. 生命周期管理
|
|
|
+ * 4. 特效系统
|
|
|
+ * 5. 多种子弹行为
|
|
|
+ *
|
|
|
+ * 支持的子弹行为:
|
|
|
+ * - explosive: 击中敌人立即销毁并播放hitEffect
|
|
|
+ * - piercing: 击中敌人不销毁,持续飞行
|
|
|
+ * - ricochet: 弹射子弹,可设置弹射次数
|
|
|
+ * - boomerang: 回旋镖,延迟后返回
|
|
|
+ * - area_burn: 区域灼烧效果
|
|
|
+ * - homing: 导弹追踪
|
|
|
*/
|
|
|
|
|
|
export type BulletBehaviour = 'explosive' | 'piercing' | 'ricochet' | 'boomerang' | 'area_burn' | 'homing' | 'normal';
|
|
|
@@ -24,7 +28,7 @@ export interface BulletInitData {
|
|
|
damage: number;
|
|
|
hitEffect?: string; // resources 路径
|
|
|
trailEffect?: string; // resources 路径
|
|
|
- lifetime?: number; // piercing 子弹有效时间
|
|
|
+ lifetime?: number; // 子弹生存时间
|
|
|
ricochetCount?: number; // ricochet 弹射次数
|
|
|
ricochetAngle?: number; // 每次弹射随机角度 (deg)
|
|
|
arcHeight?: number; // boomerang
|
|
|
@@ -32,16 +36,34 @@ export interface BulletInitData {
|
|
|
burnDuration?: number; // area burn
|
|
|
homingDelay?: number; // homing missile
|
|
|
homingStrength?: number;
|
|
|
+ // 从 BulletController 迁移的配置
|
|
|
+ firePosition?: Vec3; // 发射位置(世界坐标)
|
|
|
+ autoTarget?: boolean; // 是否自动瞄准最近敌人
|
|
|
}
|
|
|
|
|
|
@ccclass('WeaponBullet')
|
|
|
export class WeaponBullet extends Component {
|
|
|
+ // === 基础配置 ===
|
|
|
+ @property
|
|
|
+ public speed: number = 300;
|
|
|
+
|
|
|
+ @property
|
|
|
+ public damage: number = 1;
|
|
|
+
|
|
|
+ @property
|
|
|
+ public lifetime: number = 5;
|
|
|
+
|
|
|
+ @property({ tooltip: '击中特效路径(resources 相对路径,例如 Animation/tx/tx0001)' })
|
|
|
+ public hitEffectPath: string = '';
|
|
|
+
|
|
|
+ @property({ tooltip: '拖尾特效路径' })
|
|
|
+ public trailEffectPath: string = '';
|
|
|
+
|
|
|
+ @property({ tooltip: '是否自动瞄准最近敌人' })
|
|
|
+ public autoTarget: boolean = true;
|
|
|
+
|
|
|
+ // === 子弹行为配置 ===
|
|
|
private behavior: BulletBehaviour = 'explosive';
|
|
|
- private speed = 300;
|
|
|
- private damage = 1;
|
|
|
- private hitEffectPath = '';
|
|
|
- private trailEffectPath = '';
|
|
|
- private lifetime = 5;
|
|
|
private ricochetLeft = 0;
|
|
|
private ricochetAngle = 30; // deg
|
|
|
|
|
|
@@ -58,71 +80,314 @@ export class WeaponBullet extends Component {
|
|
|
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){
|
|
|
+ // === 从 BulletController 迁移的属性 ===
|
|
|
+ private targetEnemy: Node = null;
|
|
|
+ private rigidBody: RigidBody2D = null;
|
|
|
+ private lifeTimer: number = 0;
|
|
|
+ private direction: Vec3 = new Vec3(1, 0, 0);
|
|
|
+ private firePosition: Vec3 = null;
|
|
|
+ private isInitialized: boolean = false;
|
|
|
+ private needsPhysicsSetup: boolean = false;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化子弹配置
|
|
|
+ */
|
|
|
+ public init(cfg: BulletInitData) {
|
|
|
+ this.behavior = cfg.behavior || 'explosive';
|
|
|
+ this.speed = cfg.speed || this.speed;
|
|
|
+ this.damage = cfg.damage || this.damage;
|
|
|
+ this.hitEffectPath = cfg.hitEffect || this.hitEffectPath;
|
|
|
+ this.trailEffectPath = cfg.trailEffect || this.trailEffectPath;
|
|
|
+ this.lifetime = cfg.lifetime !== undefined ? cfg.lifetime : this.lifetime;
|
|
|
+ this.autoTarget = cfg.autoTarget !== undefined ? cfg.autoTarget : this.autoTarget;
|
|
|
+
|
|
|
+ // 子弹行为配置
|
|
|
+ if (cfg.ricochetCount !== undefined) this.ricochetLeft = cfg.ricochetCount;
|
|
|
+ if (cfg.ricochetAngle) this.ricochetAngle = cfg.ricochetAngle;
|
|
|
+ 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;
|
|
|
+
|
|
|
+ // 发射位置设置
|
|
|
+ if (cfg.firePosition) {
|
|
|
+ this.setFirePosition(cfg.firePosition);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 兼容性处理
|
|
|
+ if (this.behavior === 'normal') this.behavior = 'explosive';
|
|
|
+
|
|
|
+ console.log('🔫 WeaponBullet 初始化完成:', {
|
|
|
+ behavior: this.behavior,
|
|
|
+ speed: this.speed,
|
|
|
+ damage: this.damage,
|
|
|
+ autoTarget: this.autoTarget
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置子弹的发射位置(从 BulletController 迁移)
|
|
|
+ * @param position 发射位置(世界坐标)
|
|
|
+ */
|
|
|
+ public setFirePosition(position: Vec3) {
|
|
|
+ this.firePosition = position.clone();
|
|
|
+ this.needsPhysicsSetup = true;
|
|
|
+ console.log('🎯 子弹发射位置已设置:', this.firePosition);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置特效路径(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ public setEffectPaths(hit: string | null, trail?: string | null) {
|
|
|
+ if (hit) this.hitEffectPath = hit;
|
|
|
+ if (trail) this.trailEffectPath = trail;
|
|
|
+ }
|
|
|
+
|
|
|
+ onLoad() {
|
|
|
+ this.rigidBody = this.getComponent(RigidBody2D);
|
|
|
+ const collider = this.getComponent(Collider2D);
|
|
|
+ if (collider) {
|
|
|
+ collider.on(Contact2DType.BEGIN_CONTACT, this.onHit, this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ start() {
|
|
|
+ console.log('🚀 WeaponBullet start()开始...');
|
|
|
+
|
|
|
+ // 设置生命周期
|
|
|
+ this.lifeTimer = this.lifetime;
|
|
|
+
|
|
|
+ // 如果需要设置物理属性,延迟处理
|
|
|
+ if (this.needsPhysicsSetup) {
|
|
|
+ this.scheduleOnce(() => {
|
|
|
+ this.setupPhysics();
|
|
|
+ }, 0.05);
|
|
|
+ } else {
|
|
|
+ // 直接初始化
|
|
|
+ this.setupPhysics();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加拖尾特效
|
|
|
+ if (this.behavior === 'piercing' && this.trailEffectPath) {
|
|
|
this.attachTrail();
|
|
|
}
|
|
|
+
|
|
|
+ console.log('✅ WeaponBullet start()完成');
|
|
|
}
|
|
|
|
|
|
- start(){
|
|
|
- if(this.rb){
|
|
|
- const v = new Vec2(this.dir.x*this.speed, this.dir.y*this.speed);
|
|
|
- this.rb.linearVelocity = v;
|
|
|
+ /**
|
|
|
+ * 设置物理属性(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private setupPhysics() {
|
|
|
+ console.log('🔧 开始设置子弹物理属性...');
|
|
|
+
|
|
|
+ // 确保有刚体组件
|
|
|
+ if (!this.rigidBody) {
|
|
|
+ console.error('❌ 子弹预制体缺少RigidBody2D组件');
|
|
|
+ return;
|
|
|
}
|
|
|
- this.timer = this.lifetime;
|
|
|
- this.initialDir.set(this.dir);
|
|
|
+
|
|
|
+ const collider = this.getComponent(Collider2D);
|
|
|
+ if (!collider) {
|
|
|
+ console.error('❌ 子弹预制体缺少Collider2D组件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置物理属性
|
|
|
+ this.rigidBody.enabledContactListener = true;
|
|
|
+ this.rigidBody.gravityScale = 0; // 不受重力影响
|
|
|
+ this.rigidBody.linearDamping = 0; // 无阻尼
|
|
|
+ this.rigidBody.angularDamping = 0; // 无角阻尼
|
|
|
+
|
|
|
+ console.log('✅ 物理组件设置成功');
|
|
|
+
|
|
|
+ // 设置子弹位置
|
|
|
+ if (this.firePosition) {
|
|
|
+ this.setPositionInGameArea();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化方向和速度
|
|
|
+ this.initializeDirection();
|
|
|
+ this.isInitialized = true;
|
|
|
+
|
|
|
+ console.log('✅ 子弹物理属性设置完成');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在GameArea中设置子弹位置(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private setPositionInGameArea() {
|
|
|
+ const gameArea = find('Canvas/GameLevelUI/GameArea');
|
|
|
+ if (gameArea) {
|
|
|
+ const gameAreaTransform = gameArea.getComponent(UITransform);
|
|
|
+ if (gameAreaTransform) {
|
|
|
+ const localPos = gameAreaTransform.convertToNodeSpaceAR(this.firePosition);
|
|
|
+ this.node.position = localPos;
|
|
|
+ console.log('✅ 子弹位置已设置:', localPos);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化子弹方向(从 BulletController 迁移并增强)
|
|
|
+ */
|
|
|
+ private initializeDirection() {
|
|
|
+ console.log('🎯 开始初始化子弹方向...');
|
|
|
+
|
|
|
+ // 如果启用自动瞄准,寻找最近的敌人
|
|
|
+ if (this.autoTarget) {
|
|
|
+ this.findNearestEnemy();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置方向
|
|
|
+ if (this.targetEnemy && this.autoTarget) {
|
|
|
+ this.setDirectionToTarget();
|
|
|
+ } else {
|
|
|
+ // 随机方向或使用默认方向
|
|
|
+ const randomAngle = Math.random() * Math.PI * 2;
|
|
|
+ this.direction = new Vec3(Math.cos(randomAngle), Math.sin(randomAngle), 0);
|
|
|
+ console.log('🎲 使用随机方向');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存初始方向(用于boomerang)
|
|
|
+ this.initialDir.set(this.direction);
|
|
|
+
|
|
|
+ // 应用速度
|
|
|
+ this.applyVelocity();
|
|
|
}
|
|
|
|
|
|
- private onHit(self:Collider2D, other:Collider2D, contact:IPhysics2DContact|null){
|
|
|
- // 仅处理敌人
|
|
|
- const node = other.node;
|
|
|
- const isEnemy = node.getComponent('EnemyInstance')||node.getComponent('EnemyComponent');
|
|
|
- if(!isEnemy) return;
|
|
|
+ /**
|
|
|
+ * 寻找最近的敌人(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private findNearestEnemy() {
|
|
|
+ const enemyContainer = find('Canvas/GameLevelUI/enemyContainer');
|
|
|
+ if (!enemyContainer) {
|
|
|
+ console.log('❌ 未找到敌人容器');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const enemies = enemyContainer.children.filter(child =>
|
|
|
+ child.active &&
|
|
|
+ (child.name.toLowerCase().includes('enemy') ||
|
|
|
+ child.getComponent('EnemyInstance') !== null)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (enemies.length === 0) {
|
|
|
+ console.log('❌ 没有找到敌人');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let nearestEnemy: Node = null;
|
|
|
+ let nearestDistance = Infinity;
|
|
|
+ const bulletPos = this.node.worldPosition;
|
|
|
+
|
|
|
+ for (const enemy of enemies) {
|
|
|
+ const distance = Vec3.distance(bulletPos, enemy.worldPosition);
|
|
|
+ if (distance < nearestDistance) {
|
|
|
+ nearestDistance = distance;
|
|
|
+ nearestEnemy = enemy;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nearestEnemy) {
|
|
|
+ this.targetEnemy = nearestEnemy;
|
|
|
+ console.log(`🎯 锁定目标: ${nearestEnemy.name}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 伤害
|
|
|
- const ei = node.getComponent('EnemyInstance') as any;
|
|
|
- if(ei && typeof ei.takeDamage==='function') ei.takeDamage(this.damage);
|
|
|
+ /**
|
|
|
+ * 设置朝向目标的方向(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private setDirectionToTarget() {
|
|
|
+ if (!this.targetEnemy) return;
|
|
|
+
|
|
|
+ const targetPos = this.targetEnemy.worldPosition;
|
|
|
+ const currentPos = this.node.worldPosition;
|
|
|
+
|
|
|
+ this.direction = targetPos.clone().subtract(currentPos).normalize();
|
|
|
+ console.log('🎯 方向已设置,朝向目标敌人');
|
|
|
+ }
|
|
|
|
|
|
- // 播放击中特效
|
|
|
- this.spawnEffect(this.hitEffectPath, node.worldPosition);
|
|
|
+ /**
|
|
|
+ * 应用速度(从 BulletController 迁移并增强)
|
|
|
+ */
|
|
|
+ private applyVelocity() {
|
|
|
+ if (!this.rigidBody || !this.direction) {
|
|
|
+ console.error('❌ 无法应用速度:缺少刚体或方向');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 覆盖速度值(全局控制)
|
|
|
+ const weaponGlobal = WeaponBlockExample.getInstance && WeaponBlockExample.getInstance();
|
|
|
+ if (weaponGlobal) {
|
|
|
+ this.speed = weaponGlobal.getCurrentBulletSpeed();
|
|
|
+ }
|
|
|
+
|
|
|
+ const velocity = new Vec2(
|
|
|
+ this.direction.x * this.speed,
|
|
|
+ this.direction.y * this.speed
|
|
|
+ );
|
|
|
+
|
|
|
+ this.rigidBody.linearVelocity = velocity;
|
|
|
+ console.log('✅ 子弹速度已应用:', velocity);
|
|
|
+ }
|
|
|
|
|
|
- switch(this.behavior){
|
|
|
+ /**
|
|
|
+ * 碰撞处理(整合原有逻辑)
|
|
|
+ */
|
|
|
+ private onHit(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
|
|
|
+ const otherNode = otherCollider.node;
|
|
|
+ console.log('💥 子弹碰撞:', otherNode.name);
|
|
|
+
|
|
|
+ // 获取碰撞世界坐标
|
|
|
+ let contactWorldPos: Vec3 = null;
|
|
|
+ if (contact && (contact as any).getWorldManifold) {
|
|
|
+ const wm = (contact as any).getWorldManifold();
|
|
|
+ if (wm && wm.points && wm.points.length > 0) {
|
|
|
+ contactWorldPos = new Vec3(wm.points[0].x, wm.points[0].y, 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!contactWorldPos) {
|
|
|
+ contactWorldPos = otherNode.worldPosition.clone();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否击中敌人
|
|
|
+ if (this.isEnemyNode(otherNode)) {
|
|
|
+ console.log('🎯 击中敌人:', otherNode.name);
|
|
|
+
|
|
|
+ // 对敌人造成伤害
|
|
|
+ this.damageEnemy(otherNode);
|
|
|
+
|
|
|
+ // 播放击中特效
|
|
|
+ this.spawnHitEffect(contactWorldPos);
|
|
|
+
|
|
|
+ // 根据子弹行为处理
|
|
|
+ this.handleBehaviorOnHit(otherNode, contactWorldPos);
|
|
|
+ } else if (otherNode.name.toLowerCase().includes('wall')) {
|
|
|
+ console.log('🧱 击中墙体,销毁子弹');
|
|
|
+ this.node.destroy();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据子弹行为处理击中逻辑
|
|
|
+ */
|
|
|
+ private handleBehaviorOnHit(enemyNode: Node, contactPos: Vec3) {
|
|
|
+ switch (this.behavior) {
|
|
|
case 'explosive':
|
|
|
this.node.destroy();
|
|
|
break;
|
|
|
case 'piercing':
|
|
|
- // 继续飞行
|
|
|
+ // 继续飞行,不销毁
|
|
|
break;
|
|
|
case 'ricochet':
|
|
|
- if(this.ricochetLeft>0){
|
|
|
+ if (this.ricochetLeft > 0) {
|
|
|
this.ricochetLeft--;
|
|
|
this.bounceDirection();
|
|
|
- }else{
|
|
|
+ } else {
|
|
|
this.node.destroy();
|
|
|
}
|
|
|
break;
|
|
|
@@ -131,8 +396,8 @@ export class WeaponBullet extends Component {
|
|
|
break;
|
|
|
case 'area_burn':
|
|
|
// 在地面生成灼烧特效
|
|
|
- this.spawnEffect(this.trailEffectPath, node.worldPosition, true);
|
|
|
- this.scheduleOnce(()=>{/* burn area ends */}, this.burnDuration);
|
|
|
+ this.spawnEffect(this.trailEffectPath, contactPos, true);
|
|
|
+ this.scheduleOnce(() => {/* burn area ends */}, this.burnDuration);
|
|
|
this.node.destroy();
|
|
|
break;
|
|
|
case 'homing':
|
|
|
@@ -141,51 +406,147 @@ export class WeaponBullet extends Component {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- update(dt:number){
|
|
|
- if(this.behavior==='piercing'){
|
|
|
- this.timer -= dt;
|
|
|
- if(this.timer<=0){ this.node.destroy(); return; }
|
|
|
+ /**
|
|
|
+ * 判断是否为敌人节点(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private isEnemyNode(node: Node): boolean {
|
|
|
+ const name = node.name.toLowerCase();
|
|
|
+ return name.includes('enemy') ||
|
|
|
+ name.includes('敌人') ||
|
|
|
+ node.getComponent('EnemyInstance') !== null ||
|
|
|
+ node.getComponent('EnemyComponent') !== null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对敌人造成伤害(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private damageEnemy(enemyNode: Node) {
|
|
|
+ console.log('⚔️ 对敌人造成伤害:', this.damage);
|
|
|
+
|
|
|
+ // 尝试调用敌人的受伤方法
|
|
|
+ const enemyInstance = enemyNode.getComponent('EnemyInstance') as any;
|
|
|
+ if (enemyInstance) {
|
|
|
+ if (typeof enemyInstance.takeDamage === 'function') {
|
|
|
+ enemyInstance.takeDamage(this.damage);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (typeof enemyInstance.health === 'number') {
|
|
|
+ enemyInstance.health -= this.damage;
|
|
|
+ if (enemyInstance.health <= 0) {
|
|
|
+ enemyNode.destroy();
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 备用方案:通过EnemyController
|
|
|
+ const enemyController = find('Canvas/GameLevelUI/EnemyController')?.getComponent('EnemyController') as any;
|
|
|
+ if (enemyController && typeof enemyController.damageEnemy === 'function') {
|
|
|
+ enemyController.damageEnemy(enemyNode, this.damage);
|
|
|
}
|
|
|
- // 超界销毁
|
|
|
+ }
|
|
|
+
|
|
|
+ update(dt: number) {
|
|
|
+ // 只有在初始化完成后才进行更新
|
|
|
+ if (!this.isInitialized) return;
|
|
|
+
|
|
|
+ // 生命周期管理
|
|
|
+ this.updateLifetime(dt);
|
|
|
+
|
|
|
+ // 检查是否飞出游戏区域
|
|
|
+ this.checkOutOfBounds();
|
|
|
+
|
|
|
+ // 子弹行为更新
|
|
|
+ this.updateBehavior(dt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新生命周期
|
|
|
+ */
|
|
|
+ private updateLifetime(dt: number) {
|
|
|
+ if (this.behavior === 'piercing' || this.behavior === 'boomerang') {
|
|
|
+ this.lifeTimer -= dt;
|
|
|
+ if (this.lifeTimer <= 0) {
|
|
|
+ this.node.destroy();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查越界(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private checkOutOfBounds() {
|
|
|
const canvas = find('Canvas');
|
|
|
- if(!canvas) return;
|
|
|
- const halfW = canvas.getComponent(UITransform).width/2;
|
|
|
- const halfH = canvas.getComponent(UITransform).height/2;
|
|
|
+ if (!canvas || !this.node || !this.node.isValid) return;
|
|
|
+
|
|
|
+ const uiTransform = canvas.getComponent(UITransform);
|
|
|
+ if (!uiTransform) return;
|
|
|
+
|
|
|
+ const halfWidth = uiTransform.width / 2;
|
|
|
+ const halfHeight = uiTransform.height / 2;
|
|
|
+ const center = canvas.worldPosition;
|
|
|
+
|
|
|
+ const left = center.x - halfWidth;
|
|
|
+ const right = center.x + halfWidth;
|
|
|
+ const bottom = center.y - halfHeight;
|
|
|
+ const top = center.y + halfHeight;
|
|
|
+
|
|
|
+ // 允许子弹稍微超出边界后再销毁,避免边缘误差
|
|
|
+ const margin = 100;
|
|
|
+
|
|
|
const pos = this.node.worldPosition;
|
|
|
- if(Math.abs(pos.x) > halfW+200 || Math.abs(pos.y) > halfH+200){
|
|
|
+ if (pos.x < left - margin || pos.x > right + margin ||
|
|
|
+ pos.y < bottom - margin || pos.y > top + margin) {
|
|
|
this.node.destroy();
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
+ /**
|
|
|
+ * 更新子弹行为
|
|
|
+ */
|
|
|
+ private updateBehavior(dt: number) {
|
|
|
// boomerang返回
|
|
|
- if(this.behavior==='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);
|
|
|
+ if (this.boomerangTimer >= this.boomerangReturnDelay) {
|
|
|
+ this.direction.multiplyScalar(-1);
|
|
|
+ if (this.rigidBody) {
|
|
|
+ this.rigidBody.linearVelocity = new Vec2(
|
|
|
+ this.direction.x * this.speed,
|
|
|
+ this.direction.y * this.speed
|
|
|
+ );
|
|
|
}
|
|
|
this.behavior = 'piercing'; // 之后按穿透逻辑处理
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// homing 导弹
|
|
|
- if(this.behavior==='homing'){
|
|
|
- if(this.homingTimer>0){ this.homingTimer-=dt; }
|
|
|
- else{
|
|
|
+ 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;
|
|
|
+ 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 (d < dist) {
|
|
|
+ dist = d;
|
|
|
+ nearest = e;
|
|
|
+ }
|
|
|
}
|
|
|
- if(nearest){
|
|
|
+ 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);
|
|
|
+ this.direction.lerp(dir, this.homingStrength * dt);
|
|
|
+ if (this.rigidBody) {
|
|
|
+ this.rigidBody.linearVelocity = new Vec2(
|
|
|
+ this.direction.x * this.speed,
|
|
|
+ this.direction.y * this.speed
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -193,43 +554,82 @@ export class WeaponBullet extends Component {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private attachTrail(){
|
|
|
+ /**
|
|
|
+ * 添加拖尾特效
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+ /**
|
|
|
+ * 生成特效(整合并增强)
|
|
|
+ */
|
|
|
+ private spawnEffect(path: string, worldPos: Vec3, loop = false, parent?: Node) {
|
|
|
+ if (!path) return;
|
|
|
+
|
|
|
+ const match = path.match(/tx\/(tx\d{4})/);
|
|
|
+ const code = match ? match[1] : '';
|
|
|
+ const skeletonPath = code ? `Animation/WeaponTx/${code}/${code}` : path;
|
|
|
+
|
|
|
+ console.log('✨ 尝试加载特效:', skeletonPath);
|
|
|
+ resources.load(skeletonPath, sp.SkeletonData, (err, skData: sp.SkeletonData) => {
|
|
|
+ if (err) {
|
|
|
+ console.warn('加载特效失败:', skeletonPath, err);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const effectNode = new Node('Effect');
|
|
|
+ const skeleton: sp.Skeleton = effectNode.addComponent(sp.Skeleton);
|
|
|
+ skeleton.skeletonData = skData;
|
|
|
+ skeleton.setAnimation(0, 'animation', loop);
|
|
|
+
|
|
|
+ const targetParent = parent || find('Canvas/GameLevelUI/GameArea') || find('Canvas');
|
|
|
+ if (targetParent) {
|
|
|
+ targetParent.addChild(effectNode);
|
|
|
+ if (parent) {
|
|
|
+ effectNode.setPosition(0, 0, 0);
|
|
|
+ } else {
|
|
|
+ const parentTrans = targetParent.getComponent(UITransform);
|
|
|
+ if (parentTrans) {
|
|
|
+ const localPos = parentTrans.convertToNodeSpaceAR(worldPos);
|
|
|
+ effectNode.position = localPos;
|
|
|
+ } else {
|
|
|
+ effectNode.setWorldPosition(worldPos.clone());
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- if(!loop){ s.setCompleteListener(()=>n.destroy()); }
|
|
|
+
|
|
|
+ if (!loop) {
|
|
|
+ skeleton.setCompleteListener(() => {
|
|
|
+ effectNode.destroy();
|
|
|
+ });
|
|
|
+ }
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private bounceDirection(){
|
|
|
+ /**
|
|
|
+ * 生成击中特效(从 BulletController 迁移)
|
|
|
+ */
|
|
|
+ private spawnHitEffect(worldPos: Vec3) {
|
|
|
+ this.spawnEffect(this.hitEffectPath, worldPos, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 弹射方向计算
|
|
|
+ */
|
|
|
+ private bounceDirection() {
|
|
|
// 简单随机旋转 ricochetAngle
|
|
|
const sign = Math.random() < 0.5 ? -1 : 1;
|
|
|
- const rad = this.ricochetAngle * (Math.PI/180) * sign;
|
|
|
+ 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);
|
|
|
+ const nx = this.direction.x * cos - this.direction.y * sin;
|
|
|
+ const ny = this.direction.x * sin + this.direction.y * cos;
|
|
|
+ this.direction.set(nx, ny, 0);
|
|
|
+ if (this.rigidBody) {
|
|
|
+ this.rigidBody.linearVelocity = new Vec2(
|
|
|
+ this.direction.x * this.speed,
|
|
|
+ this.direction.y * this.speed
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
}
|