MatchMaster/assets/jungle_treasure/script/JungleView.ts
2026-08-03 20:04:40 +08:00

316 lines
12 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 {
JungleActivityConfig,
JungleRenderState,
JungleRewardConfig,
JungleRewardType,
JungleTierConfig,
JungleViewAssets,
JungleViewRefs,
} from "./JungleTypes";
/** 负责 Jungle Treasure 的节点创建、布局和静态渲染。 */
export default class JungleView {
private readonly designWidth: number = 1080;
private readonly slotCount: number = 6;
private contentNode: cc.Node = null;
private timerLabel: cc.Label = null;
private cards: cc.Node[] = [];
private slotPositions: cc.Vec2[] = [];
constructor(
private root: cc.Node,
private assets: JungleViewAssets,
private eventTarget: any,
private onClose: () => void,
private onCardClicked: (button: cc.Button) => void,
) { }
public build(): JungleViewRefs {
this.root.name = "jungle";
this.root.zIndex = 999;
if (!this.root.getComponent(cc.BlockInputEvents)) {
this.root.addComponent(cc.BlockInputEvents);
}
this.stretchToParent(this.root);
const shade = new cc.Node("shade");
shade.parent = this.root;
this.stretchToParent(shade);
const shadeGraphics = shade.addComponent(cc.Graphics);
shadeGraphics.fillColor = cc.color(35, 25, 20, 205);
shadeGraphics.rect(-1600, -2200, 3200, 4400);
shadeGraphics.fill();
this.contentNode = new cc.Node("content");
this.contentNode.parent = this.root;
this.contentNode.setContentSize(this.designWidth, this.root.height || 1920);
const contentScale = Math.min(1, (this.root.width || this.designWidth) / this.designWidth);
this.contentNode.setScale(contentScale);
const contentHeight = (this.root.height || 1920) / contentScale;
const top = contentHeight * 0.5 - 80;
const row1Y = top - 1000;
const rowGap = 445;
this.slotPositions = [
cc.v2(-252, row1Y),
cc.v2(252, row1Y),
cc.v2(252, row1Y - rowGap),
cc.v2(-252, row1Y - rowGap),
cc.v2(-252, row1Y - rowGap * 2),
cc.v2(252, row1Y - rowGap * 2),
];
const track = this.createSpriteNode("track", this.assets.trackFrame);
track.parent = this.contentNode;
track.setPosition(0, row1Y - rowGap);
track.zIndex = 0;
const title = this.createSpriteNode("title", this.assets.titleFrame);
title.parent = this.contentNode;
title.setPosition(0, top - 515);
title.zIndex = 3;
const timer = this.createSpriteNode("timer", this.assets.timerFrame);
timer.parent = this.contentNode;
timer.setPosition(-358, top - 235);
timer.zIndex = 5;
this.timerLabel = this.createLabel(timer, "00:00:00", 37, cc.color(128, 91, 54), 220, 62);
this.timerLabel.node.setPosition(36, -200);
const close = this.createCloseButton();
close.parent = this.contentNode;
close.setPosition(430, top - 245);
close.zIndex = 5;
for (let i = 0; i < this.slotCount; i++) {
const card = this.createCard(i);
card.parent = this.contentNode;
card.setPosition(this.slotPositions[i]);
card.zIndex = 2;
this.cards.push(card);
}
return {
contentNode: this.contentNode,
cards: this.cards,
slotPositions: this.slotPositions,
};
}
public renderAllCards(config: JungleActivityConfig, state: JungleRenderState): void {
if (!config || this.cards.length === 0) {
return;
}
for (let i = 0; i < this.cards.length; i++) {
const card = this.cards[i];
card.stopAllActions();
card.setAnchorPoint(0.5, 0.5);
card.setPosition(this.slotPositions[i]);
card.scale = 1;
card.opacity = 255;
this.renderCard(card, config, state, state.currentTierIndex + i, i);
}
}
public renderCard(
card: cc.Node,
config: JungleActivityConfig,
state: JungleRenderState,
tierIndex: number,
slotIndex: number,
): void {
const tier = config.tiers[tierIndex];
card.active = !!tier;
if (!tier) {
return;
}
card.name = "reward_card_" + tier.id;
const rewardsNode = card.getChildByName("rewards");
rewardsNode.removeAllChildren();
this.renderRewards(rewardsNode, tier.rewards);
const button = card.getChildByName("button");
const buttonLabel = button.getChildByName("button_label").getComponent(cc.Label);
const lock = button.getChildByName("lock");
const isCurrent = slotIndex === 0 && !state.isPurchasing;
const isExpired = Date.now() >= config.endAt;
buttonLabel.string = this.formatPrice(tier);
buttonLabel.node.opacity = 255;
button.scale = 1;
lock.active = !isCurrent || isExpired;
button.getComponent(cc.Button).interactable = !state.isMoving && !isExpired;
button.color = isCurrent ? cc.Color.WHITE : cc.color(220, 220, 220);
}
/** 更新倒计时并返回剩余秒数。 */
public updateTimer(config: JungleActivityConfig): number {
if (!this.timerLabel || !config) {
return 0;
}
const remaining = Math.max(0, Math.floor((config.endAt - Date.now()) / 1000));
const days = Math.floor(remaining / 86400);
const hours = Math.floor((remaining % 86400) / 3600);
const minutes = Math.floor((remaining % 3600) / 60);
const seconds = remaining % 60;
this.timerLabel.string = (days > 0 ? days + "天 " : "")
+ this.pad(hours) + ":" + this.pad(minutes) + ":" + this.pad(seconds);
return remaining;
}
private stretchToParent(node: cc.Node): void {
const parentSize = node.parent ? node.parent.getContentSize() : cc.size(this.designWidth, 1920);
node.setContentSize(parentSize.width || this.designWidth, parentSize.height || 1920);
const widget = node.addComponent(cc.Widget);
widget.isAlignLeft = true;
widget.isAlignRight = true;
widget.isAlignTop = true;
widget.isAlignBottom = true;
widget.left = widget.right = widget.top = widget.bottom = 0;
}
private createCloseButton(): cc.Node {
const node = this.createSpriteNode("close", this.assets.closeFrame);
node.addComponent(cc.Button);
node.on("click", this.onClose, this.eventTarget);
return node;
}
private createCard(index: number): cc.Node {
const card = new cc.Node("reward_card_" + index);
card.setContentSize(444, 400);
const bg = this.createSpriteNode("card_bg", this.assets.cardFrame);
bg.parent = card;
const rewards = new cc.Node("rewards");
rewards.parent = card;
rewards.setContentSize(380, 230);
rewards.setPosition(0, 48);
const button = this.createSpriteNode("button", this.assets.buttonFrame);
button.parent = card;
button.setPosition(0, -137);
button.addComponent(cc.Button);
button.on("click", this.onCardClicked, this.eventTarget);
const buttonLabel = this.createLabel(button, "免费", 48, cc.Color.WHITE, 270, 88);
buttonLabel.node.name = "button_label";
const outline = buttonLabel.node.addComponent(cc.LabelOutline);
outline.color = cc.color(120, 53, 22);
outline.width = 4;
const lock = this.createSpriteNode("lock", this.assets.lockFrame);
lock.parent = button;
lock.setPosition(120, 18);
lock.active = false;
return card;
}
private renderRewards(parent: cc.Node, rewards: JungleRewardConfig[]): void {
const safeRewards = rewards && rewards.length > 0 ? rewards.slice(0, 5) : [];
if (safeRewards.length === 0) {
return;
}
const rows: JungleRewardConfig[][] = safeRewards.length <= 3
? [safeRewards]
: [safeRewards.slice(0, 3), safeRewards.slice(3)];
rows.forEach((row, rowIndex) => {
const frames = row.map((reward) => this.getRewardFrame(reward.type));
const widths = frames.map((frame) => frame ? frame.getOriginalSize().width : 0);
const gap = 8;
const totalWidth = widths.reduce((sum, width) => sum + width, 0) + gap * Math.max(0, row.length - 1);
let cursorX = -totalWidth * 0.5;
row.forEach((reward, index) => {
const frame = frames[index];
const originalSize = frame ? frame.getOriginalSize() : cc.size(0, 0);
const group = new cc.Node("reward_" + reward.type);
group.parent = parent;
const rowY = rows.length === 1 ? 15 : (rowIndex === 0 ? 65 : -55);
group.setPosition(cursorX + widths[index] * 0.5, rowY);
cursorX += widths[index] + gap;
const icon = this.createSpriteNode("icon", frame);
icon.parent = group;
const labelText = reward.type === "infinite_health"
? this.formatInfiniteHealth(reward.count)
: "×" + reward.count;
const label = this.createLabel(
group,
labelText,
rows.length === 1 ? 36 : 29,
cc.Color.WHITE,
Math.max(105, originalSize.width),
44,
);
label.node.setPosition(0, rows.length === 1 ? -originalSize.height * 0.5 - 10 : -originalSize.height * 0.5 + 17);
const outline = label.node.addComponent(cc.LabelOutline);
outline.color = cc.color(62, 47, 38);
outline.width = 4;
});
});
}
private getRewardFrame(type: JungleRewardType): cc.SpriteFrame {
switch (type) {
case "coin": return this.assets.coinFrame;
case "freeze": return this.assets.freezeFrame;
case "hammer": return this.assets.hammerFrame;
case "magic_wand": return this.assets.magicWandFrame;
case "infinite_health": return this.assets.infiniteHealthFrame;
default: return this.assets.coinFrame;
}
}
private createSpriteNode(name: string, frame: cc.SpriteFrame): cc.Node {
const node = new cc.Node(name);
const sprite = node.addComponent(cc.Sprite);
sprite.sizeMode = cc.Sprite.SizeMode.RAW;
sprite.spriteFrame = frame;
if (frame) {
node.setContentSize(frame.getOriginalSize());
}
return node;
}
private createLabel(
parent: cc.Node,
text: string,
fontSize: number,
color: cc.Color,
width: number,
height: number,
): cc.Label {
const node = new cc.Node("label");
node.parent = parent;
node.setContentSize(width, height);
const label = node.addComponent(cc.Label);
label.string = text;
label.fontSize = fontSize;
label.lineHeight = fontSize + 8;
label.horizontalAlign = cc.Label.HorizontalAlign.CENTER;
label.verticalAlign = cc.Label.VerticalAlign.CENTER;
label.overflow = cc.Label.Overflow.SHRINK;
node.color = color;
return label;
}
private formatPrice(tier: JungleTierConfig): string {
return tier.price > 0 ? (Math.round(tier.price * 100) / 100).toString() + "元" : "免费";
}
private formatInfiniteHealth(seconds: number): string {
if (seconds >= 3600 && seconds % 3600 === 0) {
return "+" + (seconds / 3600) + "小时";
}
return "+" + Math.max(1, Math.floor(seconds / 60)) + "分钟";
}
private pad(value: number): string {
return value < 10 ? "0" + value : value.toString();
}
}