EnemyInstance.ts 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533
  1. import { _decorator, Component, Node, ProgressBar, Label, Vec3, find, UITransform, Collider2D, Contact2DType, IPhysics2DContact, instantiate, resources, Prefab, JsonAsset, RigidBody2D, ERigidBody2DType, BoxCollider2D, CircleCollider2D, Vec2, Color, UIOpacity, tween } from 'cc';
  2. import { JsonConfigLoader } from '../Core/JsonConfigLoader';
  3. import { sp } from 'cc';
  4. import { DamageNumberAni } from '../Animations/DamageNumberAni';
  5. import { HPBarAnimation } from '../Animations/HPBarAnimation';
  6. import { EnemyComponent } from './EnemyComponent';
  7. import { EnemyAudio } from '../AudioManager/EnemyAudios';
  8. import { ScreenShakeManager } from './EnemyWeapon/ScreenShakeManager';
  9. import EventBus, { GameEvents } from '../Core/EventBus';
  10. const { ccclass, property } = _decorator;
  11. // 前向声明EnemyController接口,避免循环引用
  12. interface EnemyControllerType {
  13. gameBounds: {
  14. left: number;
  15. right: number;
  16. top: number;
  17. bottom: number;
  18. };
  19. topFenceNode: Node;
  20. bottomFenceNode: Node;
  21. damageWall: (damage: number) => void;
  22. getComponent: (componentType: any) => any;
  23. }
  24. // 敌人状态枚举
  25. enum EnemyState {
  26. DRIFTING, // 漂移中(从生成位置移动到线上)
  27. MOVING, // 移动中
  28. IDLE, // 待机中(碰到墙体后停止移动但可以攻击)
  29. ATTACKING, // 攻击中
  30. DEAD // 死亡
  31. }
  32. // 单个敌人实例的组件
  33. @ccclass('EnemyInstance')
  34. export class EnemyInstance extends Component {
  35. // 敌人属性(从配置文件读取)
  36. public health: number = 0;
  37. public maxHealth: number = 0;
  38. public speed: number = 0;
  39. public attackPower: number = 0;
  40. // 敌人配置ID
  41. public enemyId: string = '';
  42. // 敌人配置数据
  43. private enemyConfig: any = null;
  44. // 敌人配置数据库
  45. private static enemyDatabase: any = null;
  46. // === 新增属性 ===
  47. /** 是否从上方生成 */
  48. public spawnFromTop: boolean = true;
  49. /** 目标 Fence 节点(TopFence / BottomFence) */
  50. public targetFence: Node | null = null;
  51. /** 目标位置(墙体上的随机点) */
  52. public targetPosition: Vec3 | null = null;
  53. /** 漂移目标位置(线上的位置) */
  54. public driftTargetPosition: Vec3 | null = null;
  55. // 移动相关属性
  56. public movingDirection: number = 1; // 1: 向右, -1: 向左
  57. public targetY: number = 0; // 目标Y位置
  58. public changeDirectionTime: number = 0; // 下次改变方向的时间
  59. // 移动配置属性(从配置文件读取)
  60. public movementPattern: string = 'direct'; // 移动模式:direct, patrol等
  61. public moveType: string = 'straight'; // 移动类型:straight, sway等
  62. public patrolRange: number = 100; // 巡逻范围
  63. public rotationSpeed: number = 90; // 旋转速度
  64. public speedVariation: number = 0; // 速度变化
  65. public swingAmplitude: number = 0; // 摆动幅度
  66. public swingFrequency: number = 1; // 摆动频率
  67. // 摆动移动相关属性
  68. private swayTime: number = 0; // 摆动时间累计
  69. private basePosition: Vec3 = new Vec3(); // 基础位置(摆动的中心点)
  70. private swayOffset: Vec3 = new Vec3(); // 当前摆动偏移
  71. // 攻击属性
  72. public attackInterval: number = 0; // 攻击间隔(秒),从配置文件读取
  73. public attackRange: number = 0; // 攻击范围,从配置文件读取
  74. private attackTimer: number = 0;
  75. // 对控制器的引用
  76. public controller: EnemyControllerType = null;
  77. // 敌人当前状态
  78. private state: EnemyState = EnemyState.DRIFTING;
  79. // 游戏区域中心
  80. private gameAreaCenter: Vec3 = new Vec3();
  81. // 碰撞的墙体
  82. private collidedWall: Node = null;
  83. // 骨骼动画组件
  84. private skeleton: sp.Skeleton | null = null;
  85. // 血条动画组件
  86. private hpBarAnimation: HPBarAnimation | null = null;
  87. // 暂停状态标记
  88. private isPaused: boolean = false;
  89. // 当前播放的动画名称,用于避免重复播放
  90. private currentAnimationName: string = '';
  91. // 攻击状态下的动画管理
  92. private isPlayingAttackAnimation: boolean = false;
  93. start() {
  94. // 初始化敌人
  95. this.initializeEnemy();
  96. }
  97. // 静态方法:加载敌人配置数据库
  98. public static async loadEnemyDatabase(): Promise<void> {
  99. if (EnemyInstance.enemyDatabase) return;
  100. try {
  101. EnemyInstance.enemyDatabase = await JsonConfigLoader.getInstance().loadConfig('enemies');
  102. if (!EnemyInstance.enemyDatabase) {
  103. throw new Error('敌人配置文件内容为空');
  104. }
  105. } catch (error) {
  106. console.error('[EnemyInstance] 加载敌人配置失败:', error);
  107. throw error;
  108. }
  109. }
  110. // 设置敌人配置
  111. public setEnemyConfig(enemyId: string): void {
  112. this.enemyId = enemyId;
  113. if (!EnemyInstance.enemyDatabase) {
  114. console.error('[EnemyInstance] 敌人配置数据库未加载');
  115. return;
  116. }
  117. // 从数据库中查找敌人配置
  118. // 修复:enemies.json是直接的数组结构,不需要.enemies包装
  119. const enemies = EnemyInstance.enemyDatabase;
  120. this.enemyConfig = enemies.find((enemy: any) => enemy.id === enemyId);
  121. if (!this.enemyConfig) {
  122. console.error(`[EnemyInstance] 未找到敌人配置: ${enemyId}`);
  123. return;
  124. }
  125. // 应用配置到敌人属性
  126. this.applyEnemyConfig();
  127. }
  128. // 应用敌人配置到属性
  129. private applyEnemyConfig(): void {
  130. if (!this.enemyConfig) return;
  131. // 从stats节点读取基础属性
  132. const stats = this.enemyConfig.stats || {};
  133. this.health = stats.health || 30;
  134. this.maxHealth = stats.maxHealth || this.health;
  135. // 从movement节点读取移动配置
  136. const movement = this.enemyConfig.movement || {};
  137. this.speed = movement.speed || 50;
  138. // 读取移动模式配置
  139. this.movementPattern = movement.pattern || 'direct';
  140. this.moveType = movement.moveType || 'straight';
  141. this.patrolRange = movement.patrolRange || 100;
  142. this.rotationSpeed = movement.rotationSpeed || 90;
  143. this.speedVariation = movement.speedVariation || 0;
  144. // 读取摆动配置
  145. this.swingAmplitude = movement.swingAmplitude || 0;
  146. this.swingFrequency = movement.swingFrequency || 1;
  147. // 从combat节点读取攻击力
  148. const combat = this.enemyConfig.combat || {};
  149. this.attackPower = combat.attackDamage || 10;
  150. // 设置攻击间隔
  151. this.attackInterval = combat.attackCooldown || 2.0;
  152. console.log(`[EnemyInstance] 应用配置 ${this.enemyId}: 移动模式=${this.movementPattern}, 移动类型=${this.moveType}, 速度=${this.speed}, 摆动幅度=${this.swingAmplitude}`);
  153. }
  154. // 获取敌人配置信息
  155. public getEnemyConfig(): any {
  156. return this.enemyConfig;
  157. }
  158. // 获取敌人名称
  159. public getEnemyName(): string {
  160. return this.enemyConfig?.name || '未知敌人';
  161. }
  162. // 获取敌人类型
  163. public getEnemyType(): string {
  164. return this.enemyConfig?.type || 'basic';
  165. }
  166. // 获取敌人稀有度
  167. public getEnemyRarity(): string {
  168. return this.enemyConfig?.rarity || 'common';
  169. }
  170. // 获取金币奖励
  171. public getGoldReward(): number {
  172. return this.enemyConfig?.goldReward || 1;
  173. }
  174. // 获取掉落金币数量
  175. public getDropCoins(): number {
  176. const stats = this.enemyConfig?.stats || {};
  177. return stats.dropCoins || 1;
  178. }
  179. // 初始化敌人
  180. private initializeEnemy() {
  181. // 确保血量正确设置
  182. if (this.maxHealth > 0) {
  183. this.health = this.maxHealth;
  184. }
  185. this.state = EnemyState.DRIFTING; // 从漂移状态开始
  186. // 只有在攻击间隔未设置时才使用默认值
  187. if (this.attackInterval <= 0) {
  188. this.attackInterval = 2.0; // 默认攻击间隔
  189. }
  190. this.attackTimer = 0;
  191. // 初始化血条动画组件
  192. this.initializeHPBarAnimation();
  193. // 初始化骨骼动画组件 - 从EnemySprite子节点获取
  194. const enemySprite = this.node.getChildByName('EnemySprite');
  195. if (enemySprite) {
  196. this.skeleton = enemySprite.getComponent(sp.Skeleton);
  197. } else {
  198. console.error('[EnemyInstance] 未找到EnemySprite子节点,无法获取骨骼动画组件');
  199. }
  200. this.playWalkAnimation();
  201. // 计算游戏区域中心
  202. this.calculateGameAreaCenter();
  203. // 初始化碰撞检测
  204. this.setupCollider();
  205. }
  206. // 设置碰撞器
  207. setupCollider() {
  208. // 从EnemySprite子节点获取碰撞器组件
  209. const enemySprite = this.node.getChildByName('EnemySprite');
  210. if (!enemySprite) {
  211. console.error('[EnemyInstance] 未找到EnemySprite子节点,无法设置碰撞器组件');
  212. return;
  213. }
  214. // 检查EnemySprite节点是否有碰撞器
  215. let collider = enemySprite.getComponent(Collider2D);
  216. if (!collider) {
  217. console.warn(`[EnemyInstance] 敌人节点 ${this.node.name} 的EnemySprite子节点没有碰撞器组件`);
  218. return;
  219. }
  220. // 确保有RigidBody2D组件,这对于碰撞检测是必需的
  221. let rigidBody = enemySprite.getComponent(RigidBody2D);
  222. if (!rigidBody) {
  223. console.log(`[EnemyInstance] 为敌人EnemySprite节点添加RigidBody2D组件`);
  224. rigidBody = enemySprite.addComponent(RigidBody2D);
  225. }
  226. // 设置刚体属性
  227. if (rigidBody) {
  228. rigidBody.type = ERigidBody2DType.Dynamic; // 动态刚体
  229. rigidBody.enabledContactListener = true; // 启用碰撞监听
  230. rigidBody.gravityScale = 0; // 不受重力影响
  231. rigidBody.linearDamping = 0; // 无线性阻尼
  232. rigidBody.angularDamping = 0; // 无角阻尼
  233. rigidBody.allowSleep = false; // 不允许休眠
  234. rigidBody.fixedRotation = true; // 固定旋转
  235. }
  236. // 根据ContentSize自适应碰撞体大小
  237. this.adaptColliderToContentSize(collider);
  238. // 设置碰撞事件监听
  239. collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
  240. console.log(`[EnemyInstance] 敌人 ${this.node.name} 碰撞器设置完成,碰撞器启用: ${collider.enabled}, 刚体启用: ${rigidBody?.enabled}`);
  241. }
  242. /**
  243. * 根据ContentSize自适应碰撞体大小
  244. */
  245. private adaptColliderToContentSize(collider: Collider2D): void {
  246. // 从EnemySprite子节点获取UITransform组件
  247. const enemySprite = this.node.getChildByName('EnemySprite');
  248. if (!enemySprite) {
  249. console.error('[EnemyInstance] 未找到EnemySprite子节点,无法获取UITransform组件');
  250. return;
  251. }
  252. const uiTransform = enemySprite.getComponent(UITransform);
  253. if (!uiTransform) {
  254. console.warn(`[EnemyInstance] EnemySprite节点没有UITransform组件,无法自适应碰撞体大小`);
  255. return;
  256. }
  257. // 尝试获取骨骼动画组件来获取更准确的尺寸
  258. const skeleton = enemySprite.getComponent(sp.Skeleton);
  259. let actualWidth = uiTransform.contentSize.width;
  260. let actualHeight = uiTransform.contentSize.height;
  261. if (skeleton && skeleton.skeletonData) {
  262. // 如果有骨骼动画,尝试从骨骼数据获取尺寸信息
  263. try {
  264. // 使用骨骼数据的默认尺寸,或者根据动画缩放计算
  265. const skeletonData = skeleton.skeletonData;
  266. if (skeletonData) {
  267. // 获取骨骼的缩放比例
  268. const scaleX = Math.abs(skeleton.node.scale.x);
  269. const scaleY = Math.abs(skeleton.node.scale.y);
  270. // 使用合理的默认尺寸并应用缩放
  271. actualWidth = Math.max(60 * scaleX, uiTransform.contentSize.width * 0.8);
  272. actualHeight = Math.max(80 * scaleY, uiTransform.contentSize.height * 0.8);
  273. console.log(`[EnemyInstance] 使用骨骼动画尺寸 (scale: ${scaleX.toFixed(2)}, ${scaleY.toFixed(2)}): ${actualWidth.toFixed(2)} x ${actualHeight.toFixed(2)}`);
  274. } else {
  275. // 如果无法获取骨骼数据,使用默认尺寸
  276. actualWidth = Math.max(60, uiTransform.contentSize.width * 0.8);
  277. actualHeight = Math.max(80, uiTransform.contentSize.height * 0.8);
  278. console.log(`[EnemyInstance] 使用默认骨骼尺寸: ${actualWidth.toFixed(2)} x ${actualHeight.toFixed(2)}`);
  279. }
  280. } catch (error) {
  281. console.warn(`[EnemyInstance] 获取骨骼信息失败,使用默认尺寸:`, error);
  282. actualWidth = Math.max(60, uiTransform.contentSize.width * 0.8);
  283. actualHeight = Math.max(80, uiTransform.contentSize.height * 0.8);
  284. }
  285. }
  286. console.log(`[EnemyInstance] 敌人 ${this.node.name} 实际尺寸: ${actualWidth.toFixed(2)} x ${actualHeight.toFixed(2)}`);
  287. // 根据碰撞器类型设置大小
  288. if (collider instanceof BoxCollider2D) {
  289. // 方形碰撞器:使用实际尺寸,稍微缩小以避免过于敏感的碰撞
  290. const boxCollider = collider as BoxCollider2D;
  291. boxCollider.size.width = actualWidth * 0.9; // 缩小10%
  292. boxCollider.size.height = actualHeight * 0.9; // 缩小10%
  293. // 调整偏移量,使碰撞器居中对齐到EnemySprite
  294. const anchorPoint = uiTransform.anchorPoint;
  295. // 对于骨骼动画,通常锚点在底部中心(0.5, 0),需要向上偏移
  296. boxCollider.offset.x = 0; // X轴居中
  297. boxCollider.offset.y = actualHeight * (0.5 - anchorPoint.y) * 0.9; // Y轴根据锚点调整,并匹配缩放后的尺寸
  298. console.log(`[EnemyInstance] BoxCollider2D 已自适应: size(${boxCollider.size.width.toFixed(2)}, ${boxCollider.size.height.toFixed(2)}), offset(${boxCollider.offset.x.toFixed(2)}, ${boxCollider.offset.y.toFixed(2)}), anchor(${anchorPoint.x.toFixed(2)}, ${anchorPoint.y.toFixed(2)})`);
  299. } else if (collider instanceof CircleCollider2D) {
  300. // 圆形碰撞器:使用实际尺寸的较小值作为直径,稍微缩小
  301. const circleCollider = collider as CircleCollider2D;
  302. const radius = Math.min(actualWidth, actualHeight) / 2 * 0.9; // 缩小10%
  303. circleCollider.radius = radius;
  304. // 调整偏移量,使碰撞器居中对齐到EnemySprite
  305. const anchorPoint = uiTransform.anchorPoint;
  306. // 对于骨骼动画,通常锚点在底部中心(0.5, 0),需要向上偏移
  307. circleCollider.offset.x = 0; // X轴居中
  308. circleCollider.offset.y = actualHeight * (0.5 - anchorPoint.y) * 0.9; // Y轴根据锚点调整,并匹配缩放后的尺寸
  309. console.log(`[EnemyInstance] CircleCollider2D 已自适应: radius(${radius.toFixed(2)}), offset(${circleCollider.offset.x.toFixed(2)}, ${circleCollider.offset.y.toFixed(2)}), anchor(${anchorPoint.x.toFixed(2)}, ${anchorPoint.y.toFixed(2)})`);
  310. } else {
  311. console.warn(`[EnemyInstance] 不支持的碰撞器类型: ${collider.constructor.name}`);
  312. }
  313. // 碰撞体调整完成后,同步更新血条位置
  314. this.updateHPBarPosition();
  315. }
  316. /**
  317. * 动态调整血条位置,使其始终显示在碰撞体上方
  318. */
  319. private updateHPBarPosition(): void {
  320. // 获取血条根节点
  321. const hpBarRoot = this.node.getChildByName('HPBar');
  322. if (!hpBarRoot) {
  323. console.warn('[EnemyInstance] 未找到HPBar节点,无法调整血条位置');
  324. return;
  325. }
  326. // 获取EnemySprite节点和其碰撞器
  327. const enemySprite = this.node.getChildByName('EnemySprite');
  328. if (!enemySprite) {
  329. console.error('[EnemyInstance] 未找到EnemySprite子节点,无法计算血条位置');
  330. return;
  331. }
  332. const collider = enemySprite.getComponent(Collider2D);
  333. const uiTransform = enemySprite.getComponent(UITransform);
  334. if (!collider || !uiTransform) {
  335. console.warn('[EnemyInstance] EnemySprite节点缺少必要组件,无法计算血条位置');
  336. return;
  337. }
  338. // 计算碰撞体的顶部位置
  339. let colliderTopY = 0;
  340. const anchorPoint = uiTransform.anchorPoint;
  341. if (collider instanceof BoxCollider2D) {
  342. const boxCollider = collider as BoxCollider2D;
  343. // 碰撞体顶部 = 碰撞体偏移Y + 碰撞体高度的一半
  344. colliderTopY = boxCollider.offset.y + boxCollider.size.height / 2;
  345. } else if (collider instanceof CircleCollider2D) {
  346. const circleCollider = collider as CircleCollider2D;
  347. // 碰撞体顶部 = 碰撞体偏移Y + 半径
  348. colliderTopY = circleCollider.offset.y + circleCollider.radius;
  349. }
  350. // 血条位置设置为碰撞体顶部上方一定距离
  351. const hpBarOffset = -35; // 血条距离碰撞体顶部的距离
  352. const hpBarY = colliderTopY + hpBarOffset;
  353. // 设置血条位置
  354. const hpBarTransform = hpBarRoot.getComponent(UITransform);
  355. if (hpBarTransform) {
  356. hpBarRoot.setPosition(0, hpBarY, 0);
  357. console.log(`[EnemyInstance] 血条位置已更新: Y = ${hpBarY.toFixed(2)} (碰撞体顶部: ${colliderTopY.toFixed(2)})`);
  358. }
  359. }
  360. // 碰撞开始事件
  361. onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  362. const nodeName = otherCollider.node.name;
  363. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 碰撞检测 - 碰撞对象: ${nodeName}, 当前状态: ${EnemyState[this.state]}`);
  364. // 只有在移动状态或攻击状态下才能响应墙体碰撞,避免重复触发
  365. if (this.state !== EnemyState.MOVING && this.state !== EnemyState.ATTACKING) {
  366. return;
  367. }
  368. // 检查是否碰到墙体(更严格的墙体识别)
  369. const isWall = this.isWallNode(otherCollider.node);
  370. if (isWall) {
  371. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 碰撞到墙体 ${nodeName},切换到待机状态`);
  372. this.state = EnemyState.IDLE;
  373. this.attackTimer = 0; // 立即开始攻击
  374. this.collidedWall = otherCollider.node; // 记录碰撞的墙体
  375. // 停止移动
  376. this.stopRigidBodyMovement();
  377. // 切换到待机动画
  378. this.playIdleAnimation();
  379. }
  380. }
  381. // 检查节点是否为墙体
  382. private isWallNode(node: Node): boolean {
  383. const nodeName = node.name.toLowerCase();
  384. // 检查节点名称是否包含墙体关键词
  385. const wallKeywords = ['wall', 'fence', 'jiguang', '墙', '围栏'];
  386. const isWallByName = wallKeywords.some(keyword => nodeName.includes(keyword));
  387. // 检查节点是否有Wall组件
  388. const hasWallComponent = node.getComponent('Wall') !== null;
  389. return isWallByName || hasWallComponent;
  390. }
  391. // 获取节点路径
  392. getNodePath(node: Node): string {
  393. let path = node.name;
  394. let current = node;
  395. while (current.parent) {
  396. current = current.parent;
  397. path = current.name + '/' + path;
  398. }
  399. return path;
  400. }
  401. // 计算游戏区域中心
  402. private calculateGameAreaCenter() {
  403. const gameArea = find('Canvas/GameLevelUI/GameArea');
  404. if (gameArea) {
  405. this.gameAreaCenter = gameArea.worldPosition;
  406. }
  407. }
  408. /**
  409. * 初始化血条动画组件
  410. */
  411. private initializeHPBarAnimation() {
  412. // 检查是否已经存在血条动画组件,避免重复添加
  413. if (this.hpBarAnimation) {
  414. console.log(`[EnemyInstance] 血条动画组件已存在,跳过初始化`);
  415. return;
  416. }
  417. const hpBar = this.node.getChildByName('HPBar');
  418. if (hpBar) {
  419. // 查找红色和黄色血条节点
  420. const redBarNode = hpBar.getChildByName('RedBar');
  421. const yellowBarNode = hpBar.getChildByName('YellowBar');
  422. if (redBarNode && yellowBarNode) {
  423. // 检查节点是否已经有HPBarAnimation组件
  424. let existingHPBarAnimation = this.node.getComponent(HPBarAnimation);
  425. if (existingHPBarAnimation) {
  426. this.hpBarAnimation = existingHPBarAnimation;
  427. console.log(`[EnemyInstance] 使用已存在的血条动画组件`);
  428. } else {
  429. // 添加血条动画组件
  430. this.hpBarAnimation = this.node.addComponent(HPBarAnimation);
  431. console.log(`[EnemyInstance] 新建血条动画组件`);
  432. }
  433. if (this.hpBarAnimation) {
  434. // 正确设置红色和黄色血条节点引用
  435. this.hpBarAnimation.redBarNode = redBarNode;
  436. this.hpBarAnimation.yellowBarNode = yellowBarNode;
  437. this.hpBarAnimation.hpBarRootNode = hpBar; // 设置HPBar根节点
  438. console.log(`[EnemyInstance] 血条动画组件已初始化`);
  439. }
  440. } else {
  441. console.warn(`[EnemyInstance] HPBar下未找到RedBar或YellowBar节点,RedBar: ${!!redBarNode}, YellowBar: ${!!yellowBarNode}`);
  442. }
  443. } else {
  444. console.warn(`[EnemyInstance] 未找到HPBar节点,无法初始化血条动画`);
  445. }
  446. }
  447. // 更新血量显示
  448. updateHealthDisplay(showBar: boolean = false) {
  449. // 确保血量值在有效范围内
  450. this.health = Math.max(0, Math.min(this.maxHealth, this.health));
  451. const healthProgress = this.maxHealth > 0 ? this.health / this.maxHealth : 0;
  452. console.log(`[EnemyInstance] 更新血量显示: ${this.health}/${this.maxHealth} (${(healthProgress * 100).toFixed(1)}%)`);
  453. // 使用血条动画组件更新血条
  454. if (this.hpBarAnimation) {
  455. this.hpBarAnimation.updateProgress(healthProgress, showBar);
  456. } else {
  457. // 备用方案:直接更新血条
  458. const hpBar = this.node.getChildByName('HPBar');
  459. if (hpBar) {
  460. const progressBar = hpBar.getComponent(ProgressBar);
  461. if (progressBar) {
  462. progressBar.progress = healthProgress;
  463. }
  464. // 根据showBar参数控制血条显示
  465. hpBar.active = showBar;
  466. }
  467. }
  468. // 更新血量数字
  469. const hpLabel = this.node.getChildByName('HPLabel');
  470. if (hpLabel) {
  471. const label = hpLabel.getComponent(Label);
  472. if (label) {
  473. // 显示整数血量值
  474. label.string = Math.ceil(this.health).toString();
  475. }
  476. }
  477. }
  478. // 受到伤害
  479. takeDamage(damage: number, isCritical: boolean = false) {
  480. // 如果已经死亡,不再处理伤害
  481. if (this.state === EnemyState.DEAD) {
  482. return;
  483. }
  484. // 确保伤害值为正数
  485. if (damage <= 0) {
  486. console.warn(`[EnemyInstance] 无效的伤害值: ${damage}`);
  487. return;
  488. }
  489. // 应用防御力减少伤害
  490. const enemyComponent = this.getComponent(EnemyComponent);
  491. const defense = enemyComponent ? enemyComponent.getDefense() : 0;
  492. let finalDamage = Math.max(1, damage - defense); // 至少造成1点伤害
  493. // 检查格挡
  494. if (enemyComponent && enemyComponent.canBlock() && this.tryBlock()) {
  495. finalDamage *= (1 - enemyComponent.getBlockDamageReduction());
  496. this.showBlockEffect();
  497. }
  498. // 计算新的血量,确保不会低于0
  499. const oldHealth = this.health;
  500. const newHealth = Math.max(0, this.health - finalDamage);
  501. const actualHealthLoss = oldHealth - newHealth; // 实际血量损失
  502. this.health = newHealth;
  503. // 检查是否触发狂暴
  504. this.checkRageTrigger();
  505. // 日志显示武器的真实伤害值,而不是血量差值
  506. console.log(`[EnemyInstance] 敌人受到伤害: ${damage} (武器伤害), 防御后伤害: ${finalDamage}, 实际血量损失: ${actualHealthLoss}, 剩余血量: ${this.health}/${this.maxHealth}`);
  507. // 受击音效已移除
  508. // 显示伤害数字动画(在敌人头顶)- 显示防御后的实际伤害
  509. // 优先使用EnemyController节点上的DamageNumberAni组件实例
  510. if (this.controller) {
  511. const damageAni = this.controller.getComponent(DamageNumberAni);
  512. if (damageAni) {
  513. damageAni.showDamageNumber(finalDamage, this.node.worldPosition, isCritical);
  514. } else {
  515. // 如果没有找到组件实例,使用静态方法作为备用
  516. DamageNumberAni.showDamageNumber(finalDamage, this.node.worldPosition, isCritical);
  517. }
  518. } else {
  519. // 如果没有controller引用,使用静态方法
  520. DamageNumberAni.showDamageNumber(finalDamage, this.node.worldPosition, isCritical);
  521. }
  522. // 更新血量显示和动画,受伤时显示血条
  523. this.updateHealthDisplay(true);
  524. // 如果血量低于等于0,销毁敌人
  525. if (this.health <= 0) {
  526. console.log(`[EnemyInstance] 敌人死亡,开始销毁流程`);
  527. this.state = EnemyState.DEAD;
  528. this.spawnCoin();
  529. // 进入死亡流程,禁用碰撞避免重复命中
  530. const enemySprite = this.node.getChildByName('EnemySprite');
  531. if (enemySprite) {
  532. const col = enemySprite.getComponent(Collider2D);
  533. if (col) col.enabled = false;
  534. }
  535. this.playDeathAnimationAndDestroy();
  536. }
  537. }
  538. onDestroy() {
  539. console.log(`[EnemyInstance] onDestroy 被调用,准备通知控制器`);
  540. // 通知控制器 & GameManager
  541. if (this.controller && typeof (this.controller as any).notifyEnemyDead === 'function') {
  542. // 检查控制器是否处于清理状态,避免在清理过程中触发游戏事件
  543. const isClearing = (this.controller as any).isClearing;
  544. if (isClearing) {
  545. console.log(`[EnemyInstance] 控制器处于清理状态,跳过死亡通知`);
  546. return;
  547. }
  548. console.log(`[EnemyInstance] 调用 notifyEnemyDead`);
  549. (this.controller as any).notifyEnemyDead(this.node);
  550. } else {
  551. console.warn(`[EnemyInstance] 无法调用 notifyEnemyDead: controller=${!!this.controller}`);
  552. }
  553. }
  554. update(deltaTime: number) {
  555. // 如果敌人被暂停,则不执行任何更新逻辑
  556. if (this.isPaused) {
  557. return;
  558. }
  559. if (this.state === EnemyState.DRIFTING) {
  560. this.updateDrifting(deltaTime);
  561. // 在漂移状态也检查墙体碰撞
  562. this.checkWallCollisionByDistance();
  563. } else if (this.state === EnemyState.MOVING) {
  564. this.updateMovement(deltaTime);
  565. // 在移动状态检查墙体碰撞
  566. this.checkWallCollisionByDistance();
  567. } else if (this.state === EnemyState.IDLE) {
  568. this.updateIdle(deltaTime);
  569. } else if (this.state === EnemyState.ATTACKING) {
  570. this.updateAttack(deltaTime);
  571. }
  572. // 不再每帧播放攻击动画,避免日志刷屏
  573. }
  574. // 更新漂移逻辑(从生成位置移动到线上)
  575. private updateDrifting(deltaTime: number) {
  576. if (!this.driftTargetPosition) {
  577. // 如果没有漂移目标位置,直接切换到移动状态
  578. this.state = EnemyState.MOVING;
  579. return;
  580. }
  581. const currentWorldPos = this.node.worldPosition.clone();
  582. const targetWorldPos = this.driftTargetPosition.clone();
  583. const dir = targetWorldPos.subtract(currentWorldPos);
  584. const distanceToTarget = dir.length();
  585. // 如果距离目标很近,切换到移动状态
  586. const stopDistance = 5; // 减小停止距离以提高精度
  587. if (distanceToTarget <= stopDistance) {
  588. this.state = EnemyState.MOVING;
  589. // 确保刚体速度为零
  590. this.stopRigidBodyMovement();
  591. return;
  592. }
  593. if (distanceToTarget === 0) return;
  594. dir.normalize();
  595. // 使用代码控制移动,不依赖物理系统
  596. const driftSpeed = Math.min(this.speed * 8, 500); // 限制最大漂移速度为5500
  597. const moveDistance = driftSpeed * deltaTime;
  598. const actualMoveDistance = Math.min(moveDistance, distanceToTarget);
  599. const newWorldPos = currentWorldPos.add(dir.multiplyScalar(actualMoveDistance));
  600. // 直接设置世界坐标
  601. this.node.setWorldPosition(newWorldPos);
  602. // 确保刚体速度为零,避免物理系统干扰
  603. this.stopRigidBodyMovement();
  604. }
  605. // 更新移动逻辑
  606. private updateMovement(deltaTime: number) {
  607. // 根据配置的移动模式执行不同的移动逻辑
  608. switch (this.movementPattern) {
  609. case 'direct':
  610. this.updateDirectMovement(deltaTime);
  611. break;
  612. case 'patrol':
  613. this.updatePatrolMovement(deltaTime);
  614. break;
  615. default:
  616. this.updateDirectMovement(deltaTime);
  617. break;
  618. }
  619. }
  620. // 直线移动模式
  621. private updateDirectMovement(deltaTime: number) {
  622. this.moveTowardsTarget(deltaTime);
  623. }
  624. // 巡逻移动模式
  625. private updatePatrolMovement(deltaTime: number) {
  626. // 更新方向变化计时器
  627. this.changeDirectionTime += deltaTime;
  628. console.log(`[EnemyInstance] 摆动 调用,changeDirectionTime=${this.changeDirectionTime}`);
  629. // 根据巡逻范围决定方向变化
  630. const enemySprite = this.node.getChildByName('EnemySprite');
  631. if (enemySprite && this.controller) {
  632. const currentPos = enemySprite.worldPosition;
  633. const bounds = this.controller.gameBounds;
  634. // 检查是否到达巡逻边界
  635. const leftBound = bounds.left + this.patrolRange;
  636. const rightBound = bounds.right - this.patrolRange;
  637. if (currentPos.x <= leftBound && this.movingDirection < 0) {
  638. this.movingDirection = 1; // 向右
  639. this.changeDirectionTime = 0;
  640. } else if (currentPos.x >= rightBound && this.movingDirection > 0) {
  641. this.movingDirection = -1; // 向左
  642. this.changeDirectionTime = 0;
  643. }
  644. // 随机改变Y目标位置
  645. if (this.changeDirectionTime >= 3 + Math.random() * 2) {
  646. const centerY = (bounds.top + bounds.bottom) / 2;
  647. const range = (bounds.top - bounds.bottom) * 0.4;
  648. this.targetY = centerY + (Math.random() - 0.5) * range;
  649. this.changeDirectionTime = 0;
  650. }
  651. }
  652. this.moveTowardsTarget(deltaTime);
  653. }
  654. // 向目标移动
  655. private moveTowardsTarget(deltaTime: number) {
  656. // 获取当前速度(考虑狂暴加成和速度变化)
  657. const enemyComponent = this.getComponent(EnemyComponent);
  658. let currentSpeed = enemyComponent ? enemyComponent.getCurrentSpeed() : this.speed;
  659. // 应用速度变化配置,限制变化幅度防止过大跳跃
  660. if (this.speedVariation > 0) {
  661. const maxVariation = Math.min(this.speedVariation, currentSpeed * 0.3); // 限制变化不超过当前速度的30%
  662. const variation = (Math.random() - 0.5) * 2 * maxVariation;
  663. currentSpeed = Math.max(currentSpeed + variation, currentSpeed * 0.5); // 确保速度不会过低
  664. }
  665. // 统一使用主节点的世界坐标,与updateDrifting保持一致
  666. const currentPos = this.node.worldPosition.clone();
  667. let dir = new Vec3();
  668. // 水平移动方向
  669. dir.x = this.movingDirection;
  670. // 垂直移动方向 - 向目标Y位置移动
  671. const yDiff = this.targetY - currentPos.y;
  672. if (Math.abs(yDiff) > 10) { // 如果距离目标Y位置超过10像素
  673. dir.y = yDiff > 0 ? 0.5 : -0.5; // 缓慢向目标Y移动
  674. } else {
  675. dir.y = 0;
  676. }
  677. // 如果有具体的目标位置(墙体),优先移动到墙体
  678. if (this.targetPosition) {
  679. const targetDir = new Vec3();
  680. Vec3.subtract(targetDir, this.targetPosition.clone(), currentPos);
  681. if (targetDir.length() > 20) { // 距离墙体还有一定距离时
  682. targetDir.normalize();
  683. dir.x = targetDir.x * 0.7; // 70%朝向墙体
  684. dir.y = targetDir.y * 0.7;
  685. }
  686. } else if (this.targetFence) {
  687. const fencePos = this.targetFence.worldPosition.clone();
  688. const targetDir = new Vec3();
  689. Vec3.subtract(targetDir, fencePos, currentPos);
  690. if (targetDir.length() > 20) {
  691. targetDir.normalize();
  692. dir.x = targetDir.x * 0.7;
  693. dir.y = targetDir.y * 0.7;
  694. }
  695. }
  696. // 归一化方向向量
  697. if (dir.length() > 0) {
  698. dir.normalize();
  699. }
  700. // 应用摆动效果(根据配置)
  701. if (this.moveType === 'sway' && this.swingAmplitude > 0) {
  702. this.swayTime += deltaTime;
  703. const swayAmount = Math.sin(this.swayTime * this.swingFrequency) * this.swingAmplitude;
  704. // 计算垂直于移动方向的摆动
  705. const swayDir = new Vec3(-dir.y, dir.x, 0);
  706. swayDir.multiplyScalar(swayAmount / 100); // 将摆动幅度转换为合适的比例
  707. dir.add(swayDir);
  708. }
  709. // 限制单帧移动距离,防止突然跳跃
  710. const moveDistance = currentSpeed * deltaTime;
  711. const maxMoveDistance = Math.min(15, currentSpeed * 0.1); // 限制单帧最大移动距离
  712. const actualMoveDistance = Math.min(moveDistance, maxMoveDistance);
  713. const moveVector = dir.multiplyScalar(actualMoveDistance);
  714. const newPos = new Vec3();
  715. Vec3.add(newPos, currentPos, moveVector);
  716. // 统一使用主节点的setWorldPosition,与updateDrifting保持一致
  717. this.node.setWorldPosition(newPos);
  718. // 确保刚体速度为零,避免物理系统干扰
  719. this.stopRigidBodyMovement();
  720. }
  721. // 更新攻击逻辑
  722. private updateAttack(deltaTime: number) {
  723. // 获取敌人的攻击范围配置
  724. const enemyComponent = this.getComponent(EnemyComponent);
  725. const attackRange = enemyComponent ? enemyComponent.getAttackRange() : 30;
  726. // 无论近战还是远程攻击,在攻击状态下都停止移动
  727. const enemySprite = this.node.getChildByName('EnemySprite');
  728. if (enemySprite) {
  729. const rigidBody = enemySprite.getComponent(RigidBody2D);
  730. if (rigidBody) {
  731. // 停止物理移动
  732. rigidBody.linearVelocity = new Vec2(0, 0);
  733. }
  734. }
  735. this.attackTimer -= deltaTime;
  736. if (this.attackTimer <= 0) {
  737. // 执行攻击
  738. this.performAttack();
  739. // 重置攻击计时器
  740. this.attackTimer = this.attackInterval;
  741. } else {
  742. // 攻击冷却期间播放待机动画(只有在不播放攻击动画时)
  743. if (!this.isPlayingAttackAnimation) {
  744. this.playIdleAnimationSafe();
  745. }
  746. }
  747. }
  748. // 待机状态更新(停止移动但可以攻击)
  749. private updateIdle(deltaTime: number) {
  750. // 在待机状态下停止移动
  751. const enemySprite = this.node.getChildByName('EnemySprite');
  752. if (enemySprite) {
  753. const rigidBody = enemySprite.getComponent(RigidBody2D);
  754. if (rigidBody) {
  755. // 停止物理移动
  756. rigidBody.linearVelocity = new Vec2(0, 0);
  757. }
  758. }
  759. this.attackTimer -= deltaTime;
  760. if (this.attackTimer <= 0) {
  761. // 执行攻击
  762. this.performAttack();
  763. // 重置攻击计时器
  764. this.attackTimer = this.attackInterval;
  765. } else {
  766. // 攻击冷却期间播放待机动画(只有在不播放攻击动画时)
  767. if (!this.isPlayingAttackAnimation) {
  768. this.playIdleAnimationSafe();
  769. }
  770. }
  771. }
  772. // 执行攻击
  773. private performAttack() {
  774. if (!this.controller) {
  775. return;
  776. }
  777. const enemyComponent = this.getComponent(EnemyComponent);
  778. const attackType = enemyComponent ? enemyComponent.getAttackType() : 'melee';
  779. // 播放攻击音效
  780. EnemyAudio.playAttackSound(this.enemyConfig);
  781. // 根据攻击类型执行不同的攻击逻辑
  782. if (attackType === 'ranged' || attackType === 'projectile' || attackType.includes('projectile')) {
  783. console.log(`[EnemyInstance] 执行远程攻击,攻击类型: ${attackType}`);
  784. this.performRangedAttack();
  785. } else {
  786. // 近战攻击:播放攻击动画,动画结束后造成伤害
  787. console.log(`[EnemyInstance] 执行近战攻击,攻击类型: ${attackType}`);
  788. this.playAttackAnimationWithDamage();
  789. }
  790. }
  791. // 执行远程攻击(投掷)
  792. private performRangedAttack() {
  793. const enemyComponent = this.getComponent(EnemyComponent);
  794. if (!enemyComponent) return;
  795. // 远程攻击也需要播放攻击动画,在动画即将结束时发射抛掷物
  796. this.playRangedAttackAnimationWithProjectile();
  797. }
  798. // 播放远程攻击动画并在动画结束时发射抛掷物
  799. private playRangedAttackAnimationWithProjectile() {
  800. if (!this.skeleton) {
  801. // 如果没有骨骼动画,直接发射抛掷物
  802. this.fireProjectile();
  803. return;
  804. }
  805. const enemyComp = this.getComponent('EnemyComponent') as any;
  806. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  807. const attackName = anims.attack ?? 'attack';
  808. const animation = this.skeleton.findAnimation(attackName);
  809. if (animation) {
  810. // 播放攻击动画(不循环)
  811. this.skeleton.setAnimation(0, attackName, false);
  812. this.currentAnimationName = attackName;
  813. this.isPlayingAttackAnimation = true;
  814. console.log(`[EnemyInstance] 播放远程攻击动画: ${attackName}`);
  815. // 动画切换后延迟更新血条位置,确保动画尺寸已生效
  816. this.scheduleOnce(() => {
  817. this.updateHPBarPosition();
  818. }, 0.1);
  819. // 获取动画时长并在动画即将结束时发射抛掷物
  820. const animationDuration = animation.duration;
  821. console.log(`[EnemyInstance] 远程攻击动画时长: ${animationDuration}秒`);
  822. // 在动画播放到90%时发射抛掷物,让抛掷物发射与动画同步
  823. const projectileFireTime = animationDuration * 0.9;
  824. // 在动画播放到90%时发射抛掷物
  825. this.scheduleOnce(() => {
  826. // 发射抛掷物
  827. this.fireProjectile();
  828. }, projectileFireTime);
  829. // 动画完成后根据当前状态切换动画
  830. this.scheduleOnce(() => {
  831. this.isPlayingAttackAnimation = false;
  832. if (this.state === EnemyState.IDLE || this.state === EnemyState.ATTACKING) {
  833. // 如果是待机状态或攻击状态,切换回待机动画
  834. this.playIdleAnimation();
  835. } else {
  836. // 其他状态切换回行走动画
  837. this.playWalkAnimation();
  838. }
  839. }, animationDuration);
  840. } else {
  841. // 如果找不到攻击动画,直接发射抛掷物
  842. this.fireProjectile();
  843. }
  844. }
  845. // 发射抛掷物
  846. private fireProjectile() {
  847. const enemyComponent = this.getComponent(EnemyComponent);
  848. if (!enemyComponent) return;
  849. const projectileType = enemyComponent.getProjectileType();
  850. const projectileSpeed = enemyComponent.getProjectileSpeed();
  851. const attackDamage = enemyComponent.getDamage();
  852. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 发射投掷物: ${projectileType}, 速度: ${projectileSpeed}, 伤害: ${attackDamage}`);
  853. // 获取EnemyProjectile组件来创建抛掷物
  854. if (this.controller) {
  855. console.log('[EnemyInstance] 找到controller,开始获取EnemyProjectile组件');
  856. const enemyProjectile = this.controller.getComponent('EnemyProjectile') as any;
  857. if (enemyProjectile) {
  858. console.log('[EnemyInstance] 找到EnemyProjectile组件,准备创建抛掷物');
  859. // 计算抛掷物发射方向(朝向目标墙体)
  860. let direction = new Vec3(0, -1, 0); // 默认向下
  861. if (this.collidedWall) {
  862. const currentPos = this.node.worldPosition;
  863. const wallPos = this.collidedWall.worldPosition;
  864. direction = wallPos.clone().subtract(currentPos).normalize();
  865. console.log('[EnemyInstance] 计算抛掷物方向,目标墙体:', this.collidedWall.name, '方向:', direction);
  866. } else {
  867. console.log('[EnemyInstance] 未找到目标墙体,使用默认方向');
  868. }
  869. // 创建抛掷物配置
  870. const projectileConfig = {
  871. damage: attackDamage,
  872. speed: projectileSpeed,
  873. direction: direction,
  874. projectileType: projectileType,
  875. startPosition: this.node.worldPosition.clone()
  876. };
  877. console.log('[EnemyInstance] 抛掷物配置:', projectileConfig);
  878. // 创建抛掷物
  879. const projectileNode = enemyProjectile.createProjectile(projectileConfig);
  880. if (projectileNode) {
  881. console.log(`[EnemyInstance] 成功创建抛掷物,目标方向: ${direction.toString()}`);
  882. } else {
  883. console.error(`[EnemyInstance] 创建抛掷物失败 - 远程敌人攻击无效`);
  884. }
  885. } else {
  886. console.error(`[EnemyInstance] 未找到EnemyProjectile组件 - 远程敌人攻击无效`);
  887. }
  888. } else {
  889. console.error(`[EnemyInstance] 没有controller引用 - 远程敌人攻击无效`);
  890. }
  891. }
  892. // 播放行走动画
  893. private playWalkAnimation() {
  894. if (!this.skeleton) return;
  895. const enemyComp = this.getComponent('EnemyComponent') as any;
  896. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  897. const walkName = anims.walk ?? 'walk';
  898. const idleName = anims.idle ?? 'idle';
  899. let animationToPlay = '';
  900. if (this.skeleton.findAnimation(walkName)) {
  901. animationToPlay = walkName;
  902. } else if (this.skeleton.findAnimation(idleName)) {
  903. animationToPlay = idleName;
  904. }
  905. if (animationToPlay && this.currentAnimationName !== animationToPlay) {
  906. this.skeleton.setAnimation(0, animationToPlay, true);
  907. this.currentAnimationName = animationToPlay;
  908. this.isPlayingAttackAnimation = false;
  909. console.log(`[EnemyInstance] 播放行走动画: ${animationToPlay}`);
  910. }
  911. // 动画切换后延迟更新血条位置,确保动画尺寸已生效
  912. this.scheduleOnce(() => {
  913. this.updateHPBarPosition();
  914. }, 0.1);
  915. }
  916. // 播放攻击动画
  917. private playAttackAnimation() {
  918. if (!this.skeleton) return;
  919. const enemyComp2 = this.getComponent('EnemyComponent') as any;
  920. const anims2 = enemyComp2?.getAnimations ? enemyComp2.getAnimations() : {};
  921. const attackName = anims2.attack ?? 'attack';
  922. if (this.skeleton.findAnimation(attackName) && this.currentAnimationName !== attackName) {
  923. this.skeleton.setAnimation(0, attackName, true);
  924. this.currentAnimationName = attackName;
  925. this.isPlayingAttackAnimation = true;
  926. console.log(`[EnemyInstance] 播放攻击动画: ${attackName}`);
  927. }
  928. // 动画切换后延迟更新血条位置,确保动画尺寸已生效
  929. this.scheduleOnce(() => {
  930. this.updateHPBarPosition();
  931. }, 0.1);
  932. }
  933. // 播放待机动画
  934. private playIdleAnimation() {
  935. if (!this.skeleton) return;
  936. const enemyComp = this.getComponent('EnemyComponent') as any;
  937. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  938. const idleName = anims.idle ?? 'idle';
  939. if (this.skeleton.findAnimation(idleName) && this.currentAnimationName !== idleName) {
  940. this.skeleton.setAnimation(0, idleName, true);
  941. this.currentAnimationName = idleName;
  942. this.isPlayingAttackAnimation = false;
  943. console.log(`[EnemyInstance] 播放待机动画: ${idleName}`);
  944. }
  945. // 动画切换后延迟更新血条位置,确保动画尺寸已生效
  946. this.scheduleOnce(() => {
  947. this.updateHPBarPosition();
  948. }, 0.1);
  949. }
  950. // 安全播放待机动画(避免重复调用)
  951. private playIdleAnimationSafe() {
  952. if (!this.skeleton) return;
  953. const enemyComp = this.getComponent('EnemyComponent') as any;
  954. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  955. const idleName = anims.idle ?? 'idle';
  956. // 只有当前不是待机动画时才播放
  957. if (this.skeleton.findAnimation(idleName) && this.currentAnimationName !== idleName && !this.isPlayingAttackAnimation) {
  958. this.skeleton.setAnimation(0, idleName, true);
  959. this.currentAnimationName = idleName;
  960. console.log(`[EnemyInstance] 安全播放待机动画: ${idleName}`);
  961. // 动画切换后延迟更新血条位置,确保动画尺寸已生效
  962. this.scheduleOnce(() => {
  963. this.updateHPBarPosition();
  964. }, 0.1);
  965. }
  966. }
  967. // 播放攻击动画并在动画结束时造成伤害
  968. private playAttackAnimationWithDamage() {
  969. if (!this.skeleton) {
  970. // 如果没有骨骼动画,直接造成伤害
  971. this.dealDamageToWall();
  972. return;
  973. }
  974. const enemyComp = this.getComponent('EnemyComponent') as any;
  975. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  976. const attackName = anims.attack ?? 'attack';
  977. const animation = this.skeleton.findAnimation(attackName);
  978. if (animation) {
  979. // 播放攻击动画(不循环)
  980. this.skeleton.setAnimation(0, attackName, false);
  981. this.currentAnimationName = attackName;
  982. this.isPlayingAttackAnimation = true;
  983. console.log(`[EnemyInstance] 播放近战攻击动画: ${attackName}`);
  984. // 动画切换后延迟更新血条位置,确保动画尺寸已生效
  985. this.scheduleOnce(() => {
  986. this.updateHPBarPosition();
  987. }, 0.1);
  988. // 获取动画时长并在动画结束时造成伤害
  989. const animationDuration = animation.duration;
  990. console.log(`[EnemyInstance] 攻击动画时长: ${animationDuration}秒`);
  991. // 使用定时器在动画结束时造成伤害
  992. this.scheduleOnce(() => {
  993. this.dealDamageToWall();
  994. this.isPlayingAttackAnimation = false;
  995. // 动画完成后根据当前状态切换动画
  996. if (this.state === EnemyState.IDLE || this.state === EnemyState.ATTACKING) {
  997. // 如果是待机状态或攻击状态,切换回待机动画
  998. this.playIdleAnimation();
  999. } else {
  1000. // 其他状态切换回行走动画
  1001. this.playWalkAnimation();
  1002. }
  1003. }, animationDuration);
  1004. } else {
  1005. // 如果找不到攻击动画,直接造成伤害
  1006. this.dealDamageToWall();
  1007. }
  1008. }
  1009. // 对墙体造成伤害
  1010. private dealDamageToWall() {
  1011. if (this.controller) {
  1012. // 获取当前攻击力(考虑狂暴加成)
  1013. const enemyComponent = this.getComponent(EnemyComponent);
  1014. const currentAttackPower = enemyComponent ? enemyComponent.getCurrentAttackPower() : this.attackPower;
  1015. // 检查是否需要触发画面震动
  1016. if (enemyComponent && enemyComponent.getCausesWallShake()) {
  1017. const attackType = enemyComponent.getAttackType();
  1018. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 攻击墙体,触发画面震动,攻击类型: ${attackType}`);
  1019. // 根据攻击类型触发对应的震动效果
  1020. ScreenShakeManager.getInstance().playShakeByAttackType(attackType);
  1021. }
  1022. // 使用统一的事件机制触发墙体伤害
  1023. EventBus.getInstance().emit(GameEvents.ENEMY_DAMAGE_WALL, {
  1024. damage: currentAttackPower,
  1025. source: this.node
  1026. });
  1027. }
  1028. }
  1029. private playDeathAnimationAndDestroy() {
  1030. console.log(`[EnemyInstance] 开始播放死亡动画并销毁`);
  1031. // 播放死亡音效
  1032. EnemyAudio.playDeathSound(this.enemyConfig);
  1033. if (this.skeleton) {
  1034. const enemyComp = this.getComponent('EnemyComponent') as any;
  1035. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  1036. const deathName = anims.dead ?? 'dead';
  1037. if (this.skeleton.findAnimation(deathName)) {
  1038. this.skeleton.setAnimation(0, deathName, false);
  1039. // 销毁节点在动画完毕后
  1040. this.skeleton.setCompleteListener(() => {
  1041. this.node.destroy();
  1042. });
  1043. return;
  1044. }
  1045. }
  1046. this.node.destroy();
  1047. }
  1048. private spawnCoin() {
  1049. const ctrl = this.controller as any; // EnemyController
  1050. if (!ctrl?.coinPrefab) return;
  1051. // 获取配置中的掉落金币数量
  1052. const dropCoinsCount = this.getDropCoins();
  1053. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 掉落 ${dropCoinsCount} 个金币`);
  1054. // 根据配置掉落相应数量的金币
  1055. for (let i = 0; i < dropCoinsCount; i++) {
  1056. const coin = instantiate(ctrl.coinPrefab);
  1057. find('Canvas')!.addChild(coin); // 放到 UI 层
  1058. const pos = new Vec3();
  1059. this.node.getWorldPosition(pos); // 取死亡敌人的世界坐标
  1060. // 如果掉落多个金币,添加一些随机偏移避免重叠
  1061. if (dropCoinsCount > 1) {
  1062. const offsetX = (Math.random() - 0.5) * 40; // 随机X偏移 -20到20
  1063. const offsetY = (Math.random() - 0.5) * 40; // 随机Y偏移 -20到20
  1064. pos.x += offsetX;
  1065. pos.y += offsetY;
  1066. }
  1067. coin.worldPosition = pos; // 金币就在敌人身上出现
  1068. }
  1069. }
  1070. /**
  1071. * 暂停敌人
  1072. */
  1073. public pause(): void {
  1074. this.isPaused = true;
  1075. // 暂停物理系统
  1076. const enemySprite = this.node.getChildByName('EnemySprite');
  1077. if (enemySprite) {
  1078. const rigidBody = enemySprite.getComponent(RigidBody2D);
  1079. if (rigidBody) {
  1080. // 保存当前速度并停止物理移动
  1081. (this as any).savedVelocity = rigidBody.linearVelocity;
  1082. rigidBody.linearVelocity = new Vec2(0, 0);
  1083. }
  1084. }
  1085. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已暂停`);
  1086. }
  1087. /**
  1088. * 恢复敌人
  1089. */
  1090. public resume(): void {
  1091. this.isPaused = false;
  1092. // 恢复物理系统
  1093. const enemySprite = this.node.getChildByName('EnemySprite');
  1094. if (enemySprite) {
  1095. const rigidBody = enemySprite.getComponent(RigidBody2D);
  1096. if (rigidBody && (this as any).savedVelocity) {
  1097. // 恢复之前保存的速度
  1098. rigidBody.linearVelocity = (this as any).savedVelocity;
  1099. (this as any).savedVelocity = null;
  1100. }
  1101. }
  1102. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已恢复`);
  1103. }
  1104. /**
  1105. * 检查是否暂停
  1106. */
  1107. public isPausedState(): boolean {
  1108. return this.isPaused;
  1109. }
  1110. /**
  1111. * 获取敌人当前状态
  1112. */
  1113. public getState(): EnemyState {
  1114. return this.state;
  1115. }
  1116. /**
  1117. * 检查敌人是否处于漂移状态
  1118. */
  1119. public isDrifting(): boolean {
  1120. return this.state === EnemyState.DRIFTING;
  1121. }
  1122. /**
  1123. * 重置血条状态(满血并隐藏)
  1124. */
  1125. public resetHealthBar(): void {
  1126. if (this.hpBarAnimation) {
  1127. this.hpBarAnimation.resetToFullAndHide();
  1128. } else {
  1129. // 备用方案:直接隐藏血条
  1130. const hpBar = this.node.getChildByName('HPBar');
  1131. if (hpBar) {
  1132. hpBar.active = false;
  1133. const progressBar = hpBar.getComponent(ProgressBar);
  1134. if (progressBar) {
  1135. progressBar.progress = 1.0;
  1136. }
  1137. }
  1138. }
  1139. }
  1140. /**
  1141. * 尝试格挡攻击
  1142. */
  1143. private tryBlock(): boolean {
  1144. const enemyComponent = this.getComponent(EnemyComponent);
  1145. if (!enemyComponent) return false;
  1146. const blockChance = enemyComponent.getBlockChance();
  1147. // 将百分比值转换为0-1范围(例如:30.0 -> 0.3)
  1148. const blockChanceNormalized = blockChance / 100.0;
  1149. return Math.random() < blockChanceNormalized;
  1150. }
  1151. /**
  1152. * 基于距离检查墙体碰撞和攻击范围
  1153. */
  1154. private checkWallCollisionByDistance(): void {
  1155. if (this.state === EnemyState.ATTACKING || this.state === EnemyState.IDLE) {
  1156. return; // 已经在攻击状态或待机状态,不需要再检查
  1157. }
  1158. const enemySprite = this.node.getChildByName('EnemySprite');
  1159. if (!enemySprite) return;
  1160. const currentPos = enemySprite.worldPosition;
  1161. // 获取敌人的攻击范围配置
  1162. const enemyComponent = this.getComponent(EnemyComponent);
  1163. const attackRange = enemyComponent ? enemyComponent.getAttackRange() : 30;
  1164. // 根据攻击范围确定检测距离和攻击类型
  1165. let detectionDistance: number;
  1166. let isRangedAttack: boolean;
  1167. if (attackRange <= 0) {
  1168. // 近战攻击:需要接近墙体
  1169. detectionDistance = 30;
  1170. isRangedAttack = false;
  1171. } else {
  1172. // 远程攻击:在攻击范围内就开始攻击
  1173. detectionDistance = attackRange;
  1174. isRangedAttack = true;
  1175. }
  1176. // 获取所有墙体节点进行距离检测
  1177. const wallNodes = this.getAllWallNodes();
  1178. for (const wallNode of wallNodes) {
  1179. const wallPos = wallNode.worldPosition;
  1180. const distance = Vec3.distance(currentPos, wallPos);
  1181. if (distance <= detectionDistance) {
  1182. const attackTypeStr = isRangedAttack ? '远程攻击' : '近战攻击';
  1183. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 进入${attackTypeStr}范围,距离墙体 ${wallNode.name}: ${distance.toFixed(2)},攻击范围: ${attackRange}`);
  1184. if (isRangedAttack) {
  1185. // 远程攻击:切换到待机状态,停止移动并轮流播放攻击和待机动画
  1186. this.state = EnemyState.IDLE;
  1187. this.attackTimer = 0; // 立即开始攻击
  1188. this.collidedWall = wallNode; // 记录目标墙体
  1189. // 停止移动
  1190. this.stopRigidBodyMovement();
  1191. // 切换到待机动画
  1192. this.playIdleAnimation();
  1193. console.log(`[EnemyInstance] 远程敌人 ${this.getEnemyName()} 进入待机状态,停止移动并准备攻击`);
  1194. } else {
  1195. // 近战攻击:切换到待机状态,停止移动
  1196. this.state = EnemyState.IDLE;
  1197. this.attackTimer = 0; // 立即开始攻击
  1198. this.collidedWall = wallNode; // 记录碰撞的墙体
  1199. // 停止移动
  1200. this.stopRigidBodyMovement();
  1201. // 切换到待机动画
  1202. this.playIdleAnimation();
  1203. }
  1204. return; // 找到一个墙体就停止检查
  1205. }
  1206. }
  1207. }
  1208. /**
  1209. * 获取所有墙体节点
  1210. */
  1211. private getAllWallNodes(): Node[] {
  1212. const wallNodes: Node[] = [];
  1213. // 通过EnemyController获取墙体节点
  1214. if (this.controller) {
  1215. if (this.controller.topFenceNode) {
  1216. wallNodes.push(this.controller.topFenceNode);
  1217. }
  1218. if (this.controller.bottomFenceNode) {
  1219. wallNodes.push(this.controller.bottomFenceNode);
  1220. }
  1221. }
  1222. // 备用方案:在场景中查找墙体节点
  1223. if (wallNodes.length === 0) {
  1224. const gameArea = find('Canvas/GameLevelUI/GameArea');
  1225. if (gameArea) {
  1226. for (let i = 0; i < gameArea.children.length; i++) {
  1227. const child = gameArea.children[i];
  1228. if (this.isWallNode(child)) {
  1229. wallNodes.push(child);
  1230. }
  1231. }
  1232. }
  1233. }
  1234. return wallNodes;
  1235. }
  1236. /**
  1237. * 停止刚体移动,确保物理系统不干扰代码控制的移动
  1238. */
  1239. private stopRigidBodyMovement(): void {
  1240. const enemySprite = this.node.getChildByName('EnemySprite');
  1241. if (enemySprite) {
  1242. const rigidBody = enemySprite.getComponent(RigidBody2D);
  1243. if (rigidBody) {
  1244. rigidBody.linearVelocity = new Vec2(0, 0);
  1245. }
  1246. }
  1247. }
  1248. /**
  1249. * 显示格挡效果
  1250. */
  1251. private showBlockEffect(): void {
  1252. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 格挡了攻击`);
  1253. // 显示"格挡"文字效果
  1254. this.showBlockText();
  1255. }
  1256. /**
  1257. * 显示格挡文字
  1258. */
  1259. private showBlockText(): void {
  1260. // 使用DamageNumberAni组件来显示格挡文字,就像显示伤害数字一样
  1261. if (this.controller) {
  1262. const damageAni = this.controller.getComponent(DamageNumberAni);
  1263. if (damageAni) {
  1264. damageAni.showBlockText(this.node.worldPosition);
  1265. } else {
  1266. console.warn('[EnemyInstance] 未找到DamageNumberAni组件,无法显示格挡文字');
  1267. }
  1268. } else {
  1269. console.warn('[EnemyInstance] 未找到controller引用,无法显示格挡文字');
  1270. }
  1271. }
  1272. /**
  1273. * 检查是否触发狂暴状态
  1274. */
  1275. private checkRageTrigger(): void {
  1276. const enemyComponent = this.getComponent(EnemyComponent);
  1277. if (!enemyComponent) return;
  1278. const healthPercent = this.health / this.maxHealth;
  1279. const rageTriggerThreshold = enemyComponent.getRageTriggerThreshold();
  1280. if (healthPercent <= rageTriggerThreshold && !enemyComponent.isInRage()) {
  1281. enemyComponent.triggerRage();
  1282. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 进入狂暴状态`);
  1283. }
  1284. }
  1285. }