MatchMaster/assets/shop/script/Shop0917View.ts
2026-09-22 10:17:58 +08:00

322 lines
14 KiB
TypeScript

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<cc.Node, string>();
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();
}
}