Wall.ts 18 KB

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