MatchMaster/assets/coin_madness/CoinMadnessPanel.ts

127 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import CoinMadnessMockApi from './CoinMadnessMockApi';
const { ccclass } = cc._decorator;
/** All view code stays in this bundle. The resident bridge injects transport. */
@ccclass('CoinMadnessPanel')
export default class CoinMadnessPanel extends cc.Component {
private api: any;
private data: any;
private body: cc.Node;
private countdown: cc.Label;
private title: cc.Label;
private message: cc.Label;
private offset = 0;
private busy = false;
private mock: CoinMadnessMockApi;
private alive = true;
private root: cc.Node;
initialize(data: any, api: any) {
this.api = api;
this.mock = new CoinMadnessMockApi(api);
data = this.mock.request('read').data;
const size = cc.view.getVisibleSize();
this.node.setContentSize(size); this.node.addComponent(cc.BlockInputEvents);
this.box(this.node, 'Dim', 0, 0, size.width, size.height, new cc.Color(0, 0, 0, 175), 0);
this.root = this.box(this.node, 'Card', 0, 0, 660, 1080, new cc.Color(250, 239, 215), 32);
this.root.scale = Math.min((size.width - 36) / 660, (size.height - 90) / 1080, 1.45);
this.box(this.root, 'Header', 0, 360, 660, 360, new cc.Color(233, 126, 29), 32);
this.title = this.label(this.root, 'COIN MADNESS', 0, 474, 46, 500, cc.Color.WHITE);
this.countdown = this.label(this.root, '', 0, 418, 26, 540, new cc.Color(255, 245, 211));
this.button(this.root, '×', 278, 491, 62, 62, () => this.api.close(), new cc.Color(194, 72, 32));
for (let i = 0; i < 7; i++) {
const n = new cc.Node('Coin'); this.root.addChild(n); n.setPosition((i - 3) * 47, 330 + (i % 2) * 16);
const g = n.addComponent(cc.Graphics); g.fillColor = new cc.Color(255, 207, 55);
g.strokeColor = new cc.Color(190, 105, 16); g.lineWidth = 5; g.circle(0, 0, 31); g.fill(); g.stroke();
this.label(n, '$', 0, 0, 36, 56, new cc.Color(192, 110, 22));
}
this.body = new cc.Node('Body'); this.root.addChild(this.body);
this.message = this.label(this.root, '购买后开始计数,每段领取后进入下一段', 0, -495, 22, 600);
this.render(data); this.schedule(this.tick, 1);
}
render(data: any) {
if (!this.alive || !data) return;
this.data = data; this.offset = data.serverNow - Date.now();
this.body.children.slice().forEach(child => child.destroy());
this.body.removeAllChildren();
const state = data.state;
if (!state) { this.label(this.body, '活动暂未开放', 0, 0, 32); return; }
const config = state.config;
this.title.string = config.title || 'COIN MADNESS';
this.label(this.body, '最高可获得 ' + config.stages.reduce((n, s) => n + s.coins, 0) + ' 金币', 0, 240, 31);
const expired = Date.now() + this.offset >= config.endsAt;
let text = state.paidAt ? '已解锁 · 逐段闯关领奖' : '解锁全部任务 ¥' + (config.priceCents / 100).toFixed(2);
if (expired) text = '本期已结束';
if (state.stage >= config.stages.length) text = '全部奖励已领取';
this.button(this.body, text, 0, 166, 570, 80, () => {
if (!state.paidAt && !expired) this.purchase();
}, state.paidAt || expired ? new cc.Color(168, 157, 129) : new cc.Color(74, 162, 60));
const viewport = new cc.Node('Viewport'); this.body.addChild(viewport);
viewport.setPosition(0, -132); viewport.setContentSize(602, 450); viewport.addComponent(cc.Mask);
const content = new cc.Node('Stages'); viewport.addChild(content); content.anchorY = 1;
content.setContentSize(602, Math.max(450, config.stages.length * 112)); content.y = 225;
const scroll = viewport.addComponent(cc.ScrollView); scroll.content = content; scroll.horizontal = false;
config.stages.forEach((stage, index) => {
const complete = index < state.stage;
const current = index === state.stage;
const earned = current && state.paidAt && state.progress >= stage.wins;
const row = this.box(content, 'Stage' + index, 0, -56 - index * 112, 580, 100,
complete ? new cc.Color(225, 231, 207) : cc.Color.WHITE, 14);
this.label(row, '+' + stage.coins, -189, 7, 32, 164, new cc.Color(178, 116, 22));
this.label(row, '完成 ' + stage.wins + ' 关', 1, 22, 25, 208);
this.label(row, complete ? '已完成' : ((current && state.paidAt ? state.progress : 0) + ' / ' + stage.wins), 1, -18, 22, 200);
const status = complete ? '已领取' : expired ? '已结束' : earned ? '领取' : current && state.paidAt ? '进行中' : '未解锁';
this.button(row, status, 199, 0, 142, 62, () => {
if (earned && !expired) this.claim(index);
}, earned && !expired ? new cc.Color(74, 162, 60) : new cc.Color(165, 167, 154));
});
this.label(this.body, '仅模拟 · 测试金币 ' + data.mockBalance + ' · 不会真实扣款或发奖', 0, -375, 20, 620);
['模拟通关', '模拟到期', '重置', '失败一次'].forEach((text, i) =>
this.button(this.body, text, -225 + i * 150, -429, 140, 52,
() => this.run(['simulate_win', 'expire', 'reset', 'fail_next'][i], {}), new cc.Color(97, 132, 158)));
this.tick();
}
private tick() {
if (!this.data?.state) return;
const remaining = Math.max(0, Math.ceil((this.data.state.config.endsAt - Date.now() - this.offset) / 1000));
this.countdown.string = remaining ? Math.floor(remaining / 86400) + '天 ' + Math.floor(remaining / 3600) % 24
+ '时 ' + Math.floor(remaining / 60) % 60 + '分 ' + remaining % 60 + '秒' : '活动已结束';
if (!remaining && !this.busy && !this.data.state.settled) this.run('read', {});
}
private claim(stage: number) { this.run('claim', { periodId: this.data.state.config.periodId, stage }); }
private run(action: string, body: any) {
if (this.busy) return;
this.busy = true; this.message.string = '模拟请求中…';
const result = this.mock.request(action, body);
this.scheduleOnce(() => {
if (!this.alive) return;
this.busy = false;
this.message.string = result.code === 1 ? (action === 'fail_next' ? '下一次请求将模拟失败' : '模拟操作完成,不影响真实资产') : result.msg;
if (result.data) this.render(result.data);
}, 0.2);
}
private purchase() { this.run('purchase', { periodId: this.data.state.config.periodId }); }
private box(parent: cc.Node, name: string, x: number, y: number, w: number, h: number, color: cc.Color, radius: number): cc.Node {
const node = new cc.Node(name); parent.addChild(node); node.setPosition(x, y); node.setContentSize(w, h);
const g = node.addComponent(cc.Graphics); g.fillColor = color;
if (radius) g.roundRect(-w / 2, -h / 2, w, h, radius); else g.rect(-w / 2, -h / 2, w, h);
g.fill(); return node;
}
private label(parent: cc.Node, text: string, x: number, y: number, size: number, width = 560, color = new cc.Color(92, 63, 37)): cc.Label {
const node = new cc.Node('Label'); parent.addChild(node); node.setPosition(x, y); node.setContentSize(width, size * 1.6);
const label = node.addComponent(cc.Label); label.string = text; label.fontSize = size; label.lineHeight = size + 6;
label.useSystemFont = true; label.fontFamily = 'Arial';
label.horizontalAlign = cc.Label.HorizontalAlign.CENTER; label.verticalAlign = cc.Label.VerticalAlign.CENTER;
label.overflow = cc.Label.Overflow.SHRINK;
// Adding an enabled Label initially sizes its empty text to zero; set bounds afterwards.
node.setContentSize(width, size * 1.6); node.color = color; return label;
}
private button(parent: cc.Node, text: string, x: number, y: number, w: number, h: number, action: () => void, color: cc.Color) {
const button = this.box(parent, 'Button', x, y, w, h, color, 14);
this.label(button, text, 0, 0, Math.min(28, h * 0.45), w - 12, cc.Color.WHITE);
button.on(cc.Node.EventType.TOUCH_END, (event: cc.Event.EventTouch) => { event.stopPropagation(); if (!this.busy) action(); });
}
onDestroy() { this.alive = false; this.unscheduleAllCallbacks(); this.api = null; this.mock = null; this.data = null; }
}