316 lines
12 KiB
TypeScript
316 lines
12 KiB
TypeScript
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();
|
||
}
|
||
}
|