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 = new Map(); private claimWaiters: Map = new Map(); private displayedClaims: Set = 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 = new Map(); private pendingReadyRows: Set = new Set(); private readyScales: Map = 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; } }