SkillNodeGenerator.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  1. import { _decorator, Component, Node, Prefab, resources, instantiate, Label, Sprite, SpriteFrame, Button, ProgressBar, ScrollView, Vec2, Vec3, UITransform, tween, Color } from 'cc';
  2. import { PersistentSkillManager } from './PersistentSkillManager';
  3. import { SaveDataManager } from '../../LevelSystem/SaveDataManager';
  4. import { SkillConfigManager } from './SkillConfigManager';
  5. import EventBus, { GameEvents } from '../../Core/EventBus';
  6. import { Audio } from '../../AudioManager/AudioManager';
  7. const { ccclass, property } = _decorator;
  8. /**
  9. * 技能节点数据接口
  10. */
  11. interface SkillNodeData {
  12. node: Node;
  13. group: number;
  14. skillIndex: number;
  15. cost: number;
  16. isUnlocked: boolean; // 本地记录的解锁状态
  17. backgroundSprite: Sprite;
  18. iconSprite: Sprite;
  19. }
  20. /**
  21. * 技能节点生成器
  22. * 自动生成12组技能节点,每组包含3个技能类型
  23. * 支持技能点亮状态管理和图片切换
  24. */
  25. @ccclass('SkillNodeGenerator')
  26. export class SkillNodeGenerator extends Component {
  27. @property(Prefab)
  28. skillNode1Prefab: Prefab = null!; // Damage
  29. @property(Prefab)
  30. skillNode2Prefab: Prefab = null!; // Ball Speed
  31. @property(Prefab)
  32. skillNode3Prefab: Prefab = null!; // Crit Damage
  33. // 图片资源
  34. @property(SpriteFrame)
  35. backgroundNormal: SpriteFrame = null!; // an001.png - 未点亮状态
  36. @property(SpriteFrame)
  37. backgroundUnlocked: SpriteFrame = null!; // an000.png - 点亮状态
  38. @property(SpriteFrame)
  39. iconNormal: SpriteFrame = null!; // tb002.png - 未点亮状态
  40. @property(SpriteFrame)
  41. iconUnlocked: SpriteFrame = null!; // tb001.png - 点亮状态
  42. @property(ProgressBar)
  43. progressBar: ProgressBar = null!; // 进度条
  44. @property(ScrollView)
  45. scrollView: ScrollView = null!; // 滚动视图
  46. @property(Node)
  47. diamondNumberNode: Node = null!; // 钻石数量显示节点
  48. // 技能节点数据存储(一维数组)
  49. private skillNodes: SkillNodeData[] = [];
  50. // 当前解锁到的节点索引
  51. private currentUnlockIndex: number = -1;
  52. // 持久化技能管理器
  53. private skillManager: PersistentSkillManager | null = null;
  54. // 保存数据管理器引用
  55. private saveDataManager: SaveDataManager | null = null;
  56. // 技能配置管理器
  57. private skillConfigManager: SkillConfigManager | null = null;
  58. // 闪烁动画相关
  59. private blinkingNodes: Set<Node> = new Set(); // 正在闪烁的节点集合
  60. async start() {
  61. console.log('SkillNodeGenerator 初始化开始');
  62. this.skillManager = PersistentSkillManager.getInstance();
  63. this.saveDataManager = SaveDataManager.getInstance();
  64. this.skillConfigManager = SkillConfigManager.getInstance();
  65. // 监听货币变化事件
  66. EventBus.getInstance().on(GameEvents.CURRENCY_CHANGED, this.onCurrencyChanged, this);
  67. // 等待技能配置加载完成
  68. if (this.skillConfigManager) {
  69. await this.skillConfigManager.waitForLoad();
  70. console.log('技能配置加载完成');
  71. }
  72. // 生成所有技能节点
  73. this.generateSkillNodes();
  74. console.log(`技能节点生成完成,共 ${this.skillNodes.length} 个节点`);
  75. // 加载已解锁的技能状态
  76. this.loadUnlockedSkills();
  77. // 更新进度条
  78. this.updateProgressBar();
  79. // 延迟一帧后滚动到最新解锁的技能节点,确保所有节点都已正确布局
  80. this.scheduleOnce(() => {
  81. this.scrollToLatestUnlockedSkill();
  82. }, 0.1);
  83. // 更新钻石UI显示
  84. this.updateDiamondUI();
  85. // 初始化闪烁状态
  86. this.updateBlinkingNodes();
  87. console.log('SkillNodeGenerator 初始化完成');
  88. }
  89. /**
  90. * 生成技能节点
  91. */
  92. private generateSkillNodes() {
  93. if (!this.skillConfigManager) {
  94. console.error('SkillConfigManager not initialized');
  95. return;
  96. }
  97. // 技能节点预制体数组
  98. const prefabs = [
  99. this.skillNode1Prefab, // Damage
  100. this.skillNode2Prefab, // Ball Speed
  101. this.skillNode3Prefab // Crit Damage
  102. ];
  103. const totalGroups = this.skillConfigManager.getTotalGroups();
  104. const skillsPerGroup = this.skillConfigManager.getSkillsPerGroup();
  105. // 生成技能节点,按顺序存储到一维数组中
  106. for (let group = 1; group <= totalGroups; group++) {
  107. const skillGroupConfig = this.skillConfigManager.getSkillGroupConfig(group);
  108. if (!skillGroupConfig) {
  109. console.error(`Failed to get skill group config for group ${group}`);
  110. continue;
  111. }
  112. // 每组生成指定数量的技能节点
  113. for (let skillIndex = 0; skillIndex < skillsPerGroup; skillIndex++) {
  114. // 计算节点在一维数组中的索引
  115. const arrayIndex = (group - 1) * skillsPerGroup + skillIndex;
  116. const skillName = this.skillConfigManager.getSkillName(skillIndex, group);
  117. const skillCost = this.skillConfigManager.getSkillCost(group);
  118. this.createSkillNode(
  119. prefabs[skillIndex],
  120. skillName,
  121. skillCost.toString(),
  122. group,
  123. skillIndex,
  124. arrayIndex
  125. );
  126. }
  127. }
  128. }
  129. /**
  130. * 创建技能节点
  131. */
  132. private createSkillNode(
  133. prefab: Prefab,
  134. skillName: string,
  135. cost: string,
  136. group: number,
  137. skillIndex: number,
  138. totalIndex: number
  139. ) {
  140. if (!prefab) {
  141. console.error(`Prefab is null for skill index: ${skillIndex}`);
  142. return;
  143. }
  144. // 实例化预制体
  145. const skillNode = instantiate(prefab);
  146. // 设置节点名称
  147. skillNode.name = `SkillNode${skillIndex + 1}_Group${group}`;
  148. // 查找并设置NameLabel
  149. const nameLabel = skillNode.getChildByName('NameLabel');
  150. if (nameLabel) {
  151. const labelComponent = nameLabel.getComponent(Label);
  152. if (labelComponent) {
  153. labelComponent.string = skillName;
  154. }
  155. }
  156. // 查找并设置CostLabel
  157. const costLabel = skillNode.getChildByName('CostLabel');
  158. if (costLabel) {
  159. const labelComponent = costLabel.getComponent(Label);
  160. if (labelComponent) {
  161. labelComponent.string = cost;
  162. }
  163. }
  164. // 添加到当前节点(content节点)
  165. this.node.addChild(skillNode);
  166. // 获取Background和Icon的Sprite组件
  167. const backgroundNode = skillNode.getChildByName('Background');
  168. const iconNode = skillNode.getChildByName('Icon');
  169. const backgroundSprite = backgroundNode?.getComponent(Sprite);
  170. const iconSprite = iconNode?.getComponent(Sprite);
  171. if (!backgroundSprite || !iconSprite) {
  172. console.error(`Failed to get sprite components for skill node: ${skillName}`);
  173. return;
  174. }
  175. // 创建技能节点数据
  176. const skillNodeData: SkillNodeData = {
  177. node: skillNode,
  178. group: group,
  179. skillIndex: skillIndex,
  180. cost: parseInt(cost),
  181. isUnlocked: false, // 初始化为未解锁状态
  182. backgroundSprite: backgroundSprite,
  183. iconSprite: iconSprite
  184. };
  185. // 存储技能节点数据
  186. this.skillNodes.push(skillNodeData);
  187. // 添加点击事件
  188. const buttonComponent = skillNode.getChildByName('Background').getComponent(Button);
  189. if (buttonComponent) {
  190. buttonComponent.node.on(Button.EventType.CLICK, () => {
  191. this.onSkillNodeClick(skillNodeData);
  192. }, this);
  193. }
  194. // 初始化为未点亮状态
  195. this.updateSkillNodeVisual(skillNodeData);
  196. }
  197. /**
  198. * 技能节点点击事件处理
  199. */
  200. private onSkillNodeClick(skillNodeData: SkillNodeData) {
  201. // 计算当前节点的索引
  202. const nodeIndex = (skillNodeData.group - 1) * 3 + skillNodeData.skillIndex;
  203. // 只允许按顺序解锁,必须是下一个待解锁的节点
  204. if (nodeIndex !== this.currentUnlockIndex + 1) {
  205. console.log(`必须按顺序解锁技能,当前可解锁索引: ${this.currentUnlockIndex + 1}`);
  206. return;
  207. }
  208. // 如果已经点亮,则不处理
  209. if (skillNodeData.isUnlocked) {
  210. console.log(`技能已点亮: ${this.getSkillTypeString(skillNodeData.skillIndex)} Group ${skillNodeData.group}`);
  211. return;
  212. }
  213. // 获取技能类型和效果百分比
  214. const skillInfo = this.getSkillInfo(skillNodeData);
  215. if (!skillInfo) {
  216. console.error('无法获取技能信息');
  217. return;
  218. }
  219. // 检查玩家是否有足够的钻石
  220. const currentDiamonds = this.saveDataManager ? this.saveDataManager.getDiamonds() : 0;
  221. if (currentDiamonds < skillNodeData.cost) {
  222. console.log(`钻石不足Need: ${skillNodeData.cost}, Have: ${currentDiamonds}`);
  223. // 发送Toast提示事件
  224. EventBus.getInstance().emit(GameEvents.SHOW_TOAST, {
  225. message: `钻石不足,需要${skillNodeData.cost}钻石`,
  226. duration: 2.0
  227. });
  228. return;
  229. }
  230. // 验证是否可以解锁(由 SkillNodeGenerator 负责验证)
  231. // 这里已经通过 nodeIndex === this.currentUnlockIndex + 1 验证了顺序
  232. if (!this.skillManager) {
  233. console.error('SkillManager not initialized');
  234. return;
  235. }
  236. // 将解锁数据传递给 PersistentSkillManager
  237. const unlockResult = this.skillManager.unlockSkill(skillInfo.skillType, skillNodeData.group, skillInfo.effectPercent);
  238. if (!unlockResult.success) {
  239. // 这里主要处理技能类型无效或已解锁的情况
  240. console.log(unlockResult.message);
  241. return;
  242. }
  243. // 扣除钻石
  244. if (this.saveDataManager && !this.saveDataManager.spendDiamonds(skillNodeData.cost)) {
  245. return;
  246. }
  247. // 播放升级成功音效
  248. Audio.playUISound('data/弹球音效/level up 2');
  249. // 更新解锁索引
  250. this.currentUnlockIndex = nodeIndex;
  251. // 更新UI状态
  252. skillNodeData.isUnlocked = true;
  253. this.updateSkillNodeVisual(skillNodeData);
  254. this.updateProgressBar();
  255. this.updateDiamondUI();
  256. // 延迟更新闪烁状态,确保所有状态更新完成
  257. this.scheduleOnce(() => {
  258. this.updateBlinkingNodes();
  259. }, 0.1);
  260. // 输出技能解锁信息,包含数组索引
  261. console.log(`技能已解锁: ${this.getSkillTypeString(skillNodeData.skillIndex)} Group ${skillNodeData.group}, 索引[${nodeIndex}/${this.skillNodes.length-1}]`);
  262. // 输出已解锁技能的数组信息
  263. this.logUnlockedSkills();
  264. // 滚动到最新点亮的技能节点
  265. this.scheduleOnce(() => {
  266. this.scrollToLatestUnlockedSkill();
  267. }, 0.1);
  268. const remainingDiamonds = this.saveDataManager ? this.saveDataManager.getDiamonds() : 0;
  269. console.log(`${unlockResult.message}, Remaining diamonds: ${remainingDiamonds}`);
  270. }
  271. /**
  272. * 更新技能节点的视觉效果
  273. * 使用本地的 isUnlocked 状态来更新节点视觉效果
  274. */
  275. private updateSkillNodeVisual(skillNodeData: SkillNodeData) {
  276. if (skillNodeData.isUnlocked) {
  277. // 点亮状态:使用an000.png和tb001.png
  278. if (this.backgroundUnlocked) {
  279. skillNodeData.backgroundSprite.spriteFrame = this.backgroundUnlocked;
  280. }
  281. if (this.iconUnlocked) {
  282. skillNodeData.iconSprite.spriteFrame = this.iconUnlocked;
  283. }
  284. } else {
  285. // 未点亮状态:使用an001.png和tb002.png
  286. if (this.backgroundNormal) {
  287. skillNodeData.backgroundSprite.spriteFrame = this.backgroundNormal;
  288. }
  289. if (this.iconNormal) {
  290. skillNodeData.iconSprite.spriteFrame = this.iconNormal;
  291. }
  292. }
  293. }
  294. /**
  295. * 获取当前钻石数量
  296. */
  297. public getDiamonds(): number {
  298. return this.saveDataManager ? this.saveDataManager.getDiamonds() : 0;
  299. }
  300. /**
  301. * 设置钻石数量(用于测试或外部系统集成)
  302. */
  303. public setDiamonds(amount: number) {
  304. if (this.saveDataManager) {
  305. const playerData = this.saveDataManager.getPlayerData();
  306. if (playerData) {
  307. playerData.diamonds = amount;
  308. this.saveDataManager.savePlayerData();
  309. this.updateDiamondUI();
  310. // 更新闪烁状态
  311. this.updateBlinkingNodes();
  312. }
  313. }
  314. }
  315. /**
  316. * 更新UI显示的钻石数量
  317. */
  318. private updateDiamondUI() {
  319. // 使用装饰器属性更新主界面的钻石显示
  320. if (this.diamondNumberNode) {
  321. const label = this.diamondNumberNode.getComponent(Label);
  322. if (label && this.saveDataManager) {
  323. const diamonds = this.saveDataManager.getDiamonds();
  324. label.string = diamonds >= 1000000 ? (diamonds / 1e6).toFixed(1) + 'M' :
  325. diamonds >= 1000 ? (diamonds / 1e3).toFixed(1) + 'K' :
  326. diamonds.toString();
  327. }
  328. } else {
  329. console.warn('Diamond number node not assigned in inspector');
  330. }
  331. // 更新闪烁状态
  332. this.updateBlinkingNodes();
  333. }
  334. /**
  335. * 加载已解锁的技能状态
  336. */
  337. private loadUnlockedSkills() {
  338. if (!this.skillManager) {
  339. console.warn('SkillManager not initialized');
  340. return;
  341. }
  342. // 遍历所有技能节点,按顺序检查解锁状态
  343. let lastUnlockedIndex = -1;
  344. for (let i = 0; i < this.skillNodes.length; i++) {
  345. const skillNodeData = this.skillNodes[i];
  346. const skillInfo = this.getSkillInfo(skillNodeData);
  347. if (!skillInfo) continue;
  348. // 检查该技能是否已解锁
  349. const isUnlocked = this.skillManager.isSkillUnlocked(skillInfo.skillType, skillNodeData.group);
  350. if (isUnlocked) {
  351. // 更新本地解锁状态
  352. skillNodeData.isUnlocked = true;
  353. this.updateSkillNodeVisual(skillNodeData);
  354. lastUnlockedIndex = i;
  355. } else {
  356. // 一旦遇到未解锁的技能,就停止检查
  357. skillNodeData.isUnlocked = false;
  358. break;
  359. }
  360. }
  361. // 更新当前解锁索引
  362. this.currentUnlockIndex = lastUnlockedIndex;
  363. // 更新进度条
  364. this.updateProgressBar();
  365. // 输出已解锁技能的数组信息
  366. this.logUnlockedSkills();
  367. }
  368. /**
  369. * 重置所有技能节点状态
  370. */
  371. public resetAllSkills() {
  372. // 通过技能管理器重置技能数据
  373. if (this.skillManager) {
  374. this.skillManager.resetAllSkills();
  375. }
  376. // 重置UI状态和本地解锁状态
  377. this.skillNodes.forEach(skillNodeData => {
  378. // 重置本地解锁状态
  379. skillNodeData.isUnlocked = false;
  380. // 通过 skillManager 重置技能状态
  381. if (this.skillManager) {
  382. const skillInfo = this.getSkillInfo(skillNodeData);
  383. if (skillInfo) {
  384. this.skillManager.unlockSkill(skillInfo.skillType, skillNodeData.group, 0); // 将效果值设为0来重置技能
  385. }
  386. }
  387. this.updateSkillNodeVisual(skillNodeData);
  388. });
  389. // 重置当前解锁索引
  390. this.currentUnlockIndex = -1;
  391. // 更新进度条
  392. this.updateProgressBar();
  393. console.log('All skills have been reset');
  394. }
  395. /**
  396. * 清除技能点亮状态存档(包括持久化数据和UI状态)
  397. */
  398. public clearSkillSaveData() {
  399. // 清除持久化技能数据
  400. if (this.skillManager) {
  401. this.skillManager.resetAllSkills();
  402. }
  403. // 重置UI状态和本地解锁状态
  404. for (const skillNodeData of this.skillNodes) {
  405. // 重置本地解锁状态
  406. skillNodeData.isUnlocked = false;
  407. // 通过 skillManager 重置技能状态
  408. const skillInfo = this.getSkillInfo(skillNodeData);
  409. if (skillInfo && this.skillManager) {
  410. this.skillManager.unlockSkill(skillInfo.skillType, skillNodeData.group, 0);
  411. }
  412. this.updateSkillNodeVisual(skillNodeData);
  413. }
  414. // 重置当前解锁索引
  415. this.currentUnlockIndex = -1;
  416. // 更新进度条
  417. this.updateProgressBar();
  418. // 更新钻石UI显示
  419. this.updateDiamondUI();
  420. }
  421. /**
  422. * 更新进度条
  423. */
  424. private updateProgressBar() {
  425. if (!this.progressBar) {
  426. console.warn('ProgressBar not found');
  427. return;
  428. }
  429. // 使用当前解锁索引计算进度
  430. const totalNodes = this.skillNodes.length;
  431. const currentProgress = totalNodes > 0 ? (this.currentUnlockIndex + 1) / totalNodes : 0;
  432. // 更新进度条
  433. this.progressBar.progress = currentProgress;
  434. // 通知连接线组件更新进度
  435. this.updateConnectionLineProgress(currentProgress);
  436. // 输出当前解锁进度信息
  437. if (this.currentUnlockIndex >= 0) {
  438. console.log(`技能解锁进度: ${this.currentUnlockIndex + 1}/${totalNodes} (${Math.round(currentProgress * 100)}%)`);
  439. } else {
  440. console.log(`技能解锁进度: 0/${totalNodes} (0%)`);
  441. }
  442. }
  443. /**
  444. * 更新连接线进度
  445. */
  446. private updateConnectionLineProgress(progress: number) {
  447. // 查找连接线组件
  448. const connectionLine = this.node.parent?.parent?.getChildByName('ConnectionLine');
  449. if (connectionLine) {
  450. const connectionLineComp = connectionLine.getComponent('SkillConnectionLine') as any;
  451. if (connectionLineComp && typeof connectionLineComp.updateProgress === 'function') {
  452. connectionLineComp.updateProgress(progress);
  453. }
  454. }
  455. }
  456. /**
  457. * 滚动到最新解锁的技能节点
  458. * 使用本地的 isUnlocked 状态来查找最新解锁的技能节点
  459. */
  460. private scrollToLatestUnlockedSkill() {
  461. // 找到最新解锁技能的索引
  462. let latestUnlockedIndex = -1;
  463. for (let i = 0; i < this.skillNodes.length; i++) {
  464. const skillNode = this.skillNodes[i];
  465. // 使用本地的 isUnlocked 状态来判断节点是否解锁
  466. if (skillNode.isUnlocked) {
  467. latestUnlockedIndex = i;
  468. }
  469. }
  470. console.log('滚动到最新的未解锁技能节点或最底部');
  471. // 延迟一帧执行,确保节点布局完成
  472. this.scheduleOnce(() => {
  473. // 使用ScrollView的百分比滚动方法
  474. if (this.scrollView) {
  475. if (latestUnlockedIndex >= 0) {
  476. // 根据最新解锁技能的索引计算滚动比例
  477. const totalNodes = this.skillNodes.length;
  478. const scrollProgress = latestUnlockedIndex / (totalNodes - 1);
  479. // 使用ScrollView的scrollToPercentVertical方法设置滚动位置
  480. // 参数:百分比(0-1), 滚动时间(秒), 是否衰减
  481. this.scrollView.scrollToPercentVertical(scrollProgress, 0.3, true);
  482. console.log(`滚动到索引 ${latestUnlockedIndex}/${totalNodes - 1}, 滚动进度: ${scrollProgress.toFixed(2)}`);
  483. } else {
  484. // 如果没有解锁技能,滚动到顶部
  485. this.scrollView.scrollToPercentVertical(0, 0.3, true);
  486. console.log('滚动到顶部');
  487. }
  488. } else {
  489. console.warn('ScrollView not assigned in inspector');
  490. }
  491. }, 0);
  492. }
  493. /**
  494. * 获取最新解锁的节点位置
  495. * @returns 最新解锁节点的Y坐标位置,如果没有解锁节点则返回0
  496. */
  497. public getLatestUnlockedNodePosition(): number {
  498. // 找到最新点亮的技能节点(如果有)
  499. let latestUnlockedNode: SkillNodeData | null = null;
  500. let maxIndex = -1;
  501. for (const skillNode of this.skillNodes) {
  502. const skillInfo = this.getSkillInfo(skillNode);
  503. if (skillInfo && this.skillManager?.isSkillUnlocked(skillInfo.skillType, skillNode.group)) {
  504. const totalIndex = (skillNode.group - 1) * 3 + skillNode.skillIndex;
  505. if (totalIndex > maxIndex) {
  506. maxIndex = totalIndex;
  507. latestUnlockedNode = skillNode;
  508. }
  509. }
  510. }
  511. if (latestUnlockedNode) {
  512. // 返回节点的Y坐标位置(负值)
  513. return -latestUnlockedNode.node.position.y;
  514. }
  515. return 0;
  516. }
  517. /**
  518. * 获取当前解锁进度
  519. * @returns 解锁进度(0-1之间)
  520. */
  521. public getUnlockProgress(): number {
  522. const unlockedCount = this.skillNodes.filter(node => {
  523. // 使用本地的 isUnlocked 状态来判断节点是否解锁
  524. return node.isUnlocked;
  525. }).length;
  526. const totalNodes = this.skillNodes.length;
  527. return totalNodes > 0 ? unlockedCount / totalNodes : 0;
  528. }
  529. /**
  530. * 输出已解锁技能的数组信息
  531. * 使用本地的 isUnlocked 状态来记录解锁的技能
  532. */
  533. private logUnlockedSkills(): void {
  534. // 创建一个表示所有技能节点状态的数组
  535. const skillStatusArray = this.skillNodes.map((node, index) => {
  536. return {
  537. index,
  538. group: node.group,
  539. skillType: this.getSkillTypeString(node.skillIndex),
  540. isUnlocked: node.isUnlocked // 使用本地的 isUnlocked 状态
  541. };
  542. });
  543. // 输出已解锁的技能节点
  544. console.log('已解锁技能节点:');
  545. const unlockedNodes = skillStatusArray.filter(node => node.isUnlocked);
  546. unlockedNodes.forEach(node => {
  547. console.log(`[${node.index}] ${node.skillType} Group ${node.group}`);
  548. });
  549. // 输出下一个待解锁的节点
  550. const nextNode = skillStatusArray.find(node => !node.isUnlocked);
  551. if (nextNode) {
  552. console.log(`下一个待解锁节点: [${nextNode.index}] ${nextNode.skillType} Group ${nextNode.group}`);
  553. } else {
  554. console.log('所有技能节点已解锁!');
  555. }
  556. // 输出解锁进度
  557. console.log(`解锁进度: ${unlockedNodes.length}/${skillStatusArray.length} (${Math.round(this.getUnlockProgress() * 100)}%)`);
  558. }
  559. /**
  560. * 获取技能信息
  561. * @param skillNodeData 技能节点数据
  562. * @returns 技能信息对象,包含技能类型和效果百分比
  563. */
  564. private getSkillInfo(skillNodeData: SkillNodeData): {skillType: string, effectPercent: number} | null {
  565. if (!this.skillConfigManager) {
  566. console.error('SkillConfigManager not initialized');
  567. return null;
  568. }
  569. // 从配置管理器获取技能类型
  570. const skillType = this.skillConfigManager.getSkillType(skillNodeData.skillIndex);
  571. if (skillType === 'unknown') {
  572. console.error(`未知的技能索引: ${skillNodeData.skillIndex}`);
  573. return null;
  574. }
  575. // 从配置管理器获取效果百分比
  576. const effectPercent = this.skillConfigManager.getSkillEffectPercent(skillNodeData.group);
  577. return {
  578. skillType: skillType,
  579. effectPercent: effectPercent
  580. };
  581. }
  582. /**
  583. * 根据技能索引获取技能类型字符串
  584. * @param skillIndex 技能索引 (0: Damage, 1: Ball Speed, 2: Crit Damage)
  585. * @returns 技能类型字符串
  586. */
  587. private getSkillTypeString(skillIndex: number): string {
  588. if (!this.skillConfigManager) {
  589. return '未知技能';
  590. }
  591. return this.skillConfigManager.getSkillDisplayName(skillIndex);
  592. }
  593. /**
  594. * 更新闪烁节点状态
  595. * 检查哪些节点可以升级并开始闪烁动画
  596. */
  597. private updateBlinkingNodes() {
  598. // 停止所有当前的闪烁动画
  599. this.stopAllBlinking();
  600. // 获取当前钻石数量
  601. const currentDiamonds = this.saveDataManager ? this.saveDataManager.getDiamonds() : 0;
  602. // 找到下一个可解锁的节点
  603. const nextUnlockIndex = this.currentUnlockIndex + 1;
  604. if (nextUnlockIndex < this.skillNodes.length) {
  605. const nextNode = this.skillNodes[nextUnlockIndex];
  606. console.log(`[闪烁检查] 下一个节点 - 组别: ${nextNode.group}, 技能: ${this.getSkillTypeString(nextNode.skillIndex)}, 费用: ${nextNode.cost}, 已解锁: ${nextNode.isUnlocked}, 当前钻石: ${currentDiamonds}`);
  607. // 检查是否有足够钻石且节点未解锁
  608. if (!nextNode.isUnlocked && currentDiamonds >= nextNode.cost) {
  609. this.startBlinking(nextNode.node);
  610. } else {
  611. console.log(`[闪烁检查] 节点不满足闪烁条件`);
  612. }
  613. } else {
  614. console.log(`[闪烁检查] 已达到最后一个节点,无需闪烁`);
  615. }
  616. }
  617. /**
  618. * 开始节点闪烁动画
  619. * @param node 要闪烁的节点
  620. */
  621. private startBlinking(node: Node) {
  622. if (this.blinkingNodes.has(node)) {
  623. return; // 已经在闪烁中
  624. }
  625. this.blinkingNodes.add(node);
  626. // 创建闪烁动画:透明度在0.5和1.0之间循环
  627. const blinkTween = tween(node)
  628. .to(0.5, { scale: new Vec3(1.1, 1.1, 1.1) }, { easing: 'sineInOut' })
  629. .to(0.5, { scale: new Vec3(1.0, 1.0, 1.0) }, { easing: 'sineInOut' })
  630. .union()
  631. .repeatForever();
  632. blinkTween.start();
  633. // 将tween存储到节点上,方便后续停止
  634. node['_blinkTween'] = blinkTween;
  635. }
  636. /**
  637. * 停止节点闪烁动画
  638. * @param node 要停止闪烁的节点
  639. */
  640. private stopBlinking(node: Node) {
  641. if (!this.blinkingNodes.has(node)) {
  642. return; // 没有在闪烁
  643. }
  644. this.blinkingNodes.delete(node);
  645. // 停止tween动画
  646. if (node['_blinkTween']) {
  647. node['_blinkTween'].stop();
  648. node['_blinkTween'] = null;
  649. }
  650. // 重置节点状态
  651. node.setScale(1, 1, 1);
  652. }
  653. /**
  654. * 停止所有闪烁动画
  655. */
  656. private stopAllBlinking() {
  657. for (const node of this.blinkingNodes) {
  658. if (node['_blinkTween']) {
  659. node['_blinkTween'].stop();
  660. node['_blinkTween'] = null;
  661. }
  662. node.setScale(1, 1, 1);
  663. }
  664. this.blinkingNodes.clear();
  665. }
  666. /**
  667. * 当钻石数量变化时调用,更新闪烁状态
  668. */
  669. public onDiamondsChanged() {
  670. this.updateBlinkingNodes();
  671. }
  672. /**
  673. * 货币变化事件处理
  674. */
  675. private onCurrencyChanged() {
  676. console.log('[SkillNodeGenerator] 检测到货币变化,更新钻石UI和闪烁状态');
  677. this.updateDiamondUI();
  678. }
  679. /**
  680. * 组件销毁时清理资源
  681. */
  682. onDestroy() {
  683. // 取消事件监听
  684. EventBus.getInstance().off(GameEvents.CURRENCY_CHANGED, this.onCurrencyChanged, this);
  685. // 停止所有闪烁动画
  686. this.stopAllBlinking();
  687. }
  688. }