SkillNodeGenerator.ts 28 KB

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