EnemyInstance.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import { _decorator, Component, Node, ProgressBar, Label, Vec3, find, UITransform, Collider2D, Contact2DType, IPhysics2DContact, instantiate, resources, Prefab, JsonAsset } 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 = 30;
  29. public maxHealth: number = 30;
  30. public speed: number = 50;
  31. public attackPower: number = 10;
  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 = 2; // 攻击间隔(秒)
  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. start() {
  63. // 初始化敌人
  64. this.initializeEnemy();
  65. }
  66. // 静态方法:加载敌人配置数据库
  67. public static async loadEnemyDatabase(): Promise<void> {
  68. if (EnemyInstance.enemyDatabase) return;
  69. return new Promise((resolve, reject) => {
  70. resources.load('data/enemies', JsonAsset, (err, jsonAsset) => {
  71. if (err) {
  72. console.error('[EnemyInstance] 加载敌人配置失败:', err);
  73. reject(err);
  74. return;
  75. }
  76. EnemyInstance.enemyDatabase = jsonAsset.json;
  77. console.log('[EnemyInstance] 敌人配置数据库加载成功');
  78. resolve();
  79. });
  80. });
  81. }
  82. // 设置敌人配置
  83. public setEnemyConfig(enemyId: string): void {
  84. this.enemyId = enemyId;
  85. if (!EnemyInstance.enemyDatabase) {
  86. console.error('[EnemyInstance] 敌人配置数据库未加载');
  87. return;
  88. }
  89. // 从数据库中查找敌人配置
  90. const enemies = EnemyInstance.enemyDatabase.enemies;
  91. this.enemyConfig = enemies.find((enemy: any) => enemy.id === enemyId);
  92. if (!this.enemyConfig) {
  93. console.error(`[EnemyInstance] 未找到敌人配置: ${enemyId}`);
  94. return;
  95. }
  96. // 应用配置到敌人属性
  97. this.applyEnemyConfig();
  98. }
  99. // 应用敌人配置到属性
  100. private applyEnemyConfig(): void {
  101. if (!this.enemyConfig) return;
  102. this.health = this.enemyConfig.health || 30;
  103. this.maxHealth = this.health;
  104. this.speed = this.enemyConfig.speed || 50;
  105. this.attackPower = this.enemyConfig.attack || 10;
  106. console.log(`[EnemyInstance] 应用敌人配置: ${this.enemyConfig.name}, 血量: ${this.health}, 速度: ${this.speed}, 攻击力: ${this.attackPower}`);
  107. }
  108. // 获取敌人配置信息
  109. public getEnemyConfig(): any {
  110. return this.enemyConfig;
  111. }
  112. // 获取敌人名称
  113. public getEnemyName(): string {
  114. return this.enemyConfig?.name || '未知敌人';
  115. }
  116. // 获取敌人类型
  117. public getEnemyType(): string {
  118. return this.enemyConfig?.type || 'basic';
  119. }
  120. // 获取敌人稀有度
  121. public getEnemyRarity(): string {
  122. return this.enemyConfig?.rarity || 'common';
  123. }
  124. // 获取金币奖励
  125. public getGoldReward(): number {
  126. return this.enemyConfig?.goldReward || 1;
  127. }
  128. // 初始化敌人
  129. private initializeEnemy() {
  130. // 确保血量正确设置
  131. if (this.maxHealth > 0) {
  132. this.health = this.maxHealth;
  133. }
  134. this.state = EnemyState.MOVING;
  135. this.attackInterval = 2.0; // 默认攻击间隔
  136. this.attackTimer = 0;
  137. // 初始化血条动画组件
  138. this.initializeHPBarAnimation();
  139. // 获取骨骼动画组件
  140. this.skeleton = this.getComponent(sp.Skeleton);
  141. this.playWalkAnimation();
  142. // 计算游戏区域中心
  143. this.calculateGameAreaCenter();
  144. // 初始化碰撞检测
  145. this.setupCollider();
  146. }
  147. // 设置碰撞器
  148. setupCollider() {
  149. // 检查节点是否有碰撞器
  150. let collider = this.node.getComponent(Collider2D);
  151. if (!collider) {
  152. return;
  153. }
  154. // 设置碰撞事件监听
  155. collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
  156. }
  157. // 碰撞开始事件
  158. onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  159. const nodeName = otherCollider.node.name;
  160. // 如果碰到墙体,停止移动并开始攻击
  161. if (nodeName.includes('Wall') || nodeName.includes('wall') || nodeName.includes('Fence') || nodeName.includes('Jiguang')) {
  162. this.state = EnemyState.ATTACKING;
  163. this.attackTimer = 0; // 立即开始攻击
  164. // 切换攻击动画
  165. this.playAttackAnimation();
  166. }
  167. }
  168. // 获取节点路径
  169. getNodePath(node: Node): string {
  170. let path = node.name;
  171. let current = node;
  172. while (current.parent) {
  173. current = current.parent;
  174. path = current.name + '/' + path;
  175. }
  176. return path;
  177. }
  178. // 计算游戏区域中心
  179. private calculateGameAreaCenter() {
  180. const gameArea = find('Canvas/GameLevelUI/GameArea');
  181. if (gameArea) {
  182. this.gameAreaCenter = gameArea.worldPosition;
  183. }
  184. }
  185. /**
  186. * 初始化血条动画组件
  187. */
  188. private initializeHPBarAnimation() {
  189. const hpBar = this.node.getChildByName('HPBar');
  190. if (hpBar) {
  191. // 查找红色和黄色血条节点
  192. const redBarNode = hpBar.getChildByName('RedBar');
  193. const yellowBarNode = hpBar.getChildByName('YellowBar');
  194. if (redBarNode && yellowBarNode) {
  195. // 添加血条动画组件
  196. this.hpBarAnimation = this.node.addComponent(HPBarAnimation);
  197. if (this.hpBarAnimation) {
  198. // 正确设置红色和黄色血条节点引用
  199. this.hpBarAnimation.redBarNode = redBarNode;
  200. this.hpBarAnimation.yellowBarNode = yellowBarNode;
  201. console.log(`[EnemyInstance] 血条动画组件已初始化`);
  202. }
  203. } else {
  204. console.warn(`[EnemyInstance] HPBar下未找到RedBar或YellowBar节点,RedBar: ${!!redBarNode}, YellowBar: ${!!yellowBarNode}`);
  205. }
  206. } else {
  207. console.warn(`[EnemyInstance] 未找到HPBar节点,无法初始化血条动画`);
  208. }
  209. }
  210. // 更新血量显示
  211. updateHealthDisplay() {
  212. const healthProgress = this.health / this.maxHealth;
  213. // 使用血条动画组件更新血条
  214. if (this.hpBarAnimation) {
  215. this.hpBarAnimation.updateProgress(healthProgress);
  216. } else {
  217. // 备用方案:直接更新血条
  218. const hpBar = this.node.getChildByName('HPBar');
  219. if (hpBar) {
  220. const progressBar = hpBar.getComponent(ProgressBar);
  221. if (progressBar) {
  222. progressBar.progress = healthProgress;
  223. }
  224. }
  225. }
  226. // 更新血量数字
  227. const hpLabel = this.node.getChildByName('HPLabel');
  228. if (hpLabel) {
  229. const label = hpLabel.getComponent(Label);
  230. if (label) {
  231. label.string = this.health.toString();
  232. }
  233. }
  234. }
  235. // 受到伤害
  236. takeDamage(damage: number, isCritical: boolean = false) {
  237. // 如果已经死亡,不再处理伤害
  238. if (this.state === EnemyState.DEAD) {
  239. return;
  240. }
  241. this.health -= damage;
  242. console.log(`[EnemyInstance] 敌人受到伤害: ${damage}, 剩余血量: ${this.health}`);
  243. // 显示伤害数字动画(在敌人头顶)
  244. // 优先使用EnemyController节点上的DamageNumberAni组件实例
  245. if (this.controller) {
  246. const damageAni = this.controller.getComponent(DamageNumberAni);
  247. if (damageAni) {
  248. damageAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
  249. } else {
  250. // 如果没有找到组件实例,使用静态方法作为备用
  251. DamageNumberAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
  252. }
  253. } else {
  254. // 如果没有controller引用,使用静态方法
  255. DamageNumberAni.showDamageNumber(damage, this.node.worldPosition, isCritical);
  256. }
  257. // 更新血量显示和动画
  258. this.updateHealthDisplay();
  259. // 如果血量低于等于0,销毁敌人
  260. if (this.health <= 0) {
  261. console.log(`[EnemyInstance] 敌人死亡,开始销毁流程`);
  262. this.state = EnemyState.DEAD;
  263. this.spawnCoin();
  264. // 进入死亡流程,禁用碰撞避免重复命中
  265. const col = this.getComponent(Collider2D);
  266. if (col) col.enabled = false;
  267. this.playDeathAnimationAndDestroy();
  268. }
  269. }
  270. onDestroy() {
  271. console.log(`[EnemyInstance] onDestroy 被调用,准备通知控制器`);
  272. // 通知控制器 & GameManager
  273. if (this.controller && typeof (this.controller as any).notifyEnemyDead === 'function') {
  274. // 检查控制器是否处于清理状态,避免在清理过程中触发游戏事件
  275. const isClearing = (this.controller as any).isClearing;
  276. if (isClearing) {
  277. console.log(`[EnemyInstance] 控制器处于清理状态,跳过死亡通知`);
  278. return;
  279. }
  280. console.log(`[EnemyInstance] 调用 notifyEnemyDead`);
  281. (this.controller as any).notifyEnemyDead(this.node);
  282. } else {
  283. console.warn(`[EnemyInstance] 无法调用 notifyEnemyDead: controller=${!!this.controller}`);
  284. }
  285. }
  286. update(deltaTime: number) {
  287. if (this.state === EnemyState.MOVING) {
  288. this.updateMovement(deltaTime);
  289. } else if (this.state === EnemyState.ATTACKING) {
  290. this.updateAttack(deltaTime);
  291. }
  292. // 不再每帧播放攻击动画,避免日志刷屏
  293. }
  294. // 更新移动逻辑
  295. private updateMovement(deltaTime: number) {
  296. // 检查是否接近游戏区域边界
  297. if (this.checkNearGameArea()) {
  298. this.state = EnemyState.ATTACKING;
  299. this.attackTimer = 0;
  300. this.playAttackAnimation();
  301. return;
  302. }
  303. // 继续移动
  304. this.moveTowardsTarget(deltaTime);
  305. }
  306. // 检查是否接近游戏区域
  307. private checkNearGameArea(): boolean {
  308. const currentPos = this.node.worldPosition;
  309. // 获取游戏区域边界
  310. const gameArea = find('Canvas/GameLevelUI/GameArea');
  311. if (!gameArea) return false;
  312. const uiTransform = gameArea.getComponent(UITransform);
  313. if (!uiTransform) return false;
  314. const gameAreaPos = gameArea.worldPosition;
  315. const halfWidth = uiTransform.width / 2;
  316. const halfHeight = uiTransform.height / 2;
  317. const bounds = {
  318. left: gameAreaPos.x - halfWidth,
  319. right: gameAreaPos.x + halfWidth,
  320. top: gameAreaPos.y + halfHeight,
  321. bottom: gameAreaPos.y - halfHeight
  322. };
  323. // 检查是否在游戏区域内或非常接近
  324. const safeDistance = 50; // 安全距离
  325. const isInside = currentPos.x >= bounds.left - safeDistance &&
  326. currentPos.x <= bounds.right + safeDistance &&
  327. currentPos.y >= bounds.bottom - safeDistance &&
  328. currentPos.y <= bounds.top + safeDistance;
  329. if (isInside) {
  330. return true;
  331. }
  332. return false;
  333. }
  334. // 移动到目标位置
  335. private moveTowardsTarget(deltaTime: number) {
  336. // 使用世界坐标进行移动计算,确保不受父节点坐标系影响
  337. const currentWorldPos = this.node.worldPosition.clone();
  338. // 目标世界坐标:优先使用指定的 Fence,其次退化到游戏区域中心
  339. let targetWorldPos: Vec3;
  340. if (this.targetFence && this.targetFence.isValid) {
  341. targetWorldPos = this.targetFence.worldPosition.clone();
  342. } else {
  343. targetWorldPos = this.gameAreaCenter.clone();
  344. }
  345. const dir = targetWorldPos.subtract(currentWorldPos);
  346. if (dir.length() === 0) return;
  347. dir.normalize();
  348. const moveDistance = this.speed * deltaTime;
  349. const newWorldPos = currentWorldPos.add(dir.multiplyScalar(moveDistance));
  350. // 直接设置世界坐标
  351. this.node.setWorldPosition(newWorldPos);
  352. }
  353. // 更新攻击逻辑
  354. private updateAttack(deltaTime: number) {
  355. this.attackTimer -= deltaTime;
  356. if (this.attackTimer <= 0) {
  357. // 执行攻击
  358. this.performAttack();
  359. // 重置攻击计时器
  360. this.attackTimer = this.attackInterval;
  361. }
  362. }
  363. // 执行攻击
  364. private performAttack() {
  365. if (!this.controller) {
  366. return;
  367. }
  368. // 对墙体造成伤害
  369. this.controller.damageWall(this.attackPower);
  370. }
  371. // 播放行走动画
  372. private playWalkAnimation() {
  373. if (!this.skeleton) return;
  374. const enemyComp = this.getComponent('EnemyComponent') as any;
  375. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  376. const walkName = anims.walk ?? 'walk';
  377. const idleName = anims.idle ?? 'idle';
  378. if (this.skeleton.findAnimation(walkName)) {
  379. this.skeleton.setAnimation(0, walkName, true);
  380. } else if (this.skeleton.findAnimation(idleName)) {
  381. this.skeleton.setAnimation(0, idleName, true);
  382. }
  383. }
  384. // 播放攻击动画
  385. private playAttackAnimation() {
  386. if (!this.skeleton) return;
  387. const enemyComp2 = this.getComponent('EnemyComponent') as any;
  388. const anims2 = enemyComp2?.getAnimations ? enemyComp2.getAnimations() : {};
  389. const attackName = anims2.attack ?? 'attack';
  390. // 移除频繁打印
  391. if (this.skeleton.findAnimation(attackName)) {
  392. this.skeleton.setAnimation(0, attackName, true);
  393. }
  394. }
  395. private playDeathAnimationAndDestroy() {
  396. console.log(`[EnemyInstance] 开始播放死亡动画并销毁`);
  397. if (this.skeleton) {
  398. const enemyComp = this.getComponent('EnemyComponent') as any;
  399. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  400. const deathName = anims.dead ?? 'dead';
  401. if (this.skeleton.findAnimation(deathName)) {
  402. console.log(`[EnemyInstance] 播放死亡动画: ${deathName}`);
  403. this.skeleton.setAnimation(0, deathName, false);
  404. // 销毁节点在动画完毕后
  405. this.skeleton.setCompleteListener(() => {
  406. console.log(`[EnemyInstance] 死亡动画完成,销毁节点`);
  407. this.node.destroy();
  408. });
  409. return;
  410. }
  411. }
  412. // 若无动画直接销毁
  413. console.log(`[EnemyInstance] 无死亡动画,直接销毁节点`);
  414. this.node.destroy();
  415. }
  416. private spawnCoin() {
  417. const ctrl = this.controller as any; // EnemyController
  418. if (!ctrl?.coinPrefab) return;
  419. const coin = instantiate(ctrl.coinPrefab);
  420. find('Canvas')!.addChild(coin); // 放到 UI 层
  421. const pos = new Vec3();
  422. this.node.getWorldPosition(pos); // 取死亡敌人的世界坐标
  423. coin.worldPosition = pos; // 金币就在敌人身上出现
  424. }
  425. }