EnemyInstance.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. import { _decorator, Component, Node, ProgressBar, Label, Vec3, find, UITransform, Collider2D, Contact2DType, IPhysics2DContact, instantiate, resources, Prefab, JsonAsset, RigidBody2D, ERigidBody2DType, BoxCollider2D, CircleCollider2D } 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. const { ccclass, property } = _decorator;
  9. // 前向声明EnemyController接口,避免循环引用
  10. interface EnemyControllerType {
  11. gameBounds: {
  12. left: number;
  13. right: number;
  14. top: number;
  15. bottom: number;
  16. };
  17. damageWall: (damage: number) => void;
  18. getComponent: (componentType: any) => any;
  19. }
  20. // 敌人状态枚举
  21. enum EnemyState {
  22. DRIFTING, // 漂移中(从生成位置移动到线上)
  23. MOVING, // 移动中
  24. ATTACKING, // 攻击中
  25. DEAD // 死亡
  26. }
  27. // 单个敌人实例的组件
  28. @ccclass('EnemyInstance')
  29. export class EnemyInstance extends Component {
  30. // 敌人属性(从配置文件读取)
  31. public health: number = 0;
  32. public maxHealth: number = 0;
  33. public speed: number = 0;
  34. public attackPower: number = 0;
  35. // 敌人配置ID
  36. public enemyId: string = '';
  37. // 敌人配置数据
  38. private enemyConfig: any = null;
  39. // 敌人配置数据库
  40. private static enemyDatabase: any = null;
  41. // === 新增属性 ===
  42. /** 是否从上方生成 */
  43. public spawnFromTop: boolean = true;
  44. /** 目标 Fence 节点(TopFence / BottomFence) */
  45. public targetFence: Node | null = null;
  46. /** 目标位置(墙体上的随机点) */
  47. public targetPosition: Vec3 | null = null;
  48. /** 漂移目标位置(线上的位置) */
  49. public driftTargetPosition: Vec3 | null = null;
  50. // 移动相关属性
  51. public movingDirection: number = 1; // 1: 向右, -1: 向左
  52. public targetY: number = 0; // 目标Y位置
  53. public changeDirectionTime: number = 0; // 下次改变方向的时间
  54. // 摆动移动相关属性
  55. private swayTime: number = 0; // 摆动时间累计
  56. private basePosition: Vec3 = new Vec3(); // 基础位置(摆动的中心点)
  57. private swayOffset: Vec3 = new Vec3(); // 当前摆动偏移
  58. // 攻击属性
  59. public attackInterval: number = 0; // 攻击间隔(秒),从配置文件读取
  60. private attackTimer: number = 0;
  61. // 对控制器的引用
  62. public controller: EnemyControllerType = null;
  63. // 敌人当前状态
  64. private state: EnemyState = EnemyState.DRIFTING;
  65. // 游戏区域中心
  66. private gameAreaCenter: Vec3 = new Vec3();
  67. // 碰撞的墙体
  68. private collidedWall: Node = null;
  69. // 骨骼动画组件
  70. private skeleton: sp.Skeleton | null = null;
  71. // 血条动画组件
  72. private hpBarAnimation: HPBarAnimation | null = null;
  73. // 暂停状态标记
  74. private isPaused: boolean = false;
  75. start() {
  76. // 初始化敌人
  77. this.initializeEnemy();
  78. }
  79. // 静态方法:加载敌人配置数据库
  80. public static async loadEnemyDatabase(): Promise<void> {
  81. if (EnemyInstance.enemyDatabase) return;
  82. try {
  83. EnemyInstance.enemyDatabase = await JsonConfigLoader.getInstance().loadConfig('enemies');
  84. if (!EnemyInstance.enemyDatabase) {
  85. throw new Error('敌人配置文件内容为空');
  86. }
  87. } catch (error) {
  88. console.error('[EnemyInstance] 加载敌人配置失败:', error);
  89. throw error;
  90. }
  91. }
  92. // 设置敌人配置
  93. public setEnemyConfig(enemyId: string): void {
  94. this.enemyId = enemyId;
  95. if (!EnemyInstance.enemyDatabase) {
  96. console.error('[EnemyInstance] 敌人配置数据库未加载');
  97. return;
  98. }
  99. // 从数据库中查找敌人配置
  100. // 修复:enemies.json是直接的数组结构,不需要.enemies包装
  101. const enemies = EnemyInstance.enemyDatabase;
  102. this.enemyConfig = enemies.find((enemy: any) => enemy.id === enemyId);
  103. if (!this.enemyConfig) {
  104. console.error(`[EnemyInstance] 未找到敌人配置: ${enemyId}`);
  105. return;
  106. }
  107. // 应用配置到敌人属性
  108. this.applyEnemyConfig();
  109. }
  110. // 应用敌人配置到属性
  111. private applyEnemyConfig(): void {
  112. if (!this.enemyConfig) return;
  113. // 从stats节点读取基础属性
  114. const stats = this.enemyConfig.stats || {};
  115. this.health = stats.health || 30;
  116. this.maxHealth = stats.maxHealth || this.health;
  117. // 从movement节点读取移动速度
  118. const movement = this.enemyConfig.movement || {};
  119. this.speed = movement.speed || 50;
  120. // 从combat节点读取攻击力
  121. const combat = this.enemyConfig.combat || {};
  122. this.attackPower = combat.attackDamage || 10;
  123. // 设置攻击间隔
  124. this.attackInterval = combat.attackCooldown || 2.0;
  125. }
  126. // 获取敌人配置信息
  127. public getEnemyConfig(): any {
  128. return this.enemyConfig;
  129. }
  130. // 获取敌人名称
  131. public getEnemyName(): string {
  132. return this.enemyConfig?.name || '未知敌人';
  133. }
  134. // 获取敌人类型
  135. public getEnemyType(): string {
  136. return this.enemyConfig?.type || 'basic';
  137. }
  138. // 获取敌人稀有度
  139. public getEnemyRarity(): string {
  140. return this.enemyConfig?.rarity || 'common';
  141. }
  142. // 获取金币奖励
  143. public getGoldReward(): number {
  144. return this.enemyConfig?.goldReward || 1;
  145. }
  146. // 获取掉落金币数量
  147. public getDropCoins(): number {
  148. const stats = this.enemyConfig?.stats || {};
  149. return stats.dropCoins || 1;
  150. }
  151. // 初始化敌人
  152. private initializeEnemy() {
  153. // 确保血量正确设置
  154. if (this.maxHealth > 0) {
  155. this.health = this.maxHealth;
  156. }
  157. this.state = EnemyState.DRIFTING; // 从漂移状态开始
  158. // 只有在攻击间隔未设置时才使用默认值
  159. if (this.attackInterval <= 0) {
  160. this.attackInterval = 2.0; // 默认攻击间隔
  161. }
  162. this.attackTimer = 0;
  163. // 初始化血条动画组件
  164. this.initializeHPBarAnimation();
  165. // 初始化骨骼动画组件 - 从EnemySprite子节点获取
  166. const enemySprite = this.node.getChildByName('EnemySprite');
  167. if (enemySprite) {
  168. this.skeleton = enemySprite.getComponent(sp.Skeleton);
  169. } else {
  170. console.error('[EnemyInstance] 未找到EnemySprite子节点,无法获取骨骼动画组件');
  171. }
  172. this.playWalkAnimation();
  173. // 计算游戏区域中心
  174. this.calculateGameAreaCenter();
  175. // 初始化碰撞检测
  176. this.setupCollider();
  177. }
  178. // 设置碰撞器
  179. setupCollider() {
  180. // 从EnemySprite子节点获取碰撞器组件
  181. const enemySprite = this.node.getChildByName('EnemySprite');
  182. if (!enemySprite) {
  183. console.error('[EnemyInstance] 未找到EnemySprite子节点,无法设置碰撞器组件');
  184. return;
  185. }
  186. // 检查EnemySprite节点是否有碰撞器
  187. let collider = enemySprite.getComponent(Collider2D);
  188. if (!collider) {
  189. console.warn(`[EnemyInstance] 敌人节点 ${this.node.name} 的EnemySprite子节点没有碰撞器组件`);
  190. return;
  191. }
  192. // 确保有RigidBody2D组件,这对于碰撞检测是必需的
  193. let rigidBody = enemySprite.getComponent(RigidBody2D);
  194. if (!rigidBody) {
  195. console.log(`[EnemyInstance] 为敌人EnemySprite节点添加RigidBody2D组件`);
  196. rigidBody = enemySprite.addComponent(RigidBody2D);
  197. }
  198. // 设置刚体属性
  199. if (rigidBody) {
  200. rigidBody.type = ERigidBody2DType.Dynamic; // 动态刚体
  201. rigidBody.enabledContactListener = true; // 启用碰撞监听
  202. rigidBody.gravityScale = 0; // 不受重力影响
  203. rigidBody.linearDamping = 0; // 无线性阻尼
  204. rigidBody.angularDamping = 0; // 无角阻尼
  205. rigidBody.allowSleep = false; // 不允许休眠
  206. rigidBody.fixedRotation = true; // 固定旋转
  207. }
  208. // 根据ContentSize自适应碰撞体大小
  209. this.adaptColliderToContentSize(collider);
  210. // 设置碰撞事件监听
  211. collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
  212. console.log(`[EnemyInstance] 敌人 ${this.node.name} 碰撞器设置完成,碰撞器启用: ${collider.enabled}, 刚体启用: ${rigidBody?.enabled}`);
  213. }
  214. /**
  215. * 根据ContentSize自适应碰撞体大小
  216. */
  217. private adaptColliderToContentSize(collider: Collider2D): void {
  218. // 从EnemySprite子节点获取UITransform组件
  219. const enemySprite = this.node.getChildByName('EnemySprite');
  220. if (!enemySprite) {
  221. console.error('[EnemyInstance] 未找到EnemySprite子节点,无法获取UITransform组件');
  222. return;
  223. }
  224. const uiTransform = enemySprite.getComponent(UITransform);
  225. if (!uiTransform) {
  226. console.warn(`[EnemyInstance] EnemySprite节点没有UITransform组件,无法自适应碰撞体大小`);
  227. return;
  228. }
  229. // 尝试获取骨骼动画组件来获取更准确的尺寸
  230. const skeleton = enemySprite.getComponent(sp.Skeleton);
  231. let actualWidth = uiTransform.contentSize.width;
  232. let actualHeight = uiTransform.contentSize.height;
  233. if (skeleton && skeleton.skeletonData) {
  234. // 如果有骨骼动画,尝试从骨骼数据获取尺寸信息
  235. try {
  236. // 使用骨骼数据的默认尺寸,或者根据动画缩放计算
  237. const skeletonData = skeleton.skeletonData;
  238. if (skeletonData) {
  239. // 获取骨骼的缩放比例
  240. const scaleX = Math.abs(skeleton.node.scale.x);
  241. const scaleY = Math.abs(skeleton.node.scale.y);
  242. // 使用合理的默认尺寸并应用缩放
  243. actualWidth = Math.max(60 * scaleX, uiTransform.contentSize.width * 0.8);
  244. actualHeight = Math.max(80 * scaleY, uiTransform.contentSize.height * 0.8);
  245. console.log(`[EnemyInstance] 使用骨骼动画尺寸 (scale: ${scaleX.toFixed(2)}, ${scaleY.toFixed(2)}): ${actualWidth.toFixed(2)} x ${actualHeight.toFixed(2)}`);
  246. } else {
  247. // 如果无法获取骨骼数据,使用默认尺寸
  248. actualWidth = Math.max(60, uiTransform.contentSize.width * 0.8);
  249. actualHeight = Math.max(80, uiTransform.contentSize.height * 0.8);
  250. console.log(`[EnemyInstance] 使用默认骨骼尺寸: ${actualWidth.toFixed(2)} x ${actualHeight.toFixed(2)}`);
  251. }
  252. } catch (error) {
  253. console.warn(`[EnemyInstance] 获取骨骼信息失败,使用默认尺寸:`, error);
  254. actualWidth = Math.max(60, uiTransform.contentSize.width * 0.8);
  255. actualHeight = Math.max(80, uiTransform.contentSize.height * 0.8);
  256. }
  257. }
  258. console.log(`[EnemyInstance] 敌人 ${this.node.name} 实际尺寸: ${actualWidth.toFixed(2)} x ${actualHeight.toFixed(2)}`);
  259. // 根据碰撞器类型设置大小
  260. if (collider instanceof BoxCollider2D) {
  261. // 方形碰撞器:使用实际尺寸,稍微缩小以避免过于敏感的碰撞
  262. const boxCollider = collider as BoxCollider2D;
  263. boxCollider.size.width = actualWidth * 0.9; // 缩小10%
  264. boxCollider.size.height = actualHeight * 0.9; // 缩小10%
  265. // 调整偏移量,使碰撞器居中对齐到EnemySprite
  266. const anchorPoint = uiTransform.anchorPoint;
  267. // 对于骨骼动画,通常锚点在底部中心(0.5, 0),需要向上偏移
  268. boxCollider.offset.x = 0; // X轴居中
  269. boxCollider.offset.y = actualHeight * (0.5 - anchorPoint.y) * 0.9; // Y轴根据锚点调整,并匹配缩放后的尺寸
  270. 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)})`);
  271. } else if (collider instanceof CircleCollider2D) {
  272. // 圆形碰撞器:使用实际尺寸的较小值作为直径,稍微缩小
  273. const circleCollider = collider as CircleCollider2D;
  274. const radius = Math.min(actualWidth, actualHeight) / 2 * 0.9; // 缩小10%
  275. circleCollider.radius = radius;
  276. // 调整偏移量,使碰撞器居中对齐到EnemySprite
  277. const anchorPoint = uiTransform.anchorPoint;
  278. // 对于骨骼动画,通常锚点在底部中心(0.5, 0),需要向上偏移
  279. circleCollider.offset.x = 0; // X轴居中
  280. circleCollider.offset.y = actualHeight * (0.5 - anchorPoint.y) * 0.9; // Y轴根据锚点调整,并匹配缩放后的尺寸
  281. 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)})`);
  282. } else {
  283. console.warn(`[EnemyInstance] 不支持的碰撞器类型: ${collider.constructor.name}`);
  284. }
  285. }
  286. // 碰撞开始事件
  287. onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
  288. const nodeName = otherCollider.node.name;
  289. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 碰撞检测 - 碰撞对象: ${nodeName}, 当前状态: ${EnemyState[this.state]}`);
  290. // 只有在移动状态下才能切换到攻击状态,避免重复触发
  291. if (this.state !== EnemyState.MOVING) {
  292. return;
  293. }
  294. // 检查是否碰到墙体(更严格的墙体识别)
  295. const isWall = this.isWallNode(otherCollider.node);
  296. if (isWall) {
  297. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 碰撞到墙体 ${nodeName},切换到攻击状态`);
  298. this.state = EnemyState.ATTACKING;
  299. this.attackTimer = 0; // 立即开始攻击
  300. this.collidedWall = otherCollider.node; // 记录碰撞的墙体
  301. // 切换攻击动画
  302. this.playAttackAnimation();
  303. }
  304. }
  305. // 检查节点是否为墙体
  306. private isWallNode(node: Node): boolean {
  307. const nodeName = node.name.toLowerCase();
  308. // 检查节点名称是否包含墙体关键词
  309. const wallKeywords = ['wall', 'fence', 'jiguang', '墙', '围栏'];
  310. const isWallByName = wallKeywords.some(keyword => nodeName.includes(keyword));
  311. // 检查节点是否有Wall组件
  312. const hasWallComponent = node.getComponent('Wall') !== null;
  313. return isWallByName || hasWallComponent;
  314. }
  315. // 获取节点路径
  316. getNodePath(node: Node): string {
  317. let path = node.name;
  318. let current = node;
  319. while (current.parent) {
  320. current = current.parent;
  321. path = current.name + '/' + path;
  322. }
  323. return path;
  324. }
  325. // 计算游戏区域中心
  326. private calculateGameAreaCenter() {
  327. const gameArea = find('Canvas/GameLevelUI/GameArea');
  328. if (gameArea) {
  329. this.gameAreaCenter = gameArea.worldPosition;
  330. }
  331. }
  332. /**
  333. * 初始化血条动画组件
  334. */
  335. private initializeHPBarAnimation() {
  336. const hpBar = this.node.getChildByName('HPBar');
  337. if (hpBar) {
  338. // 查找红色和黄色血条节点
  339. const redBarNode = hpBar.getChildByName('RedBar');
  340. const yellowBarNode = hpBar.getChildByName('YellowBar');
  341. if (redBarNode && yellowBarNode) {
  342. // 添加血条动画组件
  343. this.hpBarAnimation = this.node.addComponent(HPBarAnimation);
  344. if (this.hpBarAnimation) {
  345. // 正确设置红色和黄色血条节点引用
  346. this.hpBarAnimation.redBarNode = redBarNode;
  347. this.hpBarAnimation.yellowBarNode = yellowBarNode;
  348. this.hpBarAnimation.hpBarRootNode = hpBar; // 设置HPBar根节点
  349. console.log(`[EnemyInstance] 血条动画组件已初始化`);
  350. }
  351. } else {
  352. console.warn(`[EnemyInstance] HPBar下未找到RedBar或YellowBar节点,RedBar: ${!!redBarNode}, YellowBar: ${!!yellowBarNode}`);
  353. }
  354. } else {
  355. console.warn(`[EnemyInstance] 未找到HPBar节点,无法初始化血条动画`);
  356. }
  357. }
  358. // 更新血量显示
  359. updateHealthDisplay(showBar: boolean = false) {
  360. // 确保血量值在有效范围内
  361. this.health = Math.max(0, Math.min(this.maxHealth, this.health));
  362. const healthProgress = this.maxHealth > 0 ? this.health / this.maxHealth : 0;
  363. console.log(`[EnemyInstance] 更新血量显示: ${this.health}/${this.maxHealth} (${(healthProgress * 100).toFixed(1)}%)`);
  364. // 使用血条动画组件更新血条
  365. if (this.hpBarAnimation) {
  366. this.hpBarAnimation.updateProgress(healthProgress, showBar);
  367. } else {
  368. // 备用方案:直接更新血条
  369. const hpBar = this.node.getChildByName('HPBar');
  370. if (hpBar) {
  371. const progressBar = hpBar.getComponent(ProgressBar);
  372. if (progressBar) {
  373. progressBar.progress = healthProgress;
  374. }
  375. // 根据showBar参数控制血条显示
  376. hpBar.active = showBar;
  377. }
  378. }
  379. // 更新血量数字
  380. const hpLabel = this.node.getChildByName('HPLabel');
  381. if (hpLabel) {
  382. const label = hpLabel.getComponent(Label);
  383. if (label) {
  384. // 显示整数血量值
  385. label.string = Math.ceil(this.health).toString();
  386. }
  387. }
  388. }
  389. // 受到伤害
  390. takeDamage(damage: number, isCritical: boolean = false) {
  391. // 如果已经死亡,不再处理伤害
  392. if (this.state === EnemyState.DEAD) {
  393. return;
  394. }
  395. // 确保伤害值为正数
  396. if (damage <= 0) {
  397. console.warn(`[EnemyInstance] 无效的伤害值: ${damage}`);
  398. return;
  399. }
  400. // 应用防御力减少伤害
  401. const enemyComponent = this.getComponent(EnemyComponent);
  402. const defense = enemyComponent ? enemyComponent.getDefense() : 0;
  403. let finalDamage = Math.max(1, damage - defense); // 至少造成1点伤害
  404. // 检查格挡
  405. if (enemyComponent && enemyComponent.canBlock() && this.tryBlock()) {
  406. finalDamage *= (1 - enemyComponent.getBlockDamageReduction());
  407. this.showBlockEffect();
  408. }
  409. // 计算新的血量,确保不会低于0
  410. const oldHealth = this.health;
  411. const newHealth = Math.max(0, this.health - finalDamage);
  412. const actualHealthLoss = oldHealth - newHealth; // 实际血量损失
  413. this.health = newHealth;
  414. // 检查是否触发狂暴
  415. this.checkRageTrigger();
  416. // 日志显示武器的真实伤害值,而不是血量差值
  417. console.log(`[EnemyInstance] 敌人受到伤害: ${damage} (武器伤害), 防御后伤害: ${finalDamage}, 实际血量损失: ${actualHealthLoss}, 剩余血量: ${this.health}/${this.maxHealth}`);
  418. // 受击音效已移除
  419. // 显示伤害数字动画(在敌人头顶)- 显示防御后的实际伤害
  420. // 优先使用EnemyController节点上的DamageNumberAni组件实例
  421. if (this.controller) {
  422. const damageAni = this.controller.getComponent(DamageNumberAni);
  423. if (damageAni) {
  424. damageAni.showDamageNumber(finalDamage, this.node.worldPosition, isCritical);
  425. } else {
  426. // 如果没有找到组件实例,使用静态方法作为备用
  427. DamageNumberAni.showDamageNumber(finalDamage, this.node.worldPosition, isCritical);
  428. }
  429. } else {
  430. // 如果没有controller引用,使用静态方法
  431. DamageNumberAni.showDamageNumber(finalDamage, this.node.worldPosition, isCritical);
  432. }
  433. // 更新血量显示和动画,受伤时显示血条
  434. this.updateHealthDisplay(true);
  435. // 如果血量低于等于0,销毁敌人
  436. if (this.health <= 0) {
  437. console.log(`[EnemyInstance] 敌人死亡,开始销毁流程`);
  438. this.state = EnemyState.DEAD;
  439. this.spawnCoin();
  440. // 进入死亡流程,禁用碰撞避免重复命中
  441. const enemySprite = this.node.getChildByName('EnemySprite');
  442. if (enemySprite) {
  443. const col = enemySprite.getComponent(Collider2D);
  444. if (col) col.enabled = false;
  445. }
  446. this.playDeathAnimationAndDestroy();
  447. }
  448. }
  449. onDestroy() {
  450. console.log(`[EnemyInstance] onDestroy 被调用,准备通知控制器`);
  451. // 通知控制器 & GameManager
  452. if (this.controller && typeof (this.controller as any).notifyEnemyDead === 'function') {
  453. // 检查控制器是否处于清理状态,避免在清理过程中触发游戏事件
  454. const isClearing = (this.controller as any).isClearing;
  455. if (isClearing) {
  456. console.log(`[EnemyInstance] 控制器处于清理状态,跳过死亡通知`);
  457. return;
  458. }
  459. console.log(`[EnemyInstance] 调用 notifyEnemyDead`);
  460. (this.controller as any).notifyEnemyDead(this.node);
  461. } else {
  462. console.warn(`[EnemyInstance] 无法调用 notifyEnemyDead: controller=${!!this.controller}`);
  463. }
  464. }
  465. update(deltaTime: number) {
  466. // 如果敌人被暂停,则不执行任何更新逻辑
  467. if (this.isPaused) {
  468. return;
  469. }
  470. if (this.state === EnemyState.DRIFTING) {
  471. this.updateDrifting(deltaTime);
  472. } else if (this.state === EnemyState.MOVING) {
  473. this.updateMovement(deltaTime);
  474. } else if (this.state === EnemyState.ATTACKING) {
  475. this.updateAttack(deltaTime);
  476. }
  477. // 不再每帧播放攻击动画,避免日志刷屏
  478. }
  479. // 更新漂移逻辑(从生成位置移动到线上)
  480. private updateDrifting(deltaTime: number) {
  481. if (!this.driftTargetPosition) {
  482. // 如果没有漂移目标位置,直接切换到移动状态
  483. this.state = EnemyState.MOVING;
  484. return;
  485. }
  486. const currentWorldPos = this.node.worldPosition.clone();
  487. const targetWorldPos = this.driftTargetPosition.clone();
  488. const dir = targetWorldPos.subtract(currentWorldPos);
  489. const distanceToTarget = dir.length();
  490. // 如果距离目标很近,切换到移动状态
  491. const stopDistance = 10; // 停止距离
  492. if (distanceToTarget <= stopDistance) {
  493. this.state = EnemyState.MOVING;
  494. return;
  495. }
  496. if (distanceToTarget === 0) return;
  497. dir.normalize();
  498. // 使用较快的速度进行漂移
  499. const driftSpeed = this.speed * 5; // 漂移速度比正常移动快
  500. const moveDistance = driftSpeed * deltaTime;
  501. const newWorldPos = currentWorldPos.add(dir.multiplyScalar(moveDistance));
  502. // 直接设置世界坐标
  503. this.node.setWorldPosition(newWorldPos);
  504. }
  505. // 更新移动逻辑
  506. private updateMovement(deltaTime: number) {
  507. // 继续移动到目标墙体
  508. this.moveTowardsTarget(deltaTime);
  509. }
  510. // 移动到目标位置
  511. private moveTowardsTarget(deltaTime: number) {
  512. // 使用世界坐标进行移动计算,确保不受父节点坐标系影响
  513. const currentWorldPos = this.node.worldPosition.clone();
  514. // 目标世界坐标:使用预设的目标位置,而不是墙体中心
  515. let targetWorldPos: Vec3;
  516. if (this.targetPosition) {
  517. targetWorldPos = this.targetPosition.clone();
  518. } else if (this.targetFence && this.targetFence.isValid) {
  519. // 如果没有预设目标位置,则使用墙体中心作为备用
  520. targetWorldPos = this.targetFence.worldPosition.clone();
  521. } else {
  522. targetWorldPos = this.gameAreaCenter.clone();
  523. }
  524. const dir = targetWorldPos.subtract(currentWorldPos);
  525. const distanceToTarget = dir.length();
  526. // 移除基于距离的攻击触发逻辑,只通过碰撞回调触发攻击
  527. // 这样可以确保攻击触发的准确性,避免位置计算误差
  528. // 移除距离为0时的停止逻辑,让敌人继续按原方向移动直到碰撞体触发攻击状态
  529. // 如果距离为0,保持上一次的移动方向继续移动
  530. if (distanceToTarget > 0) {
  531. dir.normalize();
  532. } else {
  533. // 距离为0时,使用上一次保存的移动方向或默认方向
  534. dir.set(this.movingDirection, 0, 0);
  535. }
  536. // 获取当前速度(考虑狂暴加成)
  537. const enemyComponent = this.getComponent(EnemyComponent);
  538. const currentSpeed = enemyComponent ? enemyComponent.getCurrentSpeed() : this.speed;
  539. const moveDistance = currentSpeed * deltaTime;
  540. let newWorldPos = currentWorldPos.add(dir.multiplyScalar(moveDistance));
  541. // 应用摆动效果
  542. if (enemyComponent) {
  543. const swayAmplitude = enemyComponent.getSwingAmplitude();
  544. const swayFrequency = enemyComponent.getSwingFrequency();
  545. if (swayAmplitude > 0 && swayFrequency > 0) {
  546. this.swayTime += deltaTime;
  547. // 计算垂直于移动方向的摆动偏移
  548. const swayOffset = Math.sin(this.swayTime * swayFrequency * 2 * Math.PI) * swayAmplitude;
  549. // 计算垂直于移动方向的向量
  550. const perpDir = new Vec3(-dir.y, dir.x, 0);
  551. const swayVector = perpDir.multiplyScalar(swayOffset);
  552. newWorldPos = newWorldPos.add(swayVector);
  553. }
  554. }
  555. // 直接设置世界坐标
  556. this.node.setWorldPosition(newWorldPos);
  557. }
  558. // 更新攻击逻辑
  559. private updateAttack(deltaTime: number) {
  560. this.attackTimer -= deltaTime;
  561. if (this.attackTimer <= 0) {
  562. // 执行攻击
  563. this.performAttack();
  564. // 重置攻击计时器
  565. this.attackTimer = this.attackInterval;
  566. }
  567. }
  568. // 执行攻击
  569. private performAttack() {
  570. if (!this.controller) {
  571. return;
  572. }
  573. const enemyComponent = this.getComponent(EnemyComponent);
  574. const attackType = enemyComponent ? enemyComponent.getAttackType() : 'melee';
  575. // 播放攻击音效
  576. EnemyAudio.playAttackSound(this.enemyConfig);
  577. // 根据攻击类型执行不同的攻击逻辑
  578. if (attackType === 'ranged' || attackType === 'projectile') {
  579. this.performRangedAttack();
  580. } else {
  581. // 近战攻击:播放攻击动画,动画结束后造成伤害
  582. this.playAttackAnimationWithDamage();
  583. }
  584. }
  585. // 执行远程攻击(投掷)
  586. private performRangedAttack() {
  587. const enemyComponent = this.getComponent(EnemyComponent);
  588. if (!enemyComponent) return;
  589. const projectileType = enemyComponent.getProjectileType();
  590. const projectileSpeed = enemyComponent.getProjectileSpeed();
  591. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 发射投掷物: ${projectileType}, 速度: ${projectileSpeed}`);
  592. // TODO: 实际创建和发射投掷物的逻辑
  593. // 这里需要根据项目的投掷物系统来实现
  594. // 例如:创建投掷物预制体,设置速度和方向,添加碰撞检测等
  595. // 暂时直接造成伤害作为占位符
  596. this.scheduleOnce(() => {
  597. this.dealDamageToWall();
  598. }, 1.0); // 1秒后造成伤害,模拟投掷物飞行时间
  599. }
  600. // 播放行走动画
  601. private playWalkAnimation() {
  602. if (!this.skeleton) return;
  603. const enemyComp = this.getComponent('EnemyComponent') as any;
  604. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  605. const walkName = anims.walk ?? 'walk';
  606. const idleName = anims.idle ?? 'idle';
  607. if (this.skeleton.findAnimation(walkName)) {
  608. this.skeleton.setAnimation(0, walkName, true);
  609. // 行走音效已移除
  610. } else if (this.skeleton.findAnimation(idleName)) {
  611. this.skeleton.setAnimation(0, idleName, true);
  612. }
  613. }
  614. // 播放攻击动画
  615. private playAttackAnimation() {
  616. if (!this.skeleton) return;
  617. const enemyComp2 = this.getComponent('EnemyComponent') as any;
  618. const anims2 = enemyComp2?.getAnimations ? enemyComp2.getAnimations() : {};
  619. const attackName = anims2.attack ?? 'attack';
  620. // 移除频繁打印
  621. if (this.skeleton.findAnimation(attackName)) {
  622. this.skeleton.setAnimation(0, attackName, true);
  623. }
  624. }
  625. // 播放攻击动画并在动画结束时造成伤害
  626. private playAttackAnimationWithDamage() {
  627. if (!this.skeleton) {
  628. // 如果没有骨骼动画,直接造成伤害
  629. this.dealDamageToWall();
  630. return;
  631. }
  632. const enemyComp = this.getComponent('EnemyComponent') as any;
  633. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  634. const attackName = anims.attack ?? 'attack';
  635. const animation = this.skeleton.findAnimation(attackName);
  636. if (animation) {
  637. // 播放攻击动画(不循环)
  638. this.skeleton.setAnimation(0, attackName, false);
  639. // 获取动画时长并在动画结束时造成伤害
  640. const animationDuration = animation.duration;
  641. console.log(`[EnemyInstance] 攻击动画时长: ${animationDuration}秒`);
  642. // 使用定时器在动画结束时造成伤害
  643. this.scheduleOnce(() => {
  644. this.dealDamageToWall();
  645. // 动画完成后切换回行走动画
  646. this.playWalkAnimation();
  647. }, animationDuration);
  648. } else {
  649. // 如果找不到攻击动画,直接造成伤害
  650. this.dealDamageToWall();
  651. }
  652. }
  653. // 对墙体造成伤害
  654. private dealDamageToWall() {
  655. if (this.controller) {
  656. // 获取当前攻击力(考虑狂暴加成)
  657. const enemyComponent = this.getComponent(EnemyComponent);
  658. const currentAttackPower = enemyComponent ? enemyComponent.getCurrentAttackPower() : this.attackPower;
  659. this.controller.damageWall(currentAttackPower);
  660. }
  661. }
  662. private playDeathAnimationAndDestroy() {
  663. console.log(`[EnemyInstance] 开始播放死亡动画并销毁`);
  664. // 播放死亡音效
  665. EnemyAudio.playDeathSound(this.enemyConfig);
  666. if (this.skeleton) {
  667. const enemyComp = this.getComponent('EnemyComponent') as any;
  668. const anims = enemyComp?.getAnimations ? enemyComp.getAnimations() : {};
  669. const deathName = anims.dead ?? 'dead';
  670. if (this.skeleton.findAnimation(deathName)) {
  671. this.skeleton.setAnimation(0, deathName, false);
  672. // 销毁节点在动画完毕后
  673. this.skeleton.setCompleteListener(() => {
  674. this.node.destroy();
  675. });
  676. return;
  677. }
  678. }
  679. this.node.destroy();
  680. }
  681. private spawnCoin() {
  682. const ctrl = this.controller as any; // EnemyController
  683. if (!ctrl?.coinPrefab) return;
  684. // 获取配置中的掉落金币数量
  685. const dropCoinsCount = this.getDropCoins();
  686. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 掉落 ${dropCoinsCount} 个金币`);
  687. // 根据配置掉落相应数量的金币
  688. for (let i = 0; i < dropCoinsCount; i++) {
  689. const coin = instantiate(ctrl.coinPrefab);
  690. find('Canvas')!.addChild(coin); // 放到 UI 层
  691. const pos = new Vec3();
  692. this.node.getWorldPosition(pos); // 取死亡敌人的世界坐标
  693. // 如果掉落多个金币,添加一些随机偏移避免重叠
  694. if (dropCoinsCount > 1) {
  695. const offsetX = (Math.random() - 0.5) * 40; // 随机X偏移 -20到20
  696. const offsetY = (Math.random() - 0.5) * 40; // 随机Y偏移 -20到20
  697. pos.x += offsetX;
  698. pos.y += offsetY;
  699. }
  700. coin.worldPosition = pos; // 金币就在敌人身上出现
  701. }
  702. }
  703. /**
  704. * 暂停敌人
  705. */
  706. public pause(): void {
  707. this.isPaused = true;
  708. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已暂停`);
  709. }
  710. /**
  711. * 恢复敌人
  712. */
  713. public resume(): void {
  714. this.isPaused = false;
  715. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 已恢复`);
  716. }
  717. /**
  718. * 检查是否暂停
  719. */
  720. public isPausedState(): boolean {
  721. return this.isPaused;
  722. }
  723. /**
  724. * 获取敌人当前状态
  725. */
  726. public getState(): EnemyState {
  727. return this.state;
  728. }
  729. /**
  730. * 检查敌人是否处于漂移状态
  731. */
  732. public isDrifting(): boolean {
  733. return this.state === EnemyState.DRIFTING;
  734. }
  735. /**
  736. * 重置血条状态(满血并隐藏)
  737. */
  738. public resetHealthBar(): void {
  739. if (this.hpBarAnimation) {
  740. this.hpBarAnimation.resetToFullAndHide();
  741. } else {
  742. // 备用方案:直接隐藏血条
  743. const hpBar = this.node.getChildByName('HPBar');
  744. if (hpBar) {
  745. hpBar.active = false;
  746. const progressBar = hpBar.getComponent(ProgressBar);
  747. if (progressBar) {
  748. progressBar.progress = 1.0;
  749. }
  750. }
  751. }
  752. }
  753. /**
  754. * 尝试格挡攻击
  755. */
  756. private tryBlock(): boolean {
  757. const enemyComponent = this.getComponent(EnemyComponent);
  758. if (!enemyComponent) return false;
  759. const blockChance = enemyComponent.getBlockChance();
  760. return Math.random() < blockChance;
  761. }
  762. /**
  763. * 显示格挡效果
  764. */
  765. private showBlockEffect(): void {
  766. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 格挡了攻击`);
  767. // TODO: 添加格挡视觉效果
  768. }
  769. /**
  770. * 检查是否触发狂暴状态
  771. */
  772. private checkRageTrigger(): void {
  773. const enemyComponent = this.getComponent(EnemyComponent);
  774. if (!enemyComponent) return;
  775. const healthPercent = this.health / this.maxHealth;
  776. const rageTriggerThreshold = enemyComponent.getRageTriggerThreshold();
  777. if (healthPercent <= rageTriggerThreshold && !enemyComponent.isInRage()) {
  778. enemyComponent.triggerRage();
  779. console.log(`[EnemyInstance] 敌人 ${this.getEnemyName()} 进入狂暴状态`);
  780. }
  781. }
  782. }