| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- import { _decorator, Component, Node, ProgressBar, Label, Vec3, find, UITransform, Collider2D, Contact2DType, IPhysics2DContact, instantiate, resources, Prefab, JsonAsset, RigidBody2D, ERigidBody2DType, BoxCollider2D, CircleCollider2D } from 'cc';
- import { sp } from 'cc';
- import { DamageNumberAni } from '../Animations/DamageNumberAni';
- import { HPBarAnimation } from '../Animations/HPBarAnimation';
- import { EnemyComponent } from './EnemyComponent';
- import { EnemyAudio } from '../AudioManager/EnemyAudios';
- const { ccclass, property } = _decorator;
- // 前向声明EnemyController接口,避免循环引用
- interface EnemyControllerType {
- gameBounds: {
- left: number;
- right: number;
- top: number;
- bottom: number;
- };
- damageWall: (damage: number) => void;
- getComponent: (componentType: any) => any;
- }
- // 敌人状态枚举
- enum EnemyState {
- MOVING, // 移动中
- ATTACKING, // 攻击中
- DEAD // 死亡
- }
- // 单个敌人实例的组件
- @ccclass('EnemyInstance')
- export class EnemyInstance extends Component {
- // 敌人属性(从配置文件读取)
- public health: number = 0;
- public maxHealth: number = 0;
- public speed: number = 0;
- public attackPower: number = 0;
-
- // 敌人配置ID
- public enemyId: string = '';
-
- // 敌人配置数据
- private enemyConfig: any = null;
-
- // 敌人配置数据库
- private static enemyDatabase: any = null;
-
- // === 新增属性 ===
- /** 是否从上方生成 */
- public spawnFromTop: boolean = true;
- /** 目标 Fence 节点(TopFence / BottomFence) */
- public targetFence: Node | null = null;
-
- // 移动属性
- public movingDirection: number = 1; // 1: 向右, -1: 向左
- public targetY: number = 0; // 目标Y位置
- public changeDirectionTime: number = 0; // 下次改变方向的时间
-
- // 攻击属性
- public attackInterval: number = 0; // 攻击间隔(秒),从配置文件读取
- private attackTimer: number = 0;
-
- // 对控制器的引用
- public controller: EnemyControllerType = null;
-
- // 敌人当前状态
- private state: EnemyState = EnemyState.MOVING;
-
- // 游戏区域中心
- private gameAreaCenter: Vec3 = new Vec3();
-
- // 碰撞的墙体
- private collidedWall: Node = null;
- // 骨骼动画组件
- private skeleton: sp.Skeleton | null = null;
-
- // 血条动画组件
- private hpBarAnimation: HPBarAnimation | null = null;
-
- // 暂停状态标记
- private isPaused: boolean = false;
- start() {
- // 初始化敌人
- this.initializeEnemy();
- }
-
- // 静态方法:加载敌人配置数据库
- public static async loadEnemyDatabase(): Promise<void> {
- if (EnemyInstance.enemyDatabase) return;
-
- return new Promise((resolve, reject) => {
- resources.load('data/enemies', JsonAsset, (err, jsonAsset) => {
- if (err) {
- console.error('[EnemyInstance] 加载敌人配置失败:', err);
- reject(err);
- return;
- }
-
- EnemyInstance.enemyDatabase = jsonAsset.json;
- resolve();
- });
- });
- }
-
- // 设置敌人配置
- public setEnemyConfig(enemyId: string): void {
- this.enemyId = enemyId;
-
- if (!EnemyInstance.enemyDatabase) {
- console.error('[EnemyInstance] 敌人配置数据库未加载');
- return;
- }
-
- // 从数据库中查找敌人配置
- // 修复:enemies.json是直接的数组结构,不需要.enemies包装
- const enemies = EnemyInstance.enemyDatabase;
- this.enemyConfig = enemies.find((enemy: any) => enemy.id === enemyId);
-
- if (!this.enemyConfig) {
- console.error(`[EnemyInstance] 未找到敌人配置: ${enemyId}`);
- return;
- }
-
- // 应用配置到敌人属性
- this.applyEnemyConfig();
- }
-
- // 应用敌人配置到属性
- private applyEnemyConfig(): void {
- if (!this.enemyConfig) return;
-
- // 从stats节点读取基础属性
- const stats = this.enemyConfig.stats || {};
- this.health = stats.health || 30;
- this.maxHealth = stats.maxHealth || this.health;
-
- // 从movement节点读取移动速度
- const movement = this.enemyConfig.movement || {};
- this.speed = movement.speed || 50;
-
- // 从combat节点读取攻击力
- const combat = this.enemyConfig.combat || {};
- this.attackPower = combat.attackDamage || 10;
-
- // 设置攻击间隔
- this.attackInterval = combat.attackCooldown || 2.0;
-
- }
-
- // 获取敌人配置信息
- public getEnemyConfig(): any {
- return this.enemyConfig;
- }
-
- // 获取敌人名称
- public getEnemyName(): string {
- return this.enemyConfig?.name || '未知敌人';
- }
-
- // 获取敌人类型
- public getEnemyType(): string {
- return this.enemyConfig?.type || 'basic';
- }
-
- // 获取敌人稀有度
- public getEnemyRarity(): string {
- return this.enemyConfig?.rarity || 'common';
- }
-
- // 获取金币奖励
- public getGoldReward(): number {
- return this.enemyConfig?.goldReward || 1;
- }
-
- // 初始化敌人
- private initializeEnemy() {
- // 确保血量正确设置
- if (this.maxHealth > 0) {
- this.health = this.maxHealth;
- }
-
- this.state = EnemyState.MOVING;
- // 只有在攻击间隔未设置时才使用默认值
- if (this.attackInterval <= 0) {
- this.attackInterval = 2.0; // 默认攻击间隔
- }
- this.attackTimer = 0;
-
- // 初始化血条动画组件
- this.initializeHPBarAnimation();
-
- // 获取骨骼动画组件
- this.skeleton = this.getComponent(sp.Skeleton);
- this.playWalkAnimation();
-
- // 计算游戏区域中心
- this.calculateGameAreaCenter();
-
- // 初始化碰撞检测
- this.setupCollider();
- }
-
- // 设置碰撞器
- setupCollider() {
- // 检查节点是否有碰撞器
- let collider = this.node.getComponent(Collider2D);
- if (!collider) {
- console.warn(`[EnemyInstance] 敌人节点 ${this.node.name} 没有碰撞器组件`);
- return;
- }
-
- // 确保有RigidBody2D组件,这对于碰撞检测是必需的
- let rigidBody = this.node.getComponent(RigidBody2D);
- if (!rigidBody) {
- console.log(`[EnemyInstance] 为敌人节点 ${this.node.name} 添加RigidBody2D组件`);
- rigidBody = this.node.addComponent(RigidBody2D);
- }
-
- // 设置刚体属性
- if (rigidBody) {
- rigidBody.type = ERigidBody2DType.Dynamic; // 动态刚体
- rigidBody.enabledContactListener = true; // 启用碰撞监听
- rigidBody.gravityScale = 0; // 不受重力影响
- rigidBody.linearDamping = 0; // 无线性阻尼
- rigidBody.angularDamping = 0; // 无角阻尼
- rigidBody.allowSleep = false; // 不允许休眠
- rigidBody.fixedRotation = true; // 固定旋转
- }
-
- // 根据ContentSize自适应碰撞体大小
- this.adaptColliderToContentSize(collider);
-
- // 设置碰撞事件监听
- collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
-
- console.log(`[EnemyInstance] 敌人 ${this.node.name} 碰撞器设置完成,碰撞器启用: ${collider.enabled}, 刚体启用: ${rigidBody?.enabled}`);
- }
-
- /**
- * 根据ContentSize自适应碰撞体大小
- */
- private adaptColliderToContentSize(collider: Collider2D): void {
- const uiTransform = this.node.getComponent(UITransform);
- if (!uiTransform) {
- console.warn(`[EnemyInstance] 敌人节点 ${this.node.name} 没有UITransform组件,无法自适应碰撞体大小`);
- return;
- }
-
- const contentSize = uiTransform.contentSize;
- console.log(`[EnemyInstance] 敌人 ${this.node.name} ContentSize: ${contentSize.width} x ${contentSize.height}`);
-
- // 根据碰撞器类型设置大小
- if (collider instanceof BoxCollider2D) {
- // 方形碰撞器:直接使用ContentSize
- const boxCollider = collider as BoxCollider2D;
- boxCollider.size.width = contentSize.width;
- boxCollider.size.height = contentSize.height;
-
- // 调整偏移量,使碰撞器居中
- const anchorPoint = uiTransform.anchorPoint;
- boxCollider.offset.x = (0.5 - anchorPoint.x) * contentSize.width;
- boxCollider.offset.y = (0.5 - anchorPoint.y) * contentSize.height;
-
- console.log(`[EnemyInstance] BoxCollider2D 已自适应: size(${boxCollider.size.width}, ${boxCollider.size.height}), offset(${boxCollider.offset.x.toFixed(2)}, ${boxCollider.offset.y.toFixed(2)})`);
- } else if (collider instanceof CircleCollider2D) {
- // 圆形碰撞器:使用ContentSize的较小值作为直径
- const circleCollider = collider as CircleCollider2D;
- const radius = Math.min(contentSize.width, contentSize.height) / 2;
- circleCollider.radius = radius;
-
- // 调整偏移量,使碰撞器居中
- const anchorPoint = uiTransform.anchorPoint;
- circleCollider.offset.x = (0.5 - anchorPoint.x) * contentSize.width;
- circleCollider.offset.y = (0.5 - anchorPoint.y) * contentSize.height;
-
- console.log(`[EnemyInstance] CircleCollider2D 已自适应: radius(${radius.toFixed(2)}), offset(${circleCollider.offset.x.toFixed(2)}, ${circleCollider.offset.y.toFixed(2)})`);
- } else {
- console.warn(`[EnemyInstance] 不支持的碰撞器类型: ${collider.constructor.name}`);
- }
- }
-
- // 碰撞开始事件
- onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
- const nodeName = otherCollider.node.name;
-
- // 如果碰到墙体,停止移动并开始攻击
- if (nodeName.includes('Wall') || nodeName.includes('wall') || nodeName.includes('Fence') || nodeName.includes('Jiguang')) {
- this.state = EnemyState.ATTACKING;
- this.attackTimer = 0; // 立即开始攻击
-
- // 切换攻击动画
- this.playAttackAnimation();
- }
- }
-
- // 获取节点路径
- getNodePath(node: Node): string {
- let path = node.name;
- let current = node;
-
- while (current.parent) {
- current = current.parent;
- path = current.name + '/' + path;
- }
-
- return path;
- }
- // 计算游戏区域中心
- private calculateGameAreaCenter() {
- const gameArea = find('Canvas/GameLevelUI/GameArea');
- if (gameArea) {
- this.gameAreaCenter = gameArea.worldPosition;
- }
- }
- /**
- * 初始化血条动画组件
- */
- private initializeHPBarAnimation() {
- const hpBar = this.node.getChildByName('HPBar');
- if (hpBar) {
- // 查找红色和黄色血条节点
- const redBarNode = hpBar.getChildByName('RedBar');
- const yellowBarNode = hpBar.getChildByName('YellowBar');
-
- if (redBarNode && yellowBarNode) {
- // 添加血条动画组件
- this.hpBarAnimation = this.node.addComponent(HPBarAnimation);
- if (this.hpBarAnimation) {
- // 正确设置红色和黄色血条节点引用
- this.hpBarAnimation.redBarNode = redBarNode;
- this.hpBarAnimation.yellowBarNode = yellowBarNode;
- this.hpBarAnimation.hpBarRootNode = hpBar; // 设置HPBar根节点
- console.log(`[EnemyInstance] 血条动画组件已初始化`);
- }
- } else {
- console.warn(`[EnemyInstance] HPBar下未找到RedBar或YellowBar节点,RedBar: ${!!redBarNode}, YellowBar: ${!!yellowBarNode}`);
- }
- } else {
- console.warn(`[EnemyInstance] 未找到HPBar节点,无法初始化血条动画`);
- }
- }
-
- // 更新血量显示
- updateHealthDisplay(showBar: boolean = false) {
- // 确保血量值在有效范围内
- this.health = Math.max(0, Math.min(this.maxHealth, this.health));
-
- const healthProgress = this.maxHealth > 0 ? this.health / this.maxHealth : 0;
-
- console.log(`[EnemyInstance] 更新血量显示: ${this.health}/${this.maxHealth} (${(healthProgress * 100).toFixed(1)}%)`);
-
- // 使用血条动画组件更新血条
- if (this.hpBarAnimation) {
- this.hpBarAnimation.updateProgress(healthProgress, showBar);
- } else {
- // 备用方案:直接更新血条
- const hpBar = this.node.getChildByName('HPBar');
- if (hpBar) {
- const progressBar = hpBar.getComponent(ProgressBar);
- if (progressBar) {
- progressBar.progress = healthProgress;
- }
- // 根据showBar参数控制血条显示
- hpBar.active = showBar;
- }
- }
-
- // 更新血量数字
- const hpLabel = this.node.getChildByName('HPLabel');
- if (hpLabel) {
- const label = hpLabel.getComponent(Label);
- if (label) {
- // 显示整数血量值
- label.string = Math.ceil(this.health).toString();
- }
- }
- }
- // 受到伤害
- takeDamage(damage: number, isCritical: boolean = false) {
- // 如果已经死亡,不再处理伤害
- if (this.state === EnemyState.DEAD) {
- return;
- }
-
- // 确保伤害值为正数
- if (damage <= 0) {
- console.warn(`[EnemyInstance] 无效的伤害值: ${damage}`);
- return;
- }
-
- // 计算新的血量,确保不会低于0
- const oldHealth = this.health;
- const newHealth = Math.max(0, this.health - damage);
- const actualHealthLoss = oldHealth - newHealth; // 实际血量损失
- this.health = newHealth;
-
- // 日志显示武器的真实伤害值,而不是血量差值
- console.log(`[EnemyInstance] 敌人受到伤害: ${damage} (武器伤害), 实际血量损失: ${actualHealthLoss}, 剩余血量: ${this.health}/${this.maxHealth}`);
-
- // 受击音效已移除
-
- // 显示伤害数字动画(在敌人头顶)- 显示武器的真实伤害
- // 优先使用EnemyController节点上的DamageNumberAni组件实例
- if (this.controller) {
- const damageAni = this.controller.getComponent(DamageNumberAni);
- if (damageAni) {
- damageAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
- } else {
- // 如果没有找到组件实例,使用静态方法作为备用
- DamageNumberAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
- }
- } else {
- // 如果没有controller引用,使用静态方法
- DamageNumberAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
- }
-
- // 更新血量显示和动画,受伤时显示血条
- this.updateHealthDisplay(true);
-
- // 如果血量低于等于0,销毁敌人
- if (this.health <= 0) {
- console.log(`[EnemyInstance] 敌人死亡,开始销毁流程`);
- this.state = EnemyState.DEAD;
- this.spawnCoin();
- // 进入死亡流程,禁用碰撞避免重复命中
- const col = this.getComponent(Collider2D);
- if (col) col.enabled = false;
- this.playDeathAnimationAndDestroy();
- }
- }
- onDestroy() {
- console.log(`[EnemyInstance] onDestroy 被调用,准备通知控制器`);
- // 通知控制器 & GameManager
- if (this.controller && typeof (this.controller as any).notifyEnemyDead === 'function') {
- // 检查控制器是否处于清理状态,避免在清理过程中触发游戏事件
- const isClearing = (this.controller as any).isClearing;
- if (isClearing) {
- console.log(`[EnemyInstance] 控制器处于清理状态,跳过死亡通知`);
- return;
- }
- console.log(`[EnemyInstance] 调用 notifyEnemyDead`);
- (this.controller as any).notifyEnemyDead(this.node);
- } else {
- console.warn(`[EnemyInstance] 无法调用 notifyEnemyDead: controller=${!!this.controller}`);
- }
- }
- update(deltaTime: number) {
- // 如果敌人被暂停,则不执行任何更新逻辑
- if (this.isPaused) {
- return;
- }
-
- if (this.state === EnemyState.MOVING) {
- this.updateMovement(deltaTime);
- } else if (this.state === EnemyState.ATTACKING) {
- this.updateAttack(deltaTime);
- }
- // 不再每帧播放攻击动画,避免日志刷屏
- }
-
- // 更新移动逻辑
- private updateMovement(deltaTime: number) {
- // 继续移动到目标墙体
- this.moveTowardsTarget(deltaTime);
- }
- // 移动到目标位置
- private moveTowardsTarget(deltaTime: number) {
- // 使用世界坐标进行移动计算,确保不受父节点坐标系影响
- const currentWorldPos = this.node.worldPosition.clone();
- // 目标世界坐标:优先使用指定的 Fence,其次退化到游戏区域中心
- let targetWorldPos: Vec3;
- if (this.targetFence && this.targetFence.isValid) {
- targetWorldPos = this.targetFence.worldPosition.clone();
- } else {
- targetWorldPos = this.gameAreaCenter.clone();
- }
- const dir = targetWorldPos.subtract(currentWorldPos);
- if (dir.length() === 0) return;
- dir.normalize();
- const moveDistance = this.speed * deltaTime;
- const newWorldPos = currentWorldPos.add(dir.multiplyScalar(moveDistance));
- // 直接设置世界坐标
- this.node.setWorldPosition(newWorldPos);
- }
- // 更新攻击逻辑
- private updateAttack(deltaTime: number) {
- this.attackTimer -= deltaTime;
-
- if (this.attackTimer <= 0) {
- // 执行攻击
- this.performAttack();
-
- // 重置攻击计时器
- this.attackTimer = this.attackInterval;
- }
- }
- // 执行攻击
- private performAttack() {
- if (!this.controller) {
- return;
- }
-
- // 播放攻击音效
- EnemyAudio.playAttackSound(this.enemyConfig);
-
- // 播放攻击动画,动画结束后造成伤害
- this.playAttackAnimationWithDamage();
- }
- // 播放行走动画
- private playWalkAnimation() {
- if (!this.skeleton) return;
- const enemyComp = this.getComponent('EnemyComponent') as any;
- const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
- const walkName = anims.walk ?? 'walk';
- const idleName = anims.idle ?? 'idle';
- if (this.skeleton.findAnimation(walkName)) {
- this.skeleton.setAnimation(0, walkName, true);
- // 行走音效已移除
- } else if (this.skeleton.findAnimation(idleName)) {
- this.skeleton.setAnimation(0, idleName, true);
- }
- }
- // 播放攻击动画
- private playAttackAnimation() {
- if (!this.skeleton) return;
- const enemyComp2 = this.getComponent('EnemyComponent') as any;
- const anims2 = enemyComp2?.getAnimations ? enemyComp2.getAnimations() : {};
- const attackName = anims2.attack ?? 'attack';
- // 移除频繁打印
- if (this.skeleton.findAnimation(attackName)) {
- this.skeleton.setAnimation(0, attackName, true);
- }
- }
- // 播放攻击动画并在动画结束时造成伤害
- private playAttackAnimationWithDamage() {
- if (!this.skeleton) {
- // 如果没有骨骼动画,直接造成伤害
- this.dealDamageToWall();
- return;
- }
-
- const enemyComp = this.getComponent('EnemyComponent') as any;
- const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
- const attackName = anims.attack ?? 'attack';
- const animation = this.skeleton.findAnimation(attackName);
- if (animation) {
- // 播放攻击动画(不循环)
- this.skeleton.setAnimation(0, attackName, false);
-
- // 获取动画时长并在动画结束时造成伤害
- const animationDuration = animation.duration;
- console.log(`[EnemyInstance] 攻击动画时长: ${animationDuration}秒`);
-
- // 使用定时器在动画结束时造成伤害
- this.scheduleOnce(() => {
- this.dealDamageToWall();
- // 动画完成后切换回行走动画
- this.playWalkAnimation();
- }, animationDuration);
- } else {
- // 如果找不到攻击动画,直接造成伤害
- this.dealDamageToWall();
- }
- }
- // 对墙体造成伤害
- private dealDamageToWall() {
- if (this.controller) {
- this.controller.damageWall(this.attackPower);
- }
- }
- private playDeathAnimationAndDestroy() {
- console.log(`[EnemyInstance] 开始播放死亡动画并销毁`);
-
- // 播放死亡音效
- EnemyAudio.playDeathSound(this.enemyConfig);
-
- if (this.skeleton) {
- const enemyComp = this.getComponent('EnemyComponent') as any;
- const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
- const deathName = anims.dead ?? 'dead';
- if (this.skeleton.findAnimation(deathName)) {
- this.skeleton.setAnimation(0, deathName, false);
- // 销毁节点在动画完毕后
- this.skeleton.setCompleteListener(() => {
- this.node.destroy();
- });
- return;
- }
- }
- this.node.destroy();
- }
- private spawnCoin() {
- const ctrl = this.controller as any; // EnemyController
- if (!ctrl?.coinPrefab) return;
- const coin = instantiate(ctrl.coinPrefab);
- find('Canvas')!.addChild(coin); // 放到 UI 层
- const pos = new Vec3();
- this.node.getWorldPosition(pos); // 取死亡敌人的世界坐标
- coin.worldPosition = pos; // 金币就在敌人身上出现
- }
-
- /**
- * 暂停敌人
- */
- public pause(): void {
- this.isPaused = true;
- console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已暂停`);
- }
-
- /**
- * 恢复敌人
- */
- public resume(): void {
- this.isPaused = false;
- console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已恢复`);
- }
-
- /**
- * 检查是否暂停
- */
- public isPausedState(): boolean {
- return this.isPaused;
- }
-
- /**
- * 重置血条状态(满血并隐藏)
- */
- public resetHealthBar(): void {
- if (this.hpBarAnimation) {
- this.hpBarAnimation.resetToFullAndHide();
- } else {
- // 备用方案:直接隐藏血条
- const hpBar = this.node.getChildByName('HPBar');
- if (hpBar) {
- hpBar.active = false;
- const progressBar = hpBar.getComponent(ProgressBar);
- if (progressBar) {
- progressBar.progress = 1.0;
- }
- }
- }
- }
- }
|