EnemyInstance.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import { _decorator, Component, Node, ProgressBar, Label, Vec3, find, UITransform, Collider2D, Contact2DType, IPhysics2DContact, instantiate, resources, Prefab, JsonAsset, RigidBody2D, ERigidBody2DType } from 'cc';
  2. import { sp } from 'cc';
  3. import { DamageNumberAni } from '../Animations/DamageNumberAni';
  4. import { HPBarAnimation } from '../Animations/HPBarAnimation';
  5. import { EnemyComponent } from './EnemyComponent';
  6. const { ccclass, property } = _decorator;
  7. // 前向声明EnemyController接口,避免循环引用
  8. interface EnemyControllerType {
  9. gameBounds: {
  10. left: number;
  11. right: number;
  12. top: number;
  13. bottom: number;
  14. };
  15. damageWall: (damage: number) => void;
  16. getComponent: (componentType: any) => any;
  17. }
  18. // 敌人状态枚举
  19. enum EnemyState {
  20. MOVING, // 移动中
  21. ATTACKING, // 攻击中
  22. DEAD // 死亡
  23. }
  24. // 单个敌人实例的组件
  25. @ccclass('EnemyInstance')
  26. export class EnemyInstance extends Component {
  27. // 敌人属性(从配置文件读取)
  28. public health: number = 0;
  29. public maxHealth: number = 0;
  30. public speed: number = 0;
  31. public attackPower: number = 0;
  32. // 敌人配置ID
  33. public enemyId: string = '';
  34. // 敌人配置数据
  35. private enemyConfig: any = null;
  36. // 敌人配置数据库
  37. private static enemyDatabase: any = null;
  38. // === 新增属性 ===
  39. /** 是否从上方生成 */
  40. public spawnFromTop: boolean = true;
  41. /** 目标 Fence 节点(TopFence / BottomFence) */
  42. public targetFence: Node | null = null;
  43. // 移动属性
  44. public movingDirection: number = 1; // 1: 向右, -1: 向左
  45. public targetY: number = 0; // 目标Y位置
  46. public changeDirectionTime: number = 0; // 下次改变方向的时间
  47. // 攻击属性
  48. public attackInterval: number = 0; // 攻击间隔(秒),从配置文件读取
  49. private attackTimer: number = 0;
  50. // 对控制器的引用
  51. public controller: EnemyControllerType = null;
  52. // 敌人当前状态
  53. private state: EnemyState = EnemyState.MOVING;
  54. // 游戏区域中心
  55. private gameAreaCenter: Vec3 = new Vec3();
  56. // 碰撞的墙体
  57. private collidedWall: Node = null;
  58. // 骨骼动画组件
  59. private skeleton: sp.Skeleton | null = null;
  60. // 血条动画组件
  61. private hpBarAnimation: HPBarAnimation | null = null;
  62. // 暂停状态标记
  63. private isPaused: boolean = false;
  64. start() {
  65. // 初始化敌人
  66. this.initializeEnemy();
  67. }
  68. // 静态方法:加载敌人配置数据库
  69. public static async loadEnemyDatabase(): Promise<void> {
  70. if (EnemyInstance.enemyDatabase) return;
  71. return new Promise((resolve, reject) => {
  72. resources.load('data/enemies', JsonAsset, (err, jsonAsset) => {
  73. if (err) {
  74. console.error('[EnemyInstance] 加载敌人配置失败:', err);
  75. reject(err);
  76. return;
  77. }
  78. EnemyInstance.enemyDatabase = jsonAsset.json;
  79. resolve();
  80. });
  81. });
  82. }
  83. // 设置敌人配置
  84. public setEnemyConfig(enemyId: string): void {
  85. this.enemyId = enemyId;
  86. if (!EnemyInstance.enemyDatabase) {
  87. console.error('[EnemyInstance] 敌人配置数据库未加载');
  88. return;
  89. }
  90. // 从数据库中查找敌人配置
  91. // 修复:enemies.json是直接的数组结构,不需要.enemies包装
  92. const enemies = EnemyInstance.enemyDatabase;
  93. this.enemyConfig = enemies.find((enemy: any) => enemy.id === enemyId);
  94. if (!this.enemyConfig) {
  95. console.error(`[EnemyInstance] 未找到敌人配置: ${enemyId}`);
  96. return;
  97. }
  98. // 应用配置到敌人属性
  99. this.applyEnemyConfig();
  100. }
  101. // 应用敌人配置到属性
  102. private applyEnemyConfig(): void {
  103. if (!this.enemyConfig) return;
  104. // 从stats节点读取基础属性
  105. const stats = this.enemyConfig.stats || {};
  106. this.health = stats.health || 30;
  107. this.maxHealth = stats.maxHealth || this.health;
  108. // 从movement节点读取移动速度
  109. const movement = this.enemyConfig.movement || {};
  110. this.speed = movement.speed || 50;
  111. // 从combat节点读取攻击力
  112. const combat = this.enemyConfig.combat || {};
  113. this.attackPower = combat.attackDamage || 10;
  114. // 设置攻击间隔
  115. this.attackInterval = combat.attackCooldown || 2.0;
  116. }
  117. // 获取敌人配置信息
  118. public getEnemyConfig(): any {
  119. return this.enemyConfig;
  120. }
  121. // 获取敌人名称
  122. public getEnemyName(): string {
  123. return this.enemyConfig?.name || '未知敌人';
  124. }
  125. // 获取敌人类型
  126. public getEnemyType(): string {
  127. return this.enemyConfig?.type || 'basic';
  128. }
  129. // 获取敌人稀有度
  130. public getEnemyRarity(): string {
  131. return this.enemyConfig?.rarity || 'common';
  132. }
  133. // 获取金币奖励
  134. public getGoldReward(): number {
  135. return this.enemyConfig?.goldReward || 1;
  136. }
  137. // 初始化敌人
  138. private initializeEnemy() {
  139. // 确保血量正确设置
  140. if (this.maxHealth > 0) {
  141. this.health = this.maxHealth;
  142. }
  143. this.state = EnemyState.MOVING;
  144. // 只有在攻击间隔未设置时才使用默认值
  145. if (this.attackInterval <= 0) {
  146. this.attackInterval = 2.0; // 默认攻击间隔
  147. }
  148. this.attackTimer = 0;
  149. // 初始化血条动画组件
  150. this.initializeHPBarAnimation();
  151. // 获取骨骼动画组件
  152. this.skeleton = this.getComponent(sp.Skeleton);
  153. this.playWalkAnimation();
  154. // 计算游戏区域中心
  155. this.calculateGameAreaCenter();
  156. // 初始化碰撞检测
  157. this.setupCollider();
  158. }
  159. // 设置碰撞器
  160. setupCollider() {
  161. // 检查节点是否有碰撞器
  162. let collider = this.node.getComponent(Collider2D);
  163. if (!collider) {
  164. console.warn(`[EnemyInstance] 敌人节点 ${this.node.name} 没有碰撞器组件`);
  165. return;
  166. }
  167. // 确保有RigidBody2D组件,这对于碰撞检测是必需的
  168. let rigidBody = this.node.getComponent(RigidBody2D);
  169. if (!rigidBody) {
  170. console.log(`[EnemyInstance] 为敌人节点 ${this.node.name} 添加RigidBody2D组件`);
  171. rigidBody = this.node.addComponent(RigidBody2D);
  172. }
  173. // 设置刚体属性
  174. if (rigidBody) {
  175. rigidBody.type = ERigidBody2DType.Dynamic; // 动态刚体
  176. rigidBody.enabledContactListener = true; // 启用碰撞监听
  177. rigidBody.gravityScale = 0; // 不受重力影响
  178. rigidBody.linearDamping = 0; // 无线性阻尼
  179. rigidBody.angularDamping = 0; // 无角阻尼
  180. rigidBody.allowSleep = false; // 不允许休眠
  181. rigidBody.fixedRotation = true; // 固定旋转
  182. }
  183. // 设置碰撞事件监听
  184. collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
  185. console.log(`[EnemyInstance] 敌人 ${this.node.name} 碰撞器设置完成,碰撞器启用: ${collider.enabled}, 刚体启用: ${rigidBody?.enabled}`);
  186. }
  187. // 碰撞开始事件
  188. onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  189. const nodeName = otherCollider.node.name;
  190. // 如果碰到墙体,停止移动并开始攻击
  191. if (nodeName.includes('Wall') || nodeName.includes('wall') || nodeName.includes('Fence') || nodeName.includes('Jiguang')) {
  192. this.state = EnemyState.ATTACKING;
  193. this.attackTimer = 0; // 立即开始攻击
  194. // 切换攻击动画
  195. this.playAttackAnimation();
  196. }
  197. }
  198. // 获取节点路径
  199. getNodePath(node: Node): string {
  200. let path = node.name;
  201. let current = node;
  202. while (current.parent) {
  203. current = current.parent;
  204. path = current.name + '/' + path;
  205. }
  206. return path;
  207. }
  208. // 计算游戏区域中心
  209. private calculateGameAreaCenter() {
  210. const gameArea = find('Canvas/GameLevelUI/GameArea');
  211. if (gameArea) {
  212. this.gameAreaCenter = gameArea.worldPosition;
  213. }
  214. }
  215. /**
  216. * 初始化血条动画组件
  217. */
  218. private initializeHPBarAnimation() {
  219. const hpBar = this.node.getChildByName('HPBar');
  220. if (hpBar) {
  221. // 查找红色和黄色血条节点
  222. const redBarNode = hpBar.getChildByName('RedBar');
  223. const yellowBarNode = hpBar.getChildByName('YellowBar');
  224. if (redBarNode && yellowBarNode) {
  225. // 添加血条动画组件
  226. this.hpBarAnimation = this.node.addComponent(HPBarAnimation);
  227. if (this.hpBarAnimation) {
  228. // 正确设置红色和黄色血条节点引用
  229. this.hpBarAnimation.redBarNode = redBarNode;
  230. this.hpBarAnimation.yellowBarNode = yellowBarNode;
  231. console.log(`[EnemyInstance] 血条动画组件已初始化`);
  232. }
  233. } else {
  234. console.warn(`[EnemyInstance] HPBar下未找到RedBar或YellowBar节点,RedBar: ${!!redBarNode}, YellowBar: ${!!yellowBarNode}`);
  235. }
  236. } else {
  237. console.warn(`[EnemyInstance] 未找到HPBar节点,无法初始化血条动画`);
  238. }
  239. }
  240. // 更新血量显示
  241. updateHealthDisplay() {
  242. // 确保血量值在有效范围内
  243. this.health = Math.max(0, Math.min(this.maxHealth, this.health));
  244. const healthProgress = this.maxHealth > 0 ? this.health / this.maxHealth : 0;
  245. console.log(`[EnemyInstance] 更新血量显示: ${this.health}/${this.maxHealth} (${(healthProgress * 100).toFixed(1)}%)`);
  246. // 使用血条动画组件更新血条
  247. if (this.hpBarAnimation) {
  248. this.hpBarAnimation.updateProgress(healthProgress);
  249. } else {
  250. // 备用方案:直接更新血条
  251. const hpBar = this.node.getChildByName('HPBar');
  252. if (hpBar) {
  253. const progressBar = hpBar.getComponent(ProgressBar);
  254. if (progressBar) {
  255. progressBar.progress = healthProgress;
  256. }
  257. }
  258. }
  259. // 更新血量数字
  260. const hpLabel = this.node.getChildByName('HPLabel');
  261. if (hpLabel) {
  262. const label = hpLabel.getComponent(Label);
  263. if (label) {
  264. // 显示整数血量值
  265. label.string = Math.ceil(this.health).toString();
  266. }
  267. }
  268. }
  269. // 受到伤害
  270. takeDamage(damage: number, isCritical: boolean = false) {
  271. // 如果已经死亡,不再处理伤害
  272. if (this.state === EnemyState.DEAD) {
  273. return;
  274. }
  275. // 确保伤害值为正数
  276. if (damage <= 0) {
  277. console.warn(`[EnemyInstance] 无效的伤害值: ${damage}`);
  278. return;
  279. }
  280. // 计算新的血量,确保不会低于0
  281. const oldHealth = this.health;
  282. const newHealth = Math.max(0, this.health - damage);
  283. const actualHealthLoss = oldHealth - newHealth; // 实际血量损失
  284. this.health = newHealth;
  285. // 日志显示武器的真实伤害值,而不是血量差值
  286. console.log(`[EnemyInstance] 敌人受到伤害: ${damage} (武器伤害), 实际血量损失: ${actualHealthLoss}, 剩余血量: ${this.health}/${this.maxHealth}`);
  287. // 显示伤害数字动画(在敌人头顶)- 显示武器的真实伤害
  288. // 优先使用EnemyController节点上的DamageNumberAni组件实例
  289. if (this.controller) {
  290. const damageAni = this.controller.getComponent(DamageNumberAni);
  291. if (damageAni) {
  292. damageAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
  293. } else {
  294. // 如果没有找到组件实例,使用静态方法作为备用
  295. DamageNumberAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
  296. }
  297. } else {
  298. // 如果没有controller引用,使用静态方法
  299. DamageNumberAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
  300. }
  301. // 更新血量显示和动画
  302. this.updateHealthDisplay();
  303. // 如果血量低于等于0,销毁敌人
  304. if (this.health <= 0) {
  305. console.log(`[EnemyInstance] 敌人死亡,开始销毁流程`);
  306. this.state = EnemyState.DEAD;
  307. this.spawnCoin();
  308. // 进入死亡流程,禁用碰撞避免重复命中
  309. const col = this.getComponent(Collider2D);
  310. if (col) col.enabled = false;
  311. this.playDeathAnimationAndDestroy();
  312. }
  313. }
  314. onDestroy() {
  315. console.log(`[EnemyInstance] onDestroy 被调用,准备通知控制器`);
  316. // 通知控制器 & GameManager
  317. if (this.controller && typeof (this.controller as any).notifyEnemyDead === 'function') {
  318. // 检查控制器是否处于清理状态,避免在清理过程中触发游戏事件
  319. const isClearing = (this.controller as any).isClearing;
  320. if (isClearing) {
  321. console.log(`[EnemyInstance] 控制器处于清理状态,跳过死亡通知`);
  322. return;
  323. }
  324. console.log(`[EnemyInstance] 调用 notifyEnemyDead`);
  325. (this.controller as any).notifyEnemyDead(this.node);
  326. } else {
  327. console.warn(`[EnemyInstance] 无法调用 notifyEnemyDead: controller=${!!this.controller}`);
  328. }
  329. }
  330. update(deltaTime: number) {
  331. // 如果敌人被暂停,则不执行任何更新逻辑
  332. if (this.isPaused) {
  333. return;
  334. }
  335. if (this.state === EnemyState.MOVING) {
  336. this.updateMovement(deltaTime);
  337. } else if (this.state === EnemyState.ATTACKING) {
  338. this.updateAttack(deltaTime);
  339. }
  340. // 不再每帧播放攻击动画,避免日志刷屏
  341. }
  342. // 更新移动逻辑
  343. private updateMovement(deltaTime: number) {
  344. // 检查是否接近游戏区域边界
  345. if (this.checkNearGameArea()) {
  346. this.state = EnemyState.ATTACKING;
  347. this.attackTimer = 0;
  348. this.playAttackAnimation();
  349. return;
  350. }
  351. // 继续移动
  352. this.moveTowardsTarget(deltaTime);
  353. }
  354. // 检查是否接近游戏区域
  355. private checkNearGameArea(): boolean {
  356. const currentPos = this.node.worldPosition;
  357. // 获取游戏区域边界
  358. const gameArea = find('Canvas/GameLevelUI/GameArea');
  359. if (!gameArea) return false;
  360. const uiTransform = gameArea.getComponent(UITransform);
  361. if (!uiTransform) return false;
  362. const gameAreaPos = gameArea.worldPosition;
  363. const halfWidth = uiTransform.width / 2;
  364. const halfHeight = uiTransform.height / 2;
  365. const bounds = {
  366. left: gameAreaPos.x - halfWidth,
  367. right: gameAreaPos.x + halfWidth,
  368. top: gameAreaPos.y + halfHeight,
  369. bottom: gameAreaPos.y - halfHeight
  370. };
  371. // 检查是否在游戏区域内或非常接近
  372. const safeDistance = 50; // 安全距离
  373. const isInside = currentPos.x >= bounds.left - safeDistance &&
  374. currentPos.x <= bounds.right + safeDistance &&
  375. currentPos.y >= bounds.bottom - safeDistance &&
  376. currentPos.y <= bounds.top + safeDistance;
  377. if (isInside) {
  378. return true;
  379. }
  380. return false;
  381. }
  382. // 移动到目标位置
  383. private moveTowardsTarget(deltaTime: number) {
  384. // 使用世界坐标进行移动计算,确保不受父节点坐标系影响
  385. const currentWorldPos = this.node.worldPosition.clone();
  386. // 目标世界坐标:优先使用指定的 Fence,其次退化到游戏区域中心
  387. let targetWorldPos: Vec3;
  388. if (this.targetFence && this.targetFence.isValid) {
  389. targetWorldPos = this.targetFence.worldPosition.clone();
  390. } else {
  391. targetWorldPos = this.gameAreaCenter.clone();
  392. }
  393. const dir = targetWorldPos.subtract(currentWorldPos);
  394. if (dir.length() === 0) return;
  395. dir.normalize();
  396. const moveDistance = this.speed * deltaTime;
  397. const newWorldPos = currentWorldPos.add(dir.multiplyScalar(moveDistance));
  398. // 直接设置世界坐标
  399. this.node.setWorldPosition(newWorldPos);
  400. }
  401. // 更新攻击逻辑
  402. private updateAttack(deltaTime: number) {
  403. this.attackTimer -= deltaTime;
  404. if (this.attackTimer <= 0) {
  405. // 执行攻击
  406. this.performAttack();
  407. // 重置攻击计时器
  408. this.attackTimer = this.attackInterval;
  409. }
  410. }
  411. // 执行攻击
  412. private performAttack() {
  413. if (!this.controller) {
  414. return;
  415. }
  416. // 对墙体造成伤害
  417. this.controller.damageWall(this.attackPower);
  418. }
  419. // 播放行走动画
  420. private playWalkAnimation() {
  421. if (!this.skeleton) return;
  422. const enemyComp = this.getComponent('EnemyComponent') as any;
  423. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  424. const walkName = anims.walk ?? 'walk';
  425. const idleName = anims.idle ?? 'idle';
  426. if (this.skeleton.findAnimation(walkName)) {
  427. this.skeleton.setAnimation(0, walkName, true);
  428. } else if (this.skeleton.findAnimation(idleName)) {
  429. this.skeleton.setAnimation(0, idleName, true);
  430. }
  431. }
  432. // 播放攻击动画
  433. private playAttackAnimation() {
  434. if (!this.skeleton) return;
  435. const enemyComp2 = this.getComponent('EnemyComponent') as any;
  436. const anims2 = enemyComp2?.getAnimations ? enemyComp2.getAnimations() : {};
  437. const attackName = anims2.attack ?? 'attack';
  438. // 移除频繁打印
  439. if (this.skeleton.findAnimation(attackName)) {
  440. this.skeleton.setAnimation(0, attackName, true);
  441. }
  442. }
  443. private playDeathAnimationAndDestroy() {
  444. console.log(`[EnemyInstance] 开始播放死亡动画并销毁`);
  445. if (this.skeleton) {
  446. const enemyComp = this.getComponent('EnemyComponent') as any;
  447. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  448. const deathName = anims.dead ?? 'dead';
  449. if (this.skeleton.findAnimation(deathName)) {
  450. this.skeleton.setAnimation(0, deathName, false);
  451. // 销毁节点在动画完毕后
  452. this.skeleton.setCompleteListener(() => {
  453. this.node.destroy();
  454. });
  455. return;
  456. }
  457. }
  458. this.node.destroy();
  459. }
  460. private spawnCoin() {
  461. const ctrl = this.controller as any; // EnemyController
  462. if (!ctrl?.coinPrefab) return;
  463. const coin = instantiate(ctrl.coinPrefab);
  464. find('Canvas')!.addChild(coin); // 放到 UI 层
  465. const pos = new Vec3();
  466. this.node.getWorldPosition(pos); // 取死亡敌人的世界坐标
  467. coin.worldPosition = pos; // 金币就在敌人身上出现
  468. }
  469. /**
  470. * 暂停敌人
  471. */
  472. public pause(): void {
  473. this.isPaused = true;
  474. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已暂停`);
  475. }
  476. /**
  477. * 恢复敌人
  478. */
  479. public resume(): void {
  480. this.isPaused = false;
  481. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已恢复`);
  482. }
  483. /**
  484. * 检查是否暂停
  485. */
  486. public isPausedState(): boolean {
  487. return this.isPaused;
  488. }
  489. }