EnemyInstance.ts 61 KB

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