Wall.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. import { _decorator, Component, Node, Label, find, JsonAsset, Collider2D, RigidBody2D, ERigidBody2DType, BoxCollider2D, Tween, tween, Vec3, Color } from 'cc';
  2. import { SaveDataManager } from '../LevelSystem/SaveDataManager';
  3. import EventBus, { GameEvents } from '../Core/EventBus';
  4. import { JsonConfigLoader } from '../Core/JsonConfigLoader';
  5. import { SkillManager } from './SkillSelection/SkillManager';
  6. const { ccclass, property } = _decorator;
  7. /**
  8. * 墙体组件
  9. * 负责管理墙体的血量、伤害处理、等级升级等功能
  10. */
  11. @ccclass('Wall')
  12. export class Wall extends Component {
  13. @property({
  14. type: Node,
  15. tooltip: '血量显示节点 (HeartLabel)'
  16. })
  17. public heartLabelNode: Node = null;
  18. // === 私有属性 ===
  19. private currentHealth: number = 100;
  20. private heartLabel: Label = null;
  21. private saveDataManager: SaveDataManager = null;
  22. // 墙体配置数据
  23. private wallConfig: any = null;
  24. private wallHpMap: Record<number, number> = {};
  25. // 动画相关属性
  26. private originalLabelColor: Color = null;
  27. private damageAnimationTween: Tween<Node> = null;
  28. private colorAnimationTween: Tween<Label> = null;
  29. async start() {
  30. await this.initializeWall();
  31. }
  32. /**
  33. * 加载墙体配置
  34. */
  35. private async loadWallConfig(): Promise<void> {
  36. try {
  37. this.wallConfig = await JsonConfigLoader.getInstance().loadConfig('wall');
  38. if (this.wallConfig && this.wallConfig.wallConfig && this.wallConfig.wallConfig.healthByLevel) {
  39. // 转换字符串键为数字键
  40. const healthByLevel = this.wallConfig.wallConfig.healthByLevel;
  41. this.wallHpMap = {};
  42. for (const level in healthByLevel) {
  43. this.wallHpMap[parseInt(level)] = healthByLevel[level];
  44. }
  45. console.log('[Wall] 墙体配置加载成功:', this.wallHpMap);
  46. } else {
  47. console.warn('[Wall] 配置文件格式错误,使用默认配置');
  48. this.useDefaultConfig();
  49. }
  50. } catch (error) {
  51. console.error('[Wall] 加载wall.json时出错:', error);
  52. this.useDefaultConfig();
  53. }
  54. }
  55. /**
  56. * 使用默认配置
  57. */
  58. private useDefaultConfig(): void {
  59. this.wallHpMap = {
  60. 1: 100,
  61. 2: 500,
  62. 3: 1200,
  63. 4: 1500,
  64. 5: 2000
  65. };
  66. }
  67. /**
  68. * 设置墙体碰撞器
  69. * 确保墙体能够与敌人发生碰撞
  70. */
  71. private setupWallCollider(): void {
  72. // 检查是否已有碰撞器组件
  73. let collider = this.node.getComponent(Collider2D);
  74. if (!collider) {
  75. // 添加BoxCollider2D组件
  76. collider = this.node.addComponent(BoxCollider2D);
  77. console.log(`[Wall] 为墙体节点 ${this.node.name} 添加碰撞器组件`);
  78. }
  79. // 确保有RigidBody2D组件
  80. let rigidBody = this.node.getComponent(RigidBody2D);
  81. if (!rigidBody) {
  82. rigidBody = this.node.addComponent(RigidBody2D);
  83. console.log(`[Wall] 为墙体节点 ${this.node.name} 添加刚体组件`);
  84. }
  85. // 设置刚体属性
  86. if (rigidBody) {
  87. rigidBody.type = ERigidBody2DType.Static; // 静态刚体
  88. rigidBody.enabledContactListener = true; // 启用碰撞监听
  89. }
  90. // 设置碰撞器属性
  91. if (collider) {
  92. collider.sensor = false; // 不是传感器,会产生物理碰撞
  93. }
  94. console.log(`[Wall] 墙体 ${this.node.name} 碰撞器设置完成,碰撞器启用: ${collider?.enabled}, 刚体启用: ${rigidBody?.enabled}`);
  95. }
  96. /**
  97. * 初始化墙体
  98. */
  99. private async initializeWall() {
  100. // 初始化存档管理器
  101. this.saveDataManager = SaveDataManager.getInstance();
  102. if (!this.saveDataManager) {
  103. console.error('[Wall] SaveDataManager not found');
  104. return;
  105. }
  106. // 加载墙体配置
  107. await this.loadWallConfig();
  108. // 查找血量显示节点
  109. this.findHeartLabelNode();
  110. // 从存档读取墙体血量
  111. this.loadWallHealthFromSave();
  112. // 初始化血量显示
  113. this.updateHealthDisplay();
  114. // 设置墙体碰撞器
  115. this.setupWallCollider();
  116. // 监听治疗技能变化
  117. this.setupSkillListeners();
  118. // 设置事件监听器
  119. this.setupEventListeners();
  120. }
  121. /**
  122. * 查找血量显示节点
  123. */
  124. private findHeartLabelNode() {
  125. // 查找心血显示节点
  126. if (!this.heartLabelNode) {
  127. this.heartLabelNode = find('Canvas-001/TopArea/HeartNode/HeartLabel') || find('Canvas/GameLevelUI/HeartNode/HeartLabel');
  128. }
  129. if (this.heartLabelNode) {
  130. this.heartLabel = this.heartLabelNode.getComponent(Label);
  131. // 保存原始颜色
  132. if (this.heartLabel && !this.originalLabelColor) {
  133. this.originalLabelColor = this.heartLabel.color.clone();
  134. }
  135. }
  136. }
  137. /**
  138. * 从存档加载墙体血量
  139. */
  140. private loadWallHealthFromSave() {
  141. // 直接从SaveDataManager获取当前墙体血量
  142. this.currentHealth = this.saveDataManager.getWallHealth();
  143. console.log('[Wall] 从配置加载墙体血量:', this.currentHealth);
  144. // 确保当前血量不超过最大血量(考虑技能加成)
  145. const maxHealth = this.getMaxHealth();
  146. if (this.currentHealth > maxHealth) {
  147. this.currentHealth = maxHealth;
  148. }
  149. }
  150. /**
  151. * 墙体受到伤害
  152. */
  153. public takeDamage(damage: number) {
  154. if (damage <= 0) return;
  155. const previousHealth = this.currentHealth;
  156. this.currentHealth = Math.max(0, this.currentHealth - damage);
  157. // 触发受到伤害事件
  158. const eventBus = EventBus.getInstance();
  159. eventBus.emit(GameEvents.WALL_TAKE_DAMAGE, {
  160. damage: damage,
  161. previousHealth: previousHealth,
  162. currentHealth: this.currentHealth
  163. });
  164. // 触发血量变化事件
  165. eventBus.emit(GameEvents.WALL_HEALTH_CHANGED, {
  166. previousHealth: previousHealth,
  167. currentHealth: this.currentHealth,
  168. maxHealth: this.getMaxHealth()
  169. });
  170. // 更新血量显示
  171. this.updateHealthDisplay();
  172. // 播放受伤动画效果
  173. this.playDamageAnimation();
  174. console.log(`[Wall] 墙体受到伤害: ${damage}, 当前血量: ${this.currentHealth}`);
  175. // 检查墙体是否被摧毁
  176. if (this.currentHealth <= 0) {
  177. this.onWallDestroyed();
  178. }
  179. }
  180. /**
  181. * 墙体被摧毁时的处理
  182. * 统一与菜单退出的失败处理流程,直接触发GAME_DEFEAT事件
  183. */
  184. private onWallDestroyed() {
  185. console.log('[Wall] 墙体被摧毁,触发游戏失败');
  186. // 通过事件系统触发墙体被摧毁事件(保留用于其他监听器)
  187. const eventBus = EventBus.getInstance();
  188. eventBus.emit(GameEvents.WALL_DESTROYED, {
  189. finalHealth: this.currentHealth,
  190. maxHealth: this.getMaxHealth()
  191. });
  192. // 统一失败处理:直接触发GAME_DEFEAT事件,与菜单退出处理保持一致
  193. console.log('[Wall] 直接触发GAME_DEFEAT事件,与菜单退出失败处理流程一致');
  194. eventBus.emit(GameEvents.GAME_DEFEAT);
  195. }
  196. /**
  197. * 更新血量显示
  198. */
  199. public updateHealthDisplay() {
  200. if (this.heartLabel) {
  201. this.heartLabel.string = Math.floor(this.currentHealth).toString();
  202. }
  203. }
  204. /**
  205. * 播放受伤动画效果
  206. * 字体变红并伴随缩放动画(先放大后缩回原来的大小)
  207. */
  208. private playDamageAnimation(): void {
  209. if (!this.heartLabelNode || !this.heartLabel) {
  210. return;
  211. }
  212. // 停止之前的动画,防止动画冲突
  213. this.stopDamageAnimation();
  214. // 重置到原始状态
  215. this.heartLabelNode.setScale(Vec3.ONE);
  216. if (this.originalLabelColor) {
  217. this.heartLabel.color = this.originalLabelColor.clone();
  218. }
  219. // 创建缩放动画:放大到1.3倍再缩小回原始大小
  220. this.damageAnimationTween = tween(this.heartLabelNode)
  221. .to(0.15, { scale: new Vec3(1.3, 1.3, 1) }, {
  222. easing: 'sineOut'
  223. })
  224. .to(0.15, { scale: Vec3.ONE }, {
  225. easing: 'sineIn'
  226. })
  227. .call(() => {
  228. this.damageAnimationTween = null;
  229. });
  230. // 创建颜色动画:变红后逐渐恢复原色
  231. if (this.originalLabelColor) {
  232. const redColor = Color.RED.clone();
  233. this.colorAnimationTween = tween(this.heartLabel)
  234. .to(0.1, { color: redColor }, {
  235. easing: 'sineOut'
  236. })
  237. .delay(0.2) // 保持红色一段时间
  238. .to(0.5, { color: this.originalLabelColor }, {
  239. easing: 'sineIn'
  240. })
  241. .call(() => {
  242. this.colorAnimationTween = null;
  243. });
  244. }
  245. // 启动动画
  246. if (this.damageAnimationTween) {
  247. this.damageAnimationTween.start();
  248. }
  249. if (this.colorAnimationTween) {
  250. this.colorAnimationTween.start();
  251. }
  252. console.log('[Wall] 播放受伤动画效果');
  253. }
  254. /**
  255. * 停止受伤动画
  256. */
  257. private stopDamageAnimation(): void {
  258. if (this.damageAnimationTween) {
  259. this.damageAnimationTween.stop();
  260. this.damageAnimationTween = null;
  261. }
  262. if (this.colorAnimationTween) {
  263. this.colorAnimationTween.stop();
  264. this.colorAnimationTween = null;
  265. }
  266. // 恢复原始状态
  267. if (this.heartLabelNode) {
  268. Tween.stopAllByTarget(this.heartLabelNode);
  269. this.heartLabelNode.setScale(Vec3.ONE);
  270. }
  271. if (this.heartLabel && this.originalLabelColor) {
  272. Tween.stopAllByTarget(this.heartLabel);
  273. this.heartLabel.color = this.originalLabelColor.clone();
  274. }
  275. }
  276. /**
  277. * 设置墙体血量
  278. */
  279. public setHealth(health: number) {
  280. const previousHealth = this.currentHealth;
  281. this.currentHealth = Math.max(0, health);
  282. // 如果血量发生变化,触发血量变化事件
  283. if (previousHealth !== this.currentHealth) {
  284. const eventBus = EventBus.getInstance();
  285. eventBus.emit(GameEvents.WALL_HEALTH_CHANGED, {
  286. previousHealth: previousHealth,
  287. currentHealth: this.currentHealth,
  288. maxHealth: this.getMaxHealth()
  289. });
  290. }
  291. this.updateHealthDisplay();
  292. }
  293. /**
  294. * 获取当前墙体血量
  295. */
  296. public getCurrentHealth(): number {
  297. return this.currentHealth;
  298. }
  299. /**
  300. * 获取最大血量(基于当前等级和技能加成)
  301. */
  302. public getMaxHealth(): number {
  303. const currentLevel = this.getCurrentWallLevel();
  304. const baseMaxHealth = this.getWallHealthByLevel(currentLevel);
  305. // 应用治疗技能的最大血量加成
  306. const skillManager = SkillManager.getInstance();
  307. if (skillManager) {
  308. const healSkillLevel = skillManager.getSkillLevel('heal');
  309. const healthBonus = SkillManager.getHealSkillHealthBonus(healSkillLevel);
  310. return Math.floor(baseMaxHealth * (1 + healthBonus));
  311. }
  312. return baseMaxHealth;
  313. }
  314. /**
  315. * 根据等级获取墙体血量
  316. */
  317. public getWallHealthByLevel(level: number): number {
  318. // 使用本地配置
  319. return this.wallHpMap[level] || (100 + (level - 1) * 200);
  320. }
  321. /**
  322. * 获取当前墙壁等级
  323. */
  324. public getCurrentWallLevel(): number {
  325. return this.saveDataManager?.getWallLevel() || 1;
  326. }
  327. /**
  328. * 恢复墙体血量
  329. */
  330. public heal(amount: number) {
  331. const previousHealth = this.currentHealth;
  332. const maxHealth = this.getMaxHealth();
  333. this.currentHealth = Math.min(maxHealth, this.currentHealth + amount);
  334. // 如果血量发生变化,触发血量变化事件
  335. if (previousHealth !== this.currentHealth) {
  336. const eventBus = EventBus.getInstance();
  337. eventBus.emit(GameEvents.WALL_HEALTH_CHANGED, {
  338. previousHealth: previousHealth,
  339. currentHealth: this.currentHealth,
  340. maxHealth: maxHealth,
  341. healAmount: this.currentHealth - previousHealth
  342. });
  343. }
  344. this.updateHealthDisplay();
  345. }
  346. /**
  347. * 重置墙体血量到满血
  348. */
  349. public resetToFullHealth() {
  350. this.currentHealth = this.getMaxHealth();
  351. this.updateHealthDisplay();
  352. }
  353. /**
  354. * 获取血量百分比
  355. */
  356. public getHealthPercentage(): number {
  357. const maxHealth = this.getMaxHealth();
  358. return maxHealth > 0 ? this.currentHealth / maxHealth : 0;
  359. }
  360. /**
  361. * 检查墙体是否存活
  362. */
  363. public isAlive(): boolean {
  364. return this.currentHealth > 0;
  365. }
  366. /**
  367. * 设置事件监听器
  368. */
  369. private setupEventListeners() {
  370. const eventBus = EventBus.getInstance();
  371. // 监听重置墙体血量事件
  372. eventBus.on(GameEvents.RESET_WALL_HEALTH, this.onResetWallHealthEvent, this);
  373. // 监听墙体血量变化事件(用于升级后更新显示)
  374. eventBus.on(GameEvents.WALL_HEALTH_CHANGED, this.onWallHealthChangedEvent, this);
  375. }
  376. /**
  377. * 处理重置墙体血量事件
  378. */
  379. private onResetWallHealthEvent() {
  380. console.log('[Wall] 接收到重置墙体血量事件,重置到满血');
  381. this.resetToFullHealth();
  382. }
  383. /**
  384. * 处理墙体血量变化事件(用于升级后更新)
  385. */
  386. private onWallHealthChangedEvent(eventData?: any) {
  387. // 只有在特定情况下才重新加载存档数据,避免覆盖受伤后的血量
  388. // 如果事件数据包含isUpgrade标志,说明是升级触发的,需要重新加载
  389. if (eventData && eventData.isUpgrade) {
  390. console.log('[Wall] 接收到墙体升级事件,重新加载血量数据');
  391. this.loadWallHealthFromSave();
  392. this.updateHealthDisplay();
  393. }
  394. // 其他情况(如受伤、治疗)不需要重新加载存档,血量已经在相应方法中更新
  395. }
  396. /**
  397. * 设置技能监听器
  398. */
  399. private setupSkillListeners() {
  400. const skillManager = SkillManager.getInstance();
  401. if (skillManager) {
  402. // 监听治疗技能变化
  403. skillManager.onSkillChanged('heal', this.onHealSkillChanged.bind(this));
  404. }
  405. }
  406. /**
  407. * 治疗技能变化回调
  408. */
  409. private onHealSkillChanged(skillId: string, level: number) {
  410. console.log(`[Wall] 治疗技能等级变化: ${level}`);
  411. // 技能升级时,墙体最大血量可能增加,需要更新显示
  412. this.updateHealthDisplay();
  413. // 如果当前血量低于新的最大血量,可以考虑给予一些额外治疗
  414. // 这里暂时不做额外处理,因为SkillSelectionController已经处理了即时治疗
  415. }
  416. /**
  417. * 清理技能监听器
  418. */
  419. private cleanupSkillListeners() {
  420. const skillManager = SkillManager.getInstance();
  421. if (skillManager) {
  422. skillManager.offSkillChanged('heal', this.onHealSkillChanged.bind(this));
  423. }
  424. }
  425. onDestroy() {
  426. // 清理事件监听
  427. const eventBus = EventBus.getInstance();
  428. eventBus.off(GameEvents.RESET_WALL_HEALTH, this.onResetWallHealthEvent, this);
  429. eventBus.off(GameEvents.WALL_HEALTH_CHANGED, this.onWallHealthChangedEvent, this);
  430. this.cleanupSkillListeners();
  431. // 清理动画资源
  432. this.stopDamageAnimation();
  433. }
  434. }