import { starterPackRemaining } from "../../Script/module/Config/StarterPack"; const { ccclass, property } = cc._decorator; /** Presentation for shop0917. Payment and reward delivery stay in shop.ts. */ @ccclass export default class Shop0917View extends cc.Component { @property(cc.ScrollView) scrollView: cc.ScrollView = null; @property(cc.Node) carousel: cc.Node = null; @property([cc.Node]) banners: cc.Node[] = []; @property([cc.Node]) dots: cc.Node[] = []; @property([cc.Node]) firstPurchase: cc.Node[] = []; @property(cc.Node) coin: cc.Node = null; @property(cc.Node) health: cc.Node = null; @property(cc.Node) monthPrivilege: cc.Node = null; @property(cc.Node) monthRemaining: cc.Node = null; @property(cc.Node) monthDays: cc.Node = null; @property(cc.Sprite) monthButton: cc.Sprite = null; @property([cc.SpriteFrame]) countFrames: cc.SpriteFrame[] = []; @property([cc.SpriteFrame]) healthFrames: cc.SpriteFrame[] = []; @property([cc.SpriteFrame]) timeFrames: cc.SpriteFrame[] = []; @property([cc.SpriteFrame]) dayFrames: cc.SpriteFrame[] = []; @property(cc.SpriteFrame) colonFrame: cc.SpriteFrame = null; @property(cc.SpriteFrame) monthOpen: cc.SpriteFrame = null; @property(cc.SpriteFrame) monthRenew: cc.SpriteFrame = null; @property(cc.SpriteFrame) dotNormal: cc.SpriteFrame = null; @property(cc.SpriteFrame) dotSelected: cc.SpriteFrame = null; private selectedBanner: number = 1; private touching: boolean = false; private dragged: boolean = false; private touchStartX: number = 0; private lastTouchX: number = 0; private touchId: number = null; private dragStartOffset: number = 0; private carouselOffset: number = 0; private snapping: boolean = false; private snapFrom: number = 0; private snapTo: number = 0; private snapTime: number = 0; private snapDuration: number = 0; private snapBanner: number = 1; private renderedValues = new Map(); onLoad(): void { this.node.on(cc.Node.EventType.SIZE_CHANGED, this.resize, this); this.carousel.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this, true); this.carousel.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this, true); this.carousel.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this, true); this.carousel.on(cc.Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this, true); } onEnable(): void { this.restartAutoAdvance(); this.scheduleOnce(() => { this.resize(); this.scrollView.scrollToTop(0); }); } onDisable(): void { this.unschedule(this.advanceBanner); this.touching = false; this.dragged = false; this.touchId = null; this.layoutBanners(); this.setBannerButtonsEnabled(true); } private resize(): void { const widget = this.scrollView.node.getComponent(cc.Widget); widget.updateAlignment(); this.scrollView.content.parent.getComponent(cc.Widget).updateAlignment(); this.node.getChildByName("Top").getComponent(cc.Widget).updateAlignment(); ["Background", "Loading", "ConfirmBox"].forEach(name => { this.node.getChildByName(name).getComponent(cc.Widget).updateAlignment(); }); this.fitSprite(this.node.getChildByName("Background")); } fitSprite(layout: cc.Node, frame?: cc.SpriteFrame): void { const sprite = layout.getComponentInChildren(cc.Sprite); if (frame) sprite.spriteFrame = frame; sprite.sizeMode = cc.Sprite.SizeMode.TRIMMED; const rect = sprite.spriteFrame.getRect(); sprite.node.setScale(layout.width / rect.width, layout.height / rect.height); } setCoin(value: number): void { this.renderNumber(this.coin, String(Math.max(0, Math.floor(Number(value) || 0))), this.countFrames, 37, 190); } setHealth(value: number): void { this.renderNumber(this.health, String(Math.max(0, Math.floor(Number(value) || 0))), this.healthFrames, 39, 72); } setTime(target: cc.Node, value: string): void { this.renderNumber(target, value, this.timeFrames, 32, 190); } private renderNumber(target: cc.Node, value: string, frames: cc.SpriteFrame[], height: number, maxWidth: number): void { if (this.renderedValues.get(target) === value) return; this.renderedValues.set(target, value); const oldChildren = target.children.slice(); target.removeAllChildren(); oldChildren.forEach(child => child.destroy()); const glyphs = value.split("").map(char => char === ":" ? this.colonFrame : frames[Number(char)]); const widths = glyphs.map(frame => { const size = frame.getRect(); return height * size.width / size.height; }); const gap = -1; const total = widths.reduce((sum, width) => sum + width, 0) + gap * (widths.length - 1); const scale = Math.min(1, maxWidth / total); let x = -total / 2; glyphs.forEach((frame, index) => { const digit = new cc.Node("glyph_" + index); const sprite = digit.addComponent(cc.Sprite); sprite.spriteFrame = frame; sprite.sizeMode = cc.Sprite.SizeMode.TRIMMED; const rect = frame.getRect(); digit.setScale(widths[index] * scale / rect.width, height * scale / rect.height); digit.setPosition((x + widths[index] / 2) * scale, 0); target.addChild(digit); x += widths[index] + gap; }); } refreshFirstPurchase(data: { [key: string]: boolean }): void { this.firstPurchase.forEach((node, index) => { node.active = !!data && data["gold_" + (index + 1)] === false; }); } refreshMonth(active: boolean, days: number): void { this.monthPrivilege.active = !active; this.monthRemaining.active = active; this.fitSprite(this.monthButton.node.parent, active ? this.monthRenew : this.monthOpen); if (active) { this.renderNumber(this.monthDays, String(Math.max(0, Math.ceil(Number(days) || 0))), this.dayFrames, 32, 80); } } refreshBanners(jungleAvailable: boolean): void { this.banners[0].active = starterPackRemaining() > 0; this.banners[2].active = jungleAvailable; if (!this.banners[this.selectedBanner].active) this.selectedBanner = 1; this.layoutBanners(); } private layoutBanners(): void { this.snapping = false; this.carouselOffset = 0; this.dragStartOffset = 0; this.touchStartX = this.lastTouchX; this.positionBanners(); this.updateDots(); } private positionBanners(): void { const available = this.banners.filter(banner => banner.active); const selected = available.indexOf(this.banners[this.selectedBanner]); available.forEach((banner, index) => { let x = (index - selected) * 900 + this.carouselOffset; if (available.length === 3) { // Recycle only outside the mask, so both peeking neighbours move // continuously through the first/last-page boundary. while (x > 1350) x -= 2700; while (x < -1350) x += 2700; } banner.x = x + 5; }); } private updateDots(): void { const available = this.banners.filter(banner => banner.active); this.dots.forEach((dot, index) => { dot.active = available.length > 1 && this.banners[index].active; if (!dot.active) return; dot.x = (available.indexOf(this.banners[index]) - (available.length - 1) / 2) * 47; const selected = index === this.selectedBanner; dot.setContentSize(selected ? 38 : 20, selected ? 38 : 20); this.fitSprite(dot, selected ? this.dotSelected : this.dotNormal); }); } selectBanner(_event: cc.Event, value: string): void { const index = Number(value); if (!this.touching && this.banners[index] && this.banners[index].active) { this.snapToBanner(index); this.restartAutoAdvance(); } } private advanceBanner(): void { if (this.touching || this.snapping) return; const available = this.banners.filter(banner => banner.active); const current = available.indexOf(this.banners[this.selectedBanner]); if (available.length > 1) { this.snapToBanner(this.banners.indexOf(available[(current + 1) % available.length])); } } private restartAutoAdvance(): void { this.unschedule(this.advanceBanner); this.schedule(this.advanceBanner, 5); } private snapToBanner(index: number): void { const available = this.banners.filter(banner => banner.active); let steps = available.indexOf(this.banners[index]) - available.indexOf(this.banners[this.selectedBanner]); if (available.length === 3) { if (steps > 1) steps -= 3; if (steps < -1) steps += 3; } this.snapBanner = index; this.snapFrom = this.carouselOffset; this.snapTo = -steps * 900; this.snapTime = 0; this.snapDuration = Math.min(.32, Math.max(.16, Math.abs(this.snapTo - this.snapFrom) / 2800)); this.snapping = true; } update(dt: number): void { if (!this.snapping) return; this.snapTime += dt; const t = Math.min(1, this.snapTime / this.snapDuration); const eased = 1 - Math.pow(1 - t, 3); this.carouselOffset = this.snapFrom + (this.snapTo - this.snapFrom) * eased; this.positionBanners(); if (t === 1) { this.selectedBanner = this.snapBanner; this.layoutBanners(); } } private onTouchStart(event: cc.Event.EventTouch): void { if (this.touching) return; this.unschedule(this.advanceBanner); this.snapping = false; this.touching = true; this.dragged = false; this.touchId = event.getID(); this.touchStartX = this.lastTouchX = this.carousel.convertToNodeSpaceAR(event.getLocation()).x; this.dragStartOffset = this.carouselOffset; } private onTouchMove(event: cc.Event.EventTouch): void { if (!this.touching || event.getID() !== this.touchId) return; this.lastTouchX = this.carousel.convertToNodeSpaceAR(event.getLocation()).x; const distance = this.lastTouchX - this.touchStartX; if (Math.abs(distance) > 12) { this.dragged = true; this.setBannerButtonsEnabled(false); } if (!this.dragged) return; const available = this.banners.filter(banner => banner.active); const current = available.indexOf(this.banners[this.selectedBanner]); let offset = this.dragStartOffset + distance; if (available.length === 1) offset = 0; else if (available.length === 2) { const min = -(available.length - 1 - current) * 900; const max = current * 900; if (offset < min) offset = min + (offset - min) * .25; if (offset > max) offset = max + (offset - max) * .25; } this.carouselOffset = Math.max(-900, Math.min(900, offset)); this.positionBanners(); } private onTouchEnd(event: cc.Event.EventTouch): void { this.onTouchCancel(event); } private onTouchCancel(event?: cc.Event.EventTouch): void { if (!this.touching || (event && event.getID() !== this.touchId)) return; // Creator emits TOUCH_CANCEL when a drag finishes outside its starting // button, even though it is still inside the carousel. const available = this.banners.filter(banner => banner.active); let target = available.indexOf(this.banners[this.selectedBanner]); if (event && Math.abs(this.carouselOffset) > 90) target += this.carouselOffset < 0 ? 1 : -1; target = available.length === 3 ? (target + 3) % 3 : Math.max(0, Math.min(available.length - 1, target)); this.snapToBanner(this.banners.indexOf(available[target])); this.touching = false; this.touchId = null; this.restartAutoAdvance(); // Restore after Button's touch-end dispatch so a swipe cannot purchase. this.scheduleOnce(() => { if (!this.touching) this.setBannerButtonsEnabled(true); }); } private setBannerButtonsEnabled(enabled: boolean): void { this.carousel.getComponentsInChildren(cc.Button).forEach(button => button.interactable = enabled); } scrollToCoins(): void { this.scrollView.scrollToOffset(cc.v2(0, Math.min(1620, this.scrollView.getMaxScrollOffset().y)), .25); } openStarter(event: cc.Event): void { const home: any = this.node.parent && this.node.parent.getComponent("JiaZai"); const game: any = this.node.parent && this.node.parent.getComponent("SceneManager"); if (home) home.openStarter_pack(event, "shop_page"); else if (game) game.openStarterPack(); } openHealth(event: cc.Event): void { const home: any = this.node.parent && this.node.parent.getComponent("JiaZai"); if (home) home.openHeath(event, "shop_page"); } onDestroy(): void { this.node.off(cc.Node.EventType.SIZE_CHANGED, this.resize, this); this.carousel.off(cc.Node.EventType.TOUCH_START, this.onTouchStart, this, true); this.carousel.off(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this, true); this.carousel.off(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this, true); this.carousel.off(cc.Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this, true); this.renderedValues.clear(); } }