474 lines
28 KiB
TypeScript
474 lines
28 KiB
TypeScript
import CoinMadnessDigits from './CoinMadnessDigits';
|
|
import BrownLoadingSpinner from '../../Script/BrownLoadingSpinner';
|
|
const { ccclass, property } = cc._decorator;
|
|
|
|
/** Layout and sprite references live in CoinMadness.prefab. This component only binds state. */
|
|
@ccclass
|
|
export default class CoinMadnessPanel extends cc.Component {
|
|
@property(cc.Node) card: cc.Node = null;
|
|
@property(cc.Node) dim: cc.Node = null;
|
|
@property(CoinMadnessDigits) countdown: CoinMadnessDigits = null;
|
|
@property(cc.SpriteFrame) flyingCoin: cc.SpriteFrame = null;
|
|
@property(CoinMadnessDigits) totalDigits: CoinMadnessDigits = null;
|
|
@property(cc.Node) lockedOffer: cc.Node = null;
|
|
@property(cc.Node) unlockedOffer: cc.Node = null;
|
|
@property(cc.Node) unlockedTotalPosition: cc.Node = null;
|
|
@property(cc.Node) purchaseButton: cc.Node = null;
|
|
@property(cc.Label) purchaseLabel: cc.Label = null;
|
|
@property(CoinMadnessDigits) priceDigits: CoinMadnessDigits = null;
|
|
@property(cc.Node) currency: cc.Node = null;
|
|
@property(cc.Node) content: cc.Node = null;
|
|
@property(cc.Node) closeButton: cc.Node = null;
|
|
@property(cc.Node) helpButton: cc.Node = null;
|
|
@property(cc.Label) message: cc.Label = null;
|
|
@property(cc.Node) debugTools: cc.Node = null;
|
|
@property(cc.SpriteFrame) lockedFrame: cc.SpriteFrame = null;
|
|
@property(cc.SpriteFrame) unlockedFrame: cc.SpriteFrame = null;
|
|
@property(CoinMadnessDigits) discountDigits: CoinMadnessDigits = null;
|
|
@property({ tooltip: '折扣数字,例如 4 表示4折' }) discount = 4;
|
|
private api: any;
|
|
private data: any;
|
|
private offset = 0;
|
|
private busy = false;
|
|
private alive = true;
|
|
private closing = false;
|
|
private purchasing = false;
|
|
private cardScale = 1;
|
|
private boundRows: cc.Node[] = [];
|
|
private rowPitch = 166;
|
|
private authoredRowCount = 0;
|
|
private authoredContentHeight = 0;
|
|
private lockedTotalPosition: cc.Vec3 = null;
|
|
private rewardAnimations: cc.Node[] = [];
|
|
private rewardRowScales: Map<cc.Node, number> = new Map();
|
|
private claimWaiters: Map<number, cc.Node> = new Map();
|
|
private displayedClaims: Set<number> = new Set();
|
|
private progressKey = '';
|
|
private shownWins = 0;
|
|
private fromWins = 0;
|
|
private targetWins = 0;
|
|
private progressElapsed = 0;
|
|
private progressDuration = 1;
|
|
private progressSteps: { goal: number; start: number; end: number; begin: number; duration: number }[] = [];
|
|
private storedWins = -1;
|
|
private readyRows: Map<cc.Node, boolean> = new Map();
|
|
private pendingReadyRows: Set<cc.Node> = new Set();
|
|
private readyScales: Map<cc.Node, cc.Vec2> = new Map();
|
|
private focusedProgressIndex = -1;
|
|
private progressFollowStopped = false;
|
|
|
|
initialize(data: any, api: any) {
|
|
this.api = api;
|
|
const size = cc.view.getVisibleSize(); this.node.setContentSize(size); this.dim.setContentSize(size);
|
|
this.card.scale = 0.9 * Math.min((size.width - 96) / this.card.width, (size.height - 96) / this.card.height);
|
|
this.cardScale = this.card.scale;
|
|
this.authoredRowCount = this.content.children.length;
|
|
this.authoredContentHeight = this.content.height;
|
|
this.content.parent.on(cc.Node.EventType.TOUCH_START, this.stopProgressFollow, this);
|
|
if (this.content.children.length > 1) this.rowPitch = this.content.children[0].y - this.content.children[1].y;
|
|
this.closeButton.on(cc.Node.EventType.TOUCH_END, (event: cc.Event.EventTouch) => { event.stopPropagation(); this.closeWithAnimation(); });
|
|
this.helpButton.on(cc.Node.EventType.TOUCH_END, (event: cc.Event.EventTouch) => { event.stopPropagation(); if (!this.closing) this.api.openHelp(); });
|
|
this.purchaseButton.on(cc.Node.EventType.TOUCH_END, () => {
|
|
if (this.busy || this.closing || this.purchasing || !this.data || this.data.state.paidAt
|
|
|| (this.api.request && this.data.status !== 'purchasable')
|
|
|| Date.now() + this.offset >= this.data.state.config.endsAt) return;
|
|
this.purchasing = true;
|
|
const scale = this.purchaseButton.scale;
|
|
cc.tween(this.purchaseButton).to(.09, { scale: scale * .94 }, { easing: 'quadOut' })
|
|
.to(.12, { scale: scale * 1.045 }, { easing: 'sineOut' })
|
|
.to(.10, { scale }, { easing: 'sineInOut' })
|
|
.call(() => {
|
|
this.purchasing = false;
|
|
if (this.alive && !this.closing) this.run('purchase', { periodId: this.data.state.config.periodId });
|
|
}).start();
|
|
});
|
|
this.debugTools.active = false;
|
|
this.purchaseButton.active = false; this.content.children.forEach(row => row.active = false);
|
|
const rules = this.card.getComponentsInChildren(cc.Label).find(label => label.node.name === 'Rules');
|
|
if (rules) rules.node.active = false;
|
|
this.message.node.active = false;
|
|
if (data) this.render(data);
|
|
if (!api.initialReady) this.run('read', {});
|
|
this.schedule(this.tick, 1);
|
|
this.playOpenAnimation();
|
|
}
|
|
private playOpenAnimation() {
|
|
const scale = this.card.scale;
|
|
cc.Tween.stopAllByTarget(this.card);
|
|
this.card.scale = scale * .86;
|
|
cc.tween(this.card).to(.20, { scale: scale * 1.035 }, { easing: 'quadOut' })
|
|
.to(.12, { scale }, { easing: 'sineOut' }).start();
|
|
}
|
|
private closeWithAnimation() {
|
|
if (this.closing || !this.alive) return;
|
|
this.closing = true;
|
|
cc.Tween.stopAllByTarget(this.card);
|
|
cc.tween(this.card).to(.15, { scale: this.cardScale * .90 }, { easing: 'sineIn' })
|
|
.call(() => { if (this.alive && this.api) this.api.close(); }).start();
|
|
}
|
|
render(data: any) {
|
|
if (!this.alive || this.closing || !data) return;
|
|
const unlocking = !!this.data && !this.data.state.paidAt && !!data.state.paidAt;
|
|
if (this.data && this.data.state.config.periodId !== data.state.config.periodId) {
|
|
this.readyScales.forEach((_, row) => this.stopReadyPulse(row));
|
|
this.claimWaiters.forEach((_, stage) => this.stopClaimWaiting(stage));
|
|
this.displayedClaims.clear();
|
|
}
|
|
this.data = data; this.offset = data.serverNow - Date.now();
|
|
if (data.state.settled) {
|
|
this.api.openSettlement(data);
|
|
return;
|
|
}
|
|
const state = data.state, config = state.config;
|
|
this.syncProgress(state);
|
|
const expired = Date.now() + this.offset >= config.endsAt;
|
|
if (this.api.request && (expired || data.status === 'unavailable' || data.status === 'locked')) {
|
|
this.finishExpired();
|
|
return;
|
|
}
|
|
if (!this.lockedTotalPosition) this.lockedTotalPosition = this.totalDigits.node.position.clone();
|
|
const unlocked = !!state.paidAt;
|
|
this.lockedOffer.active = !unlocked;
|
|
this.unlockedOffer.active = unlocked;
|
|
this.purchaseButton.active = !unlocked && (!this.api.request || data.status === 'purchasable');
|
|
this.totalDigits.node.position = unlocked ? this.unlockedTotalPosition.position : this.lockedTotalPosition;
|
|
this.totalDigits.setText(String(config.stages.reduce((sum, s) => sum + s.coins, 0)), 164);
|
|
this.purchaseLabel.string = expired ? '已结束' : state.stage >= config.stages.length ? '已全部领取' : state.paidAt ? '已解锁' : '';
|
|
const canBuy = !state.paidAt && !expired && (!this.api.request || data.status === 'purchasable');
|
|
this.purchaseButton.color = canBuy ? cc.Color.WHITE : new cc.Color(200, 195, 180);
|
|
this.priceDigits.node.active = this.currency.active = canBuy;
|
|
if (this.discountDigits) this.discountDigits.setText(String(this.discount));
|
|
if (Number.isFinite(config.priceCents)) {
|
|
const currencyWidth = this.currency.width * Math.abs(this.currency.scaleX), gap = 12;
|
|
const width = this.priceDigits.setText(String(Math.floor(config.priceCents / 100)),
|
|
Math.max(1, this.purchaseButton.width - 64 - currencyWidth - gap));
|
|
// Center the price and currency together instead of keeping “元” at a fixed x.
|
|
this.priceDigits.node.x = -(currencyWidth + gap) / 2;
|
|
this.currency.x = (width + gap) / 2;
|
|
}
|
|
// The first complete prefab row is the template for additional configured stages.
|
|
while (this.content.children.length < config.stages.length) {
|
|
const row = cc.instantiate(this.content.children[0]); this.content.addChild(row);
|
|
row.y = this.content.children[0].y - (this.content.children.length - 1) * this.rowPitch;
|
|
}
|
|
// Preserve the authored scroll area; only extend it for additional server stages.
|
|
this.content.height = this.authoredContentHeight
|
|
+ Math.max(0, config.stages.length - this.authoredRowCount) * this.rowPitch;
|
|
this.content.children.forEach((row, i) => {
|
|
row.active = i < config.stages.length; if (!row.active) return;
|
|
row.name = 'Stage' + i;
|
|
if (this.boundRows.indexOf(row) < 0) {
|
|
this.boundRows.push(row);
|
|
row.on(cc.Node.EventType.TOUCH_END, (event: cc.Event.EventTouch) => {
|
|
const current = this.data.state, target = current.config.stages[i];
|
|
if (this.taskProgress(target.wins) < target.wins && current.totalWins >= target.wins) return;
|
|
if (this.pendingReadyRows.has(row)) return;
|
|
if (this.claimWaiters.has(i) || this.displayedClaims.has(i)) return;
|
|
if (!this.closing && !this.busy && !current.claimed[i] && !current.paidAt) {
|
|
event.stopPropagation(); this.showLockedToast(); return;
|
|
}
|
|
if (!this.closing && !this.busy && !current.claimed[i] && current.totalWins < target.wins) {
|
|
event.stopPropagation(); this.api.toast('请通过更多的关卡以解锁金币奖励!'); return;
|
|
}
|
|
if (!this.closing && !current.claimed[i] && current.totalWins >= target.wins && !this.busy && Date.now() + this.offset < current.config.endsAt) {
|
|
event.stopPropagation();
|
|
this.run('claim', { periodId: current.config.periodId, stage: i });
|
|
}
|
|
});
|
|
}
|
|
const stage = config.stages[i], complete = this.displayedClaims.has(i)
|
|
|| (!this.claimWaiters.has(i) && !!state.claimed[i]);
|
|
const child = (name: string) => row.getChildByName(name);
|
|
const lock = child('RewardLock');
|
|
if (unlocking && !complete) {
|
|
const position = lock.position.clone(), scaleX = lock.scaleX, scaleY = lock.scaleY, angle = lock.angle;
|
|
lock.active = true; lock.opacity = 255;
|
|
lock.getComponent(cc.Sprite).spriteFrame = this.unlockedFrame;
|
|
cc.Tween.stopAllByTarget(lock);
|
|
cc.tween(lock).to(.16, { scaleX: scaleX * 1.25, scaleY: scaleY * 1.25, angle: angle - 12 }).to(.16, { angle: angle + 8 })
|
|
.to(.25, { y: position.y + 30, opacity: 0 }).call(() => {
|
|
if (!cc.isValid(lock)) return;
|
|
lock.active = false; lock.position = position; lock.scaleX = scaleX; lock.scaleY = scaleY; lock.angle = angle; lock.opacity = 255;
|
|
}).start();
|
|
} else if (!unlocked) {
|
|
lock.active = !complete; lock.opacity = 255;
|
|
lock.getComponent(cc.Sprite).spriteFrame = this.lockedFrame;
|
|
} else if (!lock.getNumberOfRunningActions()) lock.active = false;
|
|
this.paintProgress(row, i, expired);
|
|
child('Reward').getComponent(CoinMadnessDigits).setText(String(stage.coins), 170);
|
|
['Task', 'CompletedTask'].forEach(name => child(name).getChildByName('Target').getComponent(CoinMadnessDigits).setText(String(stage.wins)));
|
|
});
|
|
this.focusGrowingTask();
|
|
this.tick();
|
|
}
|
|
private focusGrowingTask() {
|
|
if (this.progressFollowStopped) return;
|
|
const state = this.data.state;
|
|
const index = state.config.stages.findIndex((stage, i) => !state.claimed[i]
|
|
&& Math.min(this.targetWins, stage.wins) > this.taskProgress(stage.wins));
|
|
const row = this.content.children[index], viewport = this.content.parent;
|
|
const scroll = viewport.getComponent(cc.ScrollView);
|
|
if (!scroll) return;
|
|
const first = this.focusedProgressIndex < 0;
|
|
// Rewards already available at the saved display progress take priority on opening.
|
|
const firstOldReward = first ? state.config.stages.findIndex((stage, i) => !state.claimed[i]
|
|
&& !this.displayedClaims.has(i) && stage.wins <= this.shownWins) : -1;
|
|
if (firstOldReward >= 0) {
|
|
const offset = this.content.children[0].y - this.content.children[firstOldReward].y;
|
|
this.followOffset = Math.max(0, Math.min(offset, this.content.height - viewport.height));
|
|
this.focusedProgressIndex = firstOldReward;
|
|
scroll.stopAutoScroll();
|
|
scroll.scrollToOffset(cc.v2(0, this.followOffset), 0);
|
|
this.progressFollowStopped = true;
|
|
return;
|
|
}
|
|
// Keep the last available reward visible at the first row's authored position.
|
|
let lastClaimable = -1;
|
|
state.config.stages.forEach((stage, i) => {
|
|
if (!state.claimed[i] && !this.displayedClaims.has(i) && stage.wins <= this.targetWins) lastClaimable = i;
|
|
});
|
|
const topRow = this.content.children[0];
|
|
const followLimit = lastClaimable >= 0
|
|
? Math.max(0, topRow.y - this.content.children[lastClaimable].y) : 0;
|
|
// After replaying new progress, settle on the last available reward.
|
|
const finished = this.targetWins <= this.shownWins || !row;
|
|
const offset = finished ? followLimit : -row.y - viewport.height / 2;
|
|
const max = Math.min(followLimit, Math.max(0, this.content.height - viewport.height));
|
|
const destination = Math.max(0, Math.min(max, offset));
|
|
if (this.followOffset === destination) return;
|
|
this.followOffset = destination;
|
|
this.focusedProgressIndex = finished ? state.config.stages.length : index;
|
|
scroll.stopAutoScroll();
|
|
scroll.scrollToOffset(cc.v2(0, destination), first ? 0 : .18);
|
|
}
|
|
private followOffset = -1;
|
|
private stopProgressFollow() {
|
|
this.progressFollowStopped = true;
|
|
const scroll = this.content.parent.getComponent(cc.ScrollView);
|
|
if (scroll) scroll.stopAutoScroll();
|
|
}
|
|
private syncProgress(state: any) {
|
|
const key = 'goldMiner:shownProgress:' + (this.api.progressScope || 'preview') + ':' + state.config.periodId;
|
|
const target = Math.max(0, Number(state.totalWins) || 0);
|
|
if (key !== this.progressKey) {
|
|
this.progressKey = key;
|
|
this.focusedProgressIndex = -1;
|
|
this.followOffset = -1;
|
|
this.readyRows.clear();
|
|
const raw = cc.sys.localStorage.getItem(key), saved = Number(raw);
|
|
this.shownWins = raw !== null && raw !== '' && Number.isFinite(saved) && saved >= 0 ? Math.min(saved, target) : target;
|
|
this.targetWins = -1;
|
|
this.storedWins = -1;
|
|
this.progressSteps = [];
|
|
}
|
|
if (target === this.targetWins) return;
|
|
const previous = this.progressSteps.map(step => ({goal: step.goal, value: this.taskProgress(step.goal)}));
|
|
this.shownWins = Math.min(this.shownWins, target);
|
|
this.fromWins = this.shownWins; this.targetWins = target; this.progressElapsed = 0;
|
|
this.progressSteps = [];
|
|
this.progressDuration = 0;
|
|
state.config.stages.forEach((stage, i) => {
|
|
const old = previous.find(step => step.goal === stage.wins);
|
|
const end = Math.min(target, stage.wins);
|
|
const start = Math.min(old ? old.value : this.fromWins, end);
|
|
const duration = state.claimed[i] || stage.wins <= 0 ? 0 : .64 * Math.max(0, end - start) / stage.wins;
|
|
this.progressSteps.push({goal: stage.wins, start: state.claimed[i] ? end : start,
|
|
end, begin: this.progressDuration, duration});
|
|
this.progressDuration += duration;
|
|
});
|
|
this.progressDuration = Math.max(.001, this.progressDuration);
|
|
if (this.storedWins < 0) this.saveShownProgress();
|
|
}
|
|
private saveShownProgress() {
|
|
const value = Math.floor(this.shownWins);
|
|
if (value === this.storedWins) return;
|
|
cc.sys.localStorage.setItem(this.progressKey, String(value)); this.storedWins = value;
|
|
}
|
|
update(dt: number) {
|
|
if (!this.alive || this.closing || !this.data || this.shownWins >= this.targetWins) return;
|
|
this.progressElapsed += dt;
|
|
// Hold the last displayed progress for 0.4 seconds before increasing it.
|
|
const t = Math.min(1, Math.max(0, (this.progressElapsed - .4) / this.progressDuration));
|
|
this.shownWins = t === 1 ? this.targetWins : this.fromWins + (this.targetWins - this.fromWins) * t;
|
|
// Persist the shared baseline only after every task has shown its new progress.
|
|
if (t === 1) this.saveShownProgress();
|
|
const expired = Date.now() + this.offset >= this.data.state.config.endsAt;
|
|
this.focusGrowingTask();
|
|
this.content.children.forEach((row, i) => { if (row.active) this.paintProgress(row, i, expired); });
|
|
}
|
|
private paintProgress(row: cc.Node, i: number, expired: boolean) {
|
|
const state = this.data.state, stage = state.config.stages[i];
|
|
const complete = this.displayedClaims.has(i) || (!this.claimWaiters.has(i) && !!state.claimed[i]);
|
|
const earned = !expired && !complete && this.taskProgress(stage.wins) >= stage.wins;
|
|
const newlyEarned = earned && this.readyRows.get(row) === false && !this.claimWaiters.has(i);
|
|
this.readyRows.set(row, earned);
|
|
if (!earned) this.stopReadyPulse(row);
|
|
if (newlyEarned) this.playReadyPulse(row, () => this.paintProgress(row, i, expired));
|
|
const showReady = earned && !this.pendingReadyRows.has(row);
|
|
const child = (name: string) => row.getChildByName(name);
|
|
child('Normal').active = !showReady; child('Ready').active = showReady;
|
|
child('Check').active = complete; child('Coins').active = child('Reward').active = !complete;
|
|
const claim = child('Claim'); claim.active = showReady && !this.claimWaiters.has(i);
|
|
child('Task').active = !complete && !showReady; child('CompletedTask').active = complete;
|
|
const progress = child('Progress'); progress.active = !complete && !showReady;
|
|
const amount = this.taskProgress(stage.wins);
|
|
const fill = progress.getChildByName('Fill'); fill.active = amount > 0;
|
|
fill.getComponent(cc.Sprite).fillRange = stage.wins > 0 ? Math.min(1, amount / stage.wins) : 1;
|
|
progress.getChildByName('Expired').active = expired;
|
|
const digits = progress.getChildByName('Digits'); digits.active = !expired;
|
|
digits.getComponent(CoinMadnessDigits).setText(Math.floor(amount) + '/' + stage.wins);
|
|
}
|
|
private taskProgress(goal: number): number {
|
|
const step = this.progressSteps.find(item => item.goal === goal);
|
|
if (!step) return Math.min(this.shownWins, goal);
|
|
if (!step.duration) return step.end;
|
|
const t = Math.max(0, Math.min(1, (this.progressElapsed - .4 - step.begin) / step.duration));
|
|
return step.start + (step.end - step.start) * t;
|
|
}
|
|
private rowVisible(row: cc.Node): boolean {
|
|
const viewport = this.content.parent;
|
|
const p = viewport.convertToNodeSpaceAR(row.convertToWorldSpaceAR(cc.Vec2.ZERO));
|
|
const half = row.height * Math.abs(row.scaleY) * 1.08 / 2;
|
|
return p.y - half >= -viewport.height * viewport.anchorY
|
|
&& p.y + half <= viewport.height * (1 - viewport.anchorY);
|
|
}
|
|
private stopReadyPulse(row: cc.Node) {
|
|
this.pendingReadyRows.delete(row);
|
|
const base = this.readyScales.get(row);
|
|
if (!base) return;
|
|
cc.Tween.stopAllByTarget(row);
|
|
if (cc.isValid(row)) { row.scaleX = base.x; row.scaleY = base.y; }
|
|
this.readyScales.delete(row);
|
|
}
|
|
private playReadyPulse(row: cc.Node, reveal: () => void) {
|
|
this.stopReadyPulse(row);
|
|
if (!this.rowVisible(row)) return;
|
|
const base = cc.v2(row.scaleX, row.scaleY);
|
|
this.readyScales.set(row, base);
|
|
this.pendingReadyRows.add(row);
|
|
const size = (factor: number) => ({ scaleX: base.x * factor, scaleY: base.y * factor });
|
|
cc.tween(row).delay(.16).to(.12, size(1.08), { easing: 'sineOut' })
|
|
.to(.096, size(.1), { easing: 'sineInOut' })
|
|
.call(() => { this.pendingReadyRows.delete(row); reveal(); })
|
|
.to(.064, size(1.08), { easing: 'quadOut' })
|
|
.to(.112, size(1), { easing: 'sineInOut' })
|
|
.call(() => { this.readyScales.delete(row); }).start();
|
|
}
|
|
private showLockedToast() {
|
|
this.api.toast('购买礼包以解锁奖励');
|
|
}
|
|
private startClaimWaiting(stage: number) {
|
|
const row = this.content.children[stage];
|
|
if (!row || this.claimWaiters.has(stage)) return;
|
|
const claim = row.getChildByName('Claim');
|
|
const spinner = new cc.Node('ClaimWaiting'); row.addChild(spinner);
|
|
spinner.setPosition(claim.position);
|
|
spinner.addComponent(BrownLoadingSpinner);
|
|
this.claimWaiters.set(stage, spinner);
|
|
claim.active = false;
|
|
}
|
|
private stopClaimWaiting(stage: number) {
|
|
const spinner = this.claimWaiters.get(stage);
|
|
if (!spinner) return;
|
|
cc.Tween.stopAllByTarget(spinner);
|
|
if (cc.isValid(spinner)) { spinner.active = false; spinner.destroy(); }
|
|
this.claimWaiters.delete(stage);
|
|
}
|
|
private clearRewardAnimation(layer: cc.Node) {
|
|
if (!layer) return;
|
|
(layer.children || []).forEach(node => cc.Tween.stopAllByTarget(node));
|
|
cc.Tween.stopAllByTarget(layer);
|
|
if (cc.isValid(layer)) layer.destroy();
|
|
const index = this.rewardAnimations.indexOf(layer);
|
|
if (index >= 0) this.rewardAnimations[index] = null;
|
|
}
|
|
rewardSaved(stageIndex: number) {
|
|
this.stopClaimWaiting(stageIndex);
|
|
if (this.alive && !this.closing) {
|
|
this.displayedClaims.add(stageIndex);
|
|
this.render(this.data);
|
|
this.showRewardAnimation(stageIndex);
|
|
}
|
|
else if (this.api) this.api.rewardArrived();
|
|
}
|
|
private showRewardAnimation(stageIndex: number) {
|
|
const row = this.content.children[stageIndex];
|
|
if (!row) { this.api.rewardArrived(); return; }
|
|
this.stopReadyPulse(row);
|
|
const baseScale = this.rewardRowScales.has(row) ? this.rewardRowScales.get(row) : row.scale;
|
|
this.rewardRowScales.set(row, baseScale);
|
|
cc.Tween.stopAllByTarget(row);
|
|
row.scale = baseScale;
|
|
cc.tween(row).to(.12, { scale: baseScale * 1.035 }, { easing: 'quadOut' })
|
|
.to(.10, { scale: baseScale * .985 }, { easing: 'sineInOut' })
|
|
.to(.12, { scale: baseScale }, { easing: 'sineOut' })
|
|
.call(() => { this.rewardRowScales.delete(row); }).start();
|
|
const source = row.getChildByName('Coins');
|
|
const origin = this.node.convertToNodeSpaceAR(source.convertToWorldSpaceAR(cc.Vec2.ZERO));
|
|
let slot = this.rewardAnimations.indexOf(null);
|
|
if (slot < 0) slot = this.rewardAnimations.length;
|
|
const coinTarget = this.api.coinTarget && this.api.coinTarget();
|
|
const destination = coinTarget && cc.isValid(coinTarget) && coinTarget.activeInHierarchy
|
|
? this.node.convertToNodeSpaceAR(coinTarget.convertToWorldSpaceAR(cc.Vec2.ZERO))
|
|
: cc.v2(0, this.node.height / 2 - 100);
|
|
const layer = new cc.Node('RewardClaimAnimation');
|
|
this.rewardAnimations[slot] = layer;
|
|
this.node.addChild(layer); layer.zIndex = cc.macro.MAX_ZINDEX;
|
|
for (let i = 0; i < 16; i++) {
|
|
const coin = new cc.Node('FlyingCoin' + i); layer.addChild(coin);
|
|
coin.addComponent(cc.Sprite).spriteFrame = this.flyingCoin;
|
|
coin.setPosition(origin.x + (Math.random() - .5) * 120 + 50, origin.y + (Math.random() - .5) * 70 + 140); coin.scale = 0; coin.opacity = 255;
|
|
cc.tween(coin).delay(i * .025)
|
|
.to(.06, { scale: .75 }, { easing: 'backOut' })
|
|
.to(.175, { x: coin.x + (Math.random() - .5) * 180, y: coin.y + 80 + Math.random() * 80 }, { easing: 'sineOut' })
|
|
.to(.325, { x: destination.x, y: destination.y, scale: .35 }, { easing: 'sineIn' })
|
|
.call(() => { if (cc.isValid(coin)) coin.destroy(); }).start();
|
|
}
|
|
cc.tween(layer).delay(.935).call(() => {
|
|
if (this.alive && this.api) this.api.rewardArrived();
|
|
}).delay(.24).call(() => {
|
|
this.clearRewardAnimation(layer);
|
|
}).start();
|
|
}
|
|
private finishExpired() {
|
|
if (this.closing || !this.alive) return;
|
|
this.closing = true;
|
|
// Defer until initialization/render completes before replacing or releasing the prefab.
|
|
this.scheduleOnce(() => { if (this.alive && this.api) this.api.expired(); }, 0);
|
|
}
|
|
private tick() {
|
|
if (!this.data) return;
|
|
const remaining = Math.max(0, Math.ceil(((this.data.state.config.endsAt || 0) - Date.now() - this.offset) / 1000));
|
|
const pad = (value: number) => value < 10 ? '0' + value : String(value);
|
|
this.countdown.setText(pad(Math.floor(remaining / 3600)) + ':' + pad(Math.floor(remaining / 60) % 60) + ':' + pad(remaining % 60));
|
|
if (this.api.request && !remaining) { this.finishExpired(); return; }
|
|
}
|
|
private async run(action: string, body: any) {
|
|
if (this.busy || this.closing) return;
|
|
if (action === 'claim' && (this.claimWaiters.has(body.stage) || this.displayedClaims.has(body.stage))) return;
|
|
if (action !== 'claim') this.busy = true;
|
|
this.message.string = '请求中…';
|
|
if (action === 'claim') this.startClaimWaiting(body.stage);
|
|
{
|
|
let result;
|
|
try { result = await this.api.request(action, body); }
|
|
catch (error) {
|
|
result = { code: 0, msg: error.message || '请求失败,请点击重试' };
|
|
// Normal payment results are shown by the service; unexpected request failures still need visible feedback.
|
|
if (action === 'purchase') this.api.toast(result.msg);
|
|
}
|
|
if (!this.alive) return;
|
|
if (action !== 'claim') this.busy = false;
|
|
if (action === 'claim') this.stopClaimWaiting(body.stage);
|
|
if (result.code !== 1 && action !== 'purchase') this.api.toast(result.msg || '请求失败,请稍后重试');
|
|
this.message.string = result.msg || (result.data && result.data.status === 'locked' ? '尚未达到活动开放条件'
|
|
: result.data && result.data.status === 'unavailable' ? '活动已结束或尚未开放'
|
|
: result.data && result.data.status === 'purchase_disabled' ? '本期暂停购买,通关进度仍会累计' : '点击此处刷新活动');
|
|
if (result.data || action === 'claim') this.render(result.data || this.data);
|
|
|
|
return;
|
|
}
|
|
}
|
|
onDestroy() { this.alive = false; if (this.content && this.content.parent) this.content.parent.off(cc.Node.EventType.TOUCH_START, this.stopProgressFollow, this); this.readyScales.forEach((_, row) => this.stopReadyPulse(row)); this.claimWaiters.forEach((_, stage) => this.stopClaimWaiting(stage)); this.displayedClaims.clear(); cc.Tween.stopAllByTarget(this.purchaseButton); cc.Tween.stopAllByTarget(this.card); this.rewardRowScales.forEach((scale, row) => cc.Tween.stopAllByTarget(row)); this.rewardRowScales.clear(); this.rewardAnimations.slice().forEach(layer => this.clearRewardAnimation(layer)); this.rewardAnimations = []; this.unscheduleAllCallbacks(); this.boundRows = []; this.api = null; this.data = null; }
|
|
}
|