SeasonPassView.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import { _decorator, Button, Color, EditBox, instantiate, Label, Node, NodePool, Prefab, ProgressBar, RichText, Sprite, SpriteFrame, UITransform } from 'cc';
  2. import { UIView } from '@scripts/framework/ui/UIView';
  3. import { AppKernel } from '@scripts/app/AppKernel';
  4. import { UIViewID } from '@scripts/define/UIDefine';
  5. import { UICloseReason } from '@scripts/framework/ui/UILayer';
  6. import { formatCurrency } from '@scripts/utils/Helper';
  7. import { addBtnClick } from '@scripts/utils/UIHelper';
  8. import { RewardItem, RewardType } from './Tasks/RewardItem';
  9. import { NodeItem } from './Prize/NodeItem';
  10. const { ccclass, property } = _decorator;
  11. interface CDTask {
  12. targetTs: number;
  13. label: Label;
  14. format?: (timeStr: string) => string; // 改成接收已经格式化好的时间字符串,可选
  15. timerFunc: () => void;
  16. }
  17. @ccclass('SeasonPassView')
  18. export class SeasonPassView extends UIView {
  19. readonly viewId = UIViewID.SeasonPassView;
  20. @property(Node)
  21. btn_close: Node = null;
  22. @property(Node)
  23. btn_tasks: Node = null;
  24. @property(Node)
  25. btn_prize: Node = null;
  26. @property(Node)
  27. btn_buyPass: Node = null;
  28. @property(Node)
  29. tasks: Node = null;
  30. @property(Node)
  31. prize: Node = null;
  32. @property(Label)
  33. season_end: Label = null;
  34. @property(Label)
  35. tasks_daily_done: Label = null;
  36. @property(Label)
  37. tasks_daily_reset: Label = null;
  38. @property(Node)
  39. tasks_daily_content: Node = null;
  40. @property(Label)
  41. tasks_seasonal_done: Label = null;
  42. @property(Label)
  43. tasks_seasonal_reset: Label = null;
  44. @property(Node)
  45. tasks_seasonal_content: Node = null;
  46. @property(Prefab)
  47. tasks_reward_item: Prefab = null;
  48. @property(Node)
  49. prize_content: Node = null;
  50. @property(Prefab)
  51. prize_node_item: Prefab = null;
  52. @property(Node)
  53. prize_fixed_dlx: Node = null;
  54. @property(Node)
  55. redPoint_tasks: Node = null;
  56. @property(Node)
  57. redPoint_prize: Node = null;
  58. @property(ProgressBar)
  59. progressBar: ProgressBar = null;
  60. @property(Label)
  61. progress_label: Label = null;
  62. @property(Label)
  63. next_need_exp: Label = null;
  64. @property(Node)
  65. scrollView_content: Node = null;
  66. private _seasonPassData: any = null;
  67. private _dailyItemPool: NodePool = new NodePool();
  68. private _dailyUsedItems: Node[] = [];
  69. private _seasonalItemPool: NodePool = new NodePool();
  70. private _seasonalUsedItems: Node[] = [];
  71. private _prizeItemPool: NodePool = new NodePool();
  72. private _prizeUsedItems: Node[] = [];
  73. protected async onBeforeOpen(_params: unknown): Promise<boolean> {
  74. const seasonPassStore = AppKernel.stores.seasonPassStore;
  75. const res = await seasonPassStore.reqSeasonPassInfo();
  76. return res;
  77. }
  78. protected onCreate(): void | Promise<void> {
  79. AppKernel.events.on('seasonPass:update', this.updateSeasonPassView, this);
  80. AppKernel.events.on('checkIn:update', this.handleCheckInUpdate, this);
  81. addBtnClick(this.btn_close, this.onBtnCloseClick, this);
  82. addBtnClick(this.btn_tasks, this.onBtnTasksClick, this);
  83. addBtnClick(this.btn_prize, this.onBtnPrizeClick, this);
  84. addBtnClick(this.btn_buyPass, this.onBtnBuyPassClick, this);
  85. this.initView();
  86. }
  87. // protected onBeforeClose(_reason: UICloseReason): boolean | Promise<boolean> {
  88. // return true;
  89. // }
  90. private handleCheckInUpdate() {
  91. AppKernel.stores.seasonPassStore.reqSeasonPassInfo();
  92. }
  93. private updateSeasonPassView() {
  94. this._seasonPassData = AppKernel.stores.seasonPassStore.seasonPassData;
  95. this.updateTasks();
  96. this.updatePrize();
  97. this.updateRedPoint();
  98. this.updateProgress();
  99. }
  100. private updateTasks() {
  101. const { season_tasks } = this._seasonPassData ?? {};
  102. this.updateDailyTasks();
  103. this.updateSeasonalTasks();
  104. const seasonal_targetTs = season_tasks?.end_at * 1000;
  105. this.startTimer(seasonal_targetTs, this.season_end);
  106. }
  107. private updateDailyTasks() {
  108. const { tasks, task_end_timestamp } = this._seasonPassData ?? {};
  109. // 将当前正在使用的item全部回收进对象池
  110. for (const node of this._dailyUsedItems) {
  111. this._dailyItemPool.put(node);
  112. }
  113. this._dailyUsedItems.length = 0;
  114. for (const task of tasks ?? []) {
  115. let itemNode: Node;
  116. if (this._dailyItemPool.size() > 0) {
  117. itemNode = this._dailyItemPool.get();
  118. } else {
  119. itemNode = instantiate(this.tasks_reward_item);
  120. }
  121. itemNode.active = true;
  122. itemNode.parent = this.tasks_daily_content;
  123. const itemCmp = itemNode.getComponent(RewardItem);
  124. if (itemCmp) itemCmp.initItem(task, RewardType.Daily);
  125. this._dailyUsedItems.push(itemNode);
  126. }
  127. const daily_done = tasks?.filter(task => task.status > 0)?.length ?? 0;
  128. this.tasks_daily_done.string = `${daily_done}/${tasks?.length ?? 0} DONE`;
  129. const daily_targetTs = task_end_timestamp * 1000;
  130. this.startTimer(daily_targetTs, this.tasks_daily_reset, (cd) => "RESET IN " + cd);
  131. }
  132. private updateSeasonalTasks() {
  133. const { season_tasks } = this._seasonPassData ?? {};
  134. for (const node of this._seasonalUsedItems) {
  135. this._seasonalItemPool.put(node);
  136. }
  137. this._seasonalUsedItems.length = 0;
  138. for (const task of season_tasks?.tasks ?? []) {
  139. let itemNode: Node;
  140. if (this._seasonalItemPool.size() > 0) {
  141. itemNode = this._seasonalItemPool.get();
  142. } else {
  143. itemNode = instantiate(this.tasks_reward_item);
  144. }
  145. itemNode.active = true;
  146. itemNode.parent = this.tasks_seasonal_content;
  147. const itemCmp = itemNode.getComponent(RewardItem);
  148. if (itemCmp) itemCmp.initItem(task, RewardType.Seasonal);
  149. this._seasonalUsedItems.push(itemNode);
  150. }
  151. const seasonal_done = season_tasks?.tasks.filter(task => task.status > 0)?.length ?? 0;
  152. this.tasks_seasonal_done.string = `${seasonal_done}/${season_tasks?.tasks.length ?? 0} DONE`;
  153. const seasonal_targetTs = season_tasks?.end_at * 1000;
  154. this.startTimer(seasonal_targetTs, this.tasks_seasonal_reset, (cd) => "RESET IN " + cd);
  155. }
  156. private updatePrize() {
  157. const { pass } = this._seasonPassData ?? {};
  158. const { current_xp, is_deluxe } = pass ?? {};
  159. // 将当前正在使用的item全部回收进对象池
  160. for (const node of this._prizeUsedItems) {
  161. this._prizeItemPool.put(node);
  162. }
  163. this._prizeUsedItems.length = 0;
  164. for (const node of pass?.nodes ?? []) {
  165. let itemNode: Node;
  166. if (this._prizeItemPool.size() > 0) {
  167. itemNode = this._prizeItemPool.get();
  168. } else {
  169. itemNode = instantiate(this.prize_node_item);
  170. }
  171. itemNode.active = true;
  172. itemNode.parent = this.prize_content;
  173. const itemCmp = itemNode.getComponent(NodeItem);
  174. if (itemCmp) itemCmp.initItem({ node, current_xp, is_deluxe });
  175. this._prizeUsedItems.push(itemNode);
  176. }
  177. const activeLabel = this.prize_fixed_dlx.getChildByName('label');
  178. const btn_unlock = this.prize_fixed_dlx.getChildByName('btn_unlock');
  179. const lock = this.prize_fixed_dlx.getChildByName('lock');
  180. activeLabel.active = is_deluxe;
  181. btn_unlock.active = !is_deluxe;
  182. lock.active = !is_deluxe;
  183. }
  184. private updateRedPoint() {
  185. const { tasks, season_tasks, pass } = this._seasonPassData ?? {};
  186. const hasTaskReward = tasks?.some(task => task.status === 1) ?? false;
  187. const hasSeasonalReward = season_tasks?.tasks.some(task => task.status === 1) ?? false;
  188. const { is_deluxe, nodes } = pass ?? {};
  189. const hasFreePrizeReward = nodes?.some(node => {
  190. const unlocked = node.unlocked === 1;
  191. const canClaim = node.free.claimed === 0;
  192. const hasReward = Object.values(node.free.reward)?.some(reward => reward != 0);
  193. return unlocked && canClaim && hasReward;
  194. });
  195. const hasDeluxePrizeReward = nodes?.some(node => {
  196. const unlocked = node.unlocked === 1;
  197. const canClaim = node.deluxe.claimed === 0;
  198. const hasReward = Object.values(node.deluxe.reward)?.some(reward => reward != 0);
  199. return unlocked && canClaim && hasReward && is_deluxe;
  200. })
  201. console.log("hasTaskReward", hasTaskReward, "hasSeasonalReward", hasSeasonalReward, "hasFreePrizeReward", hasFreePrizeReward, "hasDeluxePrizeReward", hasDeluxePrizeReward);
  202. this.redPoint_tasks.active = hasTaskReward || hasSeasonalReward;
  203. this.redPoint_prize.active = hasFreePrizeReward || hasDeluxePrizeReward;
  204. }
  205. private updateProgress() {
  206. const { pass } = this._seasonPassData ?? {};
  207. const { current_xp, nodes } = pass ?? {};
  208. const next_need_exp = nodes?.find(node => node.required_xp > current_xp)?.required_xp;
  209. if (!next_need_exp) {
  210. this.progress_label.string = 'MAX';
  211. this.progressBar.progress = 1;
  212. this.next_need_exp.string = 'MAX LEVEL';
  213. } else {
  214. this.progress_label.string = `${current_xp}/${next_need_exp ?? 0} XP`;
  215. this.progressBar.progress = current_xp / next_need_exp;
  216. this.next_need_exp.string = `${next_need_exp - current_xp} XP TO\nNEXT PRIZE`;
  217. }
  218. }
  219. // 弹窗关闭,回收节点
  220. protected async onAfterClose() {
  221. for (const node of this._dailyUsedItems) {
  222. this._dailyItemPool.put(node);
  223. }
  224. this._dailyUsedItems.length = 0;
  225. for (const node of this._seasonalUsedItems) {
  226. this._seasonalItemPool.put(node);
  227. }
  228. this._seasonalUsedItems.length = 0;
  229. for (const node of this._prizeUsedItems) {
  230. this._prizeItemPool.put(node);
  231. }
  232. this._prizeUsedItems.length = 0;
  233. }
  234. // 弹窗销毁,清空池子释放资源
  235. protected onDestroy(): void {
  236. this._dailyItemPool.clear();
  237. this._seasonalItemPool.clear();
  238. this._prizeItemPool.clear();
  239. this.stopAllTimer();
  240. AppKernel.events.off('seasonPass:update', this.updateSeasonPassView, this);
  241. AppKernel.events.off('checkIn:update', this.handleCheckInUpdate, this);
  242. super.onDestroy();
  243. }
  244. private initView() {
  245. this.onToggleSidebar("tasks");
  246. // const items_data = new Array(10).fill(0);
  247. // items_data.forEach((item, index) => {
  248. // const item_DLX = instantiate(this.item_DLX);
  249. // item_DLX.parent = this.item_DLX.parent;
  250. // item_DLX.active = true;
  251. // const item_free = instantiate(this.item_free);
  252. // item_free.parent = this.item_free.parent;
  253. // item_free.active = true;
  254. // const item_progress = instantiate(this.item_progress);
  255. // item_progress.parent = this.item_progress.parent;
  256. // item_progress.active = true;
  257. // })
  258. // this.scheduleOnce(() => {
  259. // console.log("item_DLX.parent.width", this.item_DLX.parent.getComponent(UITransform).width)
  260. // console.log("item_free.parent.width", this.item_free.parent.getComponent(UITransform).width)
  261. // console.log("item_progress.parent.width", this.item_progress.parent.getComponent(UITransform).width)
  262. // this.scrollView_content.getComponent(UITransform).width = this.item_free.parent.getComponent(UITransform).width;
  263. // })
  264. }
  265. private onBtnCloseClick() {
  266. AppKernel.ui.close(this.viewId);
  267. }
  268. private onToggleSidebar(type: string) {
  269. const selected_tasks = this.btn_tasks.getChildByName('selected');
  270. const unselected_tasks = this.btn_tasks.getChildByName('unselected');
  271. const selected_prize = this.btn_prize.getChildByName('selected');
  272. const unselected_prize = this.btn_prize.getChildByName('unselected');
  273. selected_tasks.active = type === "tasks";
  274. unselected_tasks.active = type !== "tasks";
  275. selected_prize.active = type === "prize";
  276. unselected_prize.active = type !== "prize";
  277. this.tasks.active = type === "tasks";
  278. this.prize.active = type === "prize";
  279. }
  280. private onBtnTasksClick() {
  281. this.onToggleSidebar("tasks");
  282. }
  283. private onBtnPrizeClick() {
  284. this.onToggleSidebar("prize");
  285. }
  286. private onBtnBuyPassClick() {
  287. const { pass } = this._seasonPassData ?? {};
  288. const gear = pass?.purchase_gear;
  289. if (!gear) return;
  290. AppKernel.ui.open(UIViewID.SeasonPassPurchase, gear);
  291. }
  292. private _cdTaskList: CDTask[] = [];
  293. /**
  294. * 开启倒计时
  295. * @param targetTs 结束时间戳(ms)
  296. * @param label 显示文字的Label
  297. * @param format 格式化回调,入参剩余秒数,返回要展示的字符串
  298. */
  299. private startTimer(targetTs: number, label: Label, format?: (timeStr: string) => string) {
  300. if (targetTs == null || isNaN(targetTs)) return;
  301. // 先查找这个label有没有正在跑的倒计时,存在就先停止旧的
  302. const existIdx = this._cdTaskList.findIndex(item => item.label === label);
  303. if (existIdx >= 0) {
  304. const oldTask = this._cdTaskList[existIdx];
  305. this.unschedule(oldTask.timerFunc);
  306. this._cdTaskList.splice(existIdx, 1);
  307. }
  308. // 计算当前剩余秒数
  309. const diff = targetTs - Date.now();
  310. const remainSec = Math.max(0, Math.ceil(diff / 1000));
  311. const timeStr = this.formatHMS(remainSec);
  312. // 有format就调用,没有直接用timeStr
  313. label.string = format ? format(timeStr) : timeStr;
  314. if (remainSec <= 0) {
  315. return;
  316. }
  317. // 新建任务
  318. const task: CDTask = {
  319. targetTs,
  320. label,
  321. format,
  322. timerFunc: () => this.onUpdateCDTask(task)
  323. };
  324. this._cdTaskList.push(task);
  325. this.onUpdateCDTask(task);
  326. this.schedule(task.timerFunc, 1);
  327. }
  328. private stopTimerByLabel(label: Label) {
  329. const idx = this._cdTaskList.findIndex(item => item.label === label);
  330. if (idx >= 0) {
  331. const task = this._cdTaskList[idx];
  332. this.unschedule(task.timerFunc);
  333. this._cdTaskList.splice(idx, 1);
  334. }
  335. }
  336. private stopAllTimer() {
  337. for (const task of this._cdTaskList) {
  338. this.unschedule(task.timerFunc);
  339. }
  340. this._cdTaskList.length = 0;
  341. }
  342. private onUpdateCDTask(task: CDTask) {
  343. const now = Date.now();
  344. const diff = task.targetTs - now;
  345. const remainSec = Math.max(0, Math.ceil(diff / 1000));
  346. const timeStr = this.formatHMS(remainSec);
  347. // 存在format回调就调用,没有直接使用时间字符串
  348. task.label.string = task.format ? task.format(timeStr) : timeStr;
  349. if (remainSec <= 0) {
  350. this.unschedule(task.timerFunc);
  351. const idx = this._cdTaskList.indexOf(task);
  352. if (idx > -1) {
  353. this._cdTaskList.splice(idx, 1);
  354. }
  355. }
  356. }
  357. private formatHMS(totalSec: number): string {
  358. const sec = Math.max(0, Math.floor(totalSec));
  359. const d = Math.floor(sec / (3600 * 24));
  360. const h = Math.floor((sec % (3600 * 24)) / 3600);
  361. const m = Math.floor((sec % 3600) / 60);
  362. const s = sec % 60;
  363. const pad = (n: number) => n.toString().padStart(2, '0');
  364. const hhmmss = `${pad(h)}:${pad(m)}:${pad(s)}`;
  365. if (d > 0) {
  366. return `${d}:${hhmmss}`;
  367. }
  368. return hhmmss;
  369. }
  370. }