479 lines
19 KiB
TypeScript
479 lines
19 KiB
TypeScript
import {
|
|
JungleActivityConfig,
|
|
JungleRenderState,
|
|
JungleRewardConfig,
|
|
JungleRewardType,
|
|
JungleTierConfig,
|
|
JungleViewAssets,
|
|
JungleViewRefs,
|
|
} from "./JungleTypes";
|
|
import JungleTitleShine from "./JungleTitleShine";
|
|
|
|
/** 负责 Jungle Treasure 的节点创建、布局和静态渲染。 */
|
|
export default class JungleView {
|
|
private readonly designWidth: number = 1080;
|
|
private readonly slotCount: number = 6;
|
|
public contentNode: cc.Node = null;
|
|
public timerLabel: cc.Label = null;
|
|
public cards: cc.Node[] = [];
|
|
public 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);
|
|
|
|
let shade = new cc.Node("shade");
|
|
shade.parent = this.root;
|
|
this.stretchToParent(shade);
|
|
let 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);
|
|
let contentScale = Math.min(1, (this.root.width || this.designWidth) / this.designWidth);
|
|
this.contentNode.setScale(contentScale);
|
|
|
|
let contentHeight = (this.root.height || 1920) / contentScale;
|
|
let top = contentHeight * 0.5 - 80;
|
|
let row1Y = top - 1000;
|
|
let 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),
|
|
];
|
|
|
|
let track = this.createSpriteNode("track", this.assets.trackFrame);
|
|
track.parent = this.contentNode;
|
|
track.setPosition(0, row1Y - rowGap);
|
|
track.zIndex = 0;
|
|
|
|
let title = this.createSpriteNode("title", this.assets.titleFrame);
|
|
title.parent = this.contentNode;
|
|
title.setPosition(0, top - 515);
|
|
title.zIndex = 3;
|
|
title.addComponent(JungleTitleShine);
|
|
|
|
let timer = this.createSpriteNode("timer", this.assets.timerFrame);
|
|
timer.parent = this.contentNode;
|
|
timer.setPosition(-358, top - 235);
|
|
timer.zIndex = 5;
|
|
|
|
let timeNode = new cc.Node("time");
|
|
timeNode.parent = timer;
|
|
timeNode.setContentSize(270, 62);
|
|
timeNode.setPosition(38, 0);
|
|
timeNode.zIndex = 10;
|
|
|
|
this.timerLabel = this.createLabel(timeNode, "00:00:00", 46, cc.color(151, 116, 74), 270, 62);
|
|
this.timerLabel.enableBold = false;
|
|
this.timerLabel.node.name = "timerLabel";
|
|
|
|
|
|
let close = this.createCloseButton();
|
|
close.parent = this.contentNode;
|
|
close.setPosition(430, top - 245);
|
|
close.zIndex = 5;
|
|
|
|
for (let i = 0; i < this.slotCount; i++) {
|
|
let 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++) {
|
|
let 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 setClaimFrame(frame: cc.SpriteFrame): void {
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
this.assets.claimFrame = frame;
|
|
this.cards.forEach((card) => {
|
|
let button = card.getChildByName("button");
|
|
let content = button && button.getChildByName("button_content");
|
|
let claimImage = content && content.getChildByName("claim_image");
|
|
let sprite = claimImage && claimImage.getComponent(cc.Sprite);
|
|
if (!sprite) {
|
|
return;
|
|
}
|
|
sprite.spriteFrame = frame;
|
|
sprite.sizeMode = cc.Sprite.SizeMode.RAW;
|
|
claimImage.setContentSize(frame.getOriginalSize());
|
|
});
|
|
}
|
|
|
|
public renderCard(
|
|
card: cc.Node,
|
|
config: JungleActivityConfig,
|
|
state: JungleRenderState,
|
|
tierIndex: number,
|
|
slotIndex: number,
|
|
): void {
|
|
let tier = config.tiers[tierIndex];
|
|
card.active = !!tier;
|
|
if (!tier) {
|
|
return;
|
|
}
|
|
|
|
card.name = "reward_card_" + tier.id;
|
|
let rewardsNode = card.getChildByName("rewards");
|
|
rewardsNode.removeAllChildren();
|
|
this.renderRewards(rewardsNode, tier.rewards);
|
|
|
|
let button = card.getChildByName("button");
|
|
let buttonContent = button.getChildByName("button_content");
|
|
let lock = button.getChildByName("lock");
|
|
let isCurrent = slotIndex === 0 && !state.isPurchasing;
|
|
let isUnlocked = tier.state !== 0;
|
|
let isExpired = !!config.endAt && Date.now() >= config.endAt;
|
|
let isPurchaseBlocked = isExpired && tier.price > 0 && tier.state === 0;
|
|
|
|
this.renderButtonContent(buttonContent, tier);
|
|
buttonContent.opacity = 255;
|
|
buttonContent.scale = 1;
|
|
button.scale = 1;
|
|
lock.active = !isCurrent || !isUnlocked || isPurchaseBlocked;
|
|
button.getComponent(cc.Button).interactable = !state.isMoving && !isPurchaseBlocked;
|
|
button.color = isCurrent ? cc.Color.WHITE : cc.color(220, 220, 220);
|
|
}
|
|
|
|
/** 更新倒计时并返回剩余秒数。 */
|
|
public updateTimer(config: JungleActivityConfig): number {
|
|
// console.log("更新倒计时", config);
|
|
// return;
|
|
if (!this.timerLabel || !config) {
|
|
return 0;
|
|
}
|
|
let endAt = Number(config.endAt);
|
|
if (!endAt || !isFinite(endAt)) {
|
|
this.timerLabel.string = "活动中";
|
|
return -1;
|
|
}
|
|
let remaining = Math.max(0, Math.ceil((endAt - Date.now()) / 1000));
|
|
if (remaining >= 24 * 60 * 60) {
|
|
let days = Math.floor(remaining / 86400);
|
|
let hours = Math.floor((remaining % 86400) / 3600);
|
|
this.timerLabel.string = days + "天" + hours + "时";
|
|
}
|
|
else {
|
|
let hours = Math.floor(remaining / 3600);
|
|
let minutes = Math.floor((remaining % 3600) / 60);
|
|
let seconds = remaining % 60;
|
|
this.timerLabel.string = this.pad(hours)
|
|
+ ":" + this.pad(minutes) + ":" + this.pad(seconds);
|
|
}
|
|
|
|
return remaining;
|
|
}
|
|
|
|
private stretchToParent(node: cc.Node): void {
|
|
let parentSize = node.parent ? node.parent.getContentSize() : cc.size(this.designWidth, 1920);
|
|
node.setContentSize(parentSize.width || this.designWidth, parentSize.height || 1920);
|
|
let 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 {
|
|
let 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 {
|
|
let card = new cc.Node("reward_card_" + index);
|
|
card.setContentSize(444, 400);
|
|
|
|
let bg = this.createSpriteNode("card_bg", this.assets.cardFrame);
|
|
bg.parent = card;
|
|
|
|
let rewards = new cc.Node("rewards");
|
|
rewards.parent = card;
|
|
rewards.setContentSize(380, 230);
|
|
rewards.setPosition(0, 48);
|
|
|
|
let 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);
|
|
|
|
let buttonContent = new cc.Node("button_content");
|
|
buttonContent.parent = button;
|
|
buttonContent.setContentSize(270, 88);
|
|
|
|
let freeImage = this.createSpriteNode("free_image", this.assets.freeFrame);
|
|
freeImage.parent = buttonContent;
|
|
|
|
let priceLabel = this.createLabel(buttonContent, "0", 56, cc.Color.WHITE, 110, 64);
|
|
priceLabel.node.name = "price_label";
|
|
priceLabel.lineHeight = 56;
|
|
if (this.assets.priceFont) {
|
|
priceLabel.font = this.assets.priceFont;
|
|
}
|
|
|
|
let yuanImage = this.createSpriteNode("yuan_image", this.assets.yuanFrame);
|
|
yuanImage.parent = buttonContent;
|
|
|
|
let claimImage = this.createSpriteNode("claim_image", this.assets.claimFrame);
|
|
claimImage.parent = buttonContent;
|
|
|
|
let 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 {
|
|
let safeRewards = rewards && rewards.length > 0 ? rewards.slice(0, 5) : [];
|
|
if (safeRewards.length === 0) {
|
|
return;
|
|
}
|
|
let rewardYOffset = -25;
|
|
let numberScale = 1.2;
|
|
let iconScale = safeRewards.length === 1
|
|
? 1.1
|
|
: (safeRewards.length === 2 ? 0.85 : 0.6);
|
|
let trianglePositions = [
|
|
cc.v2(0, 75),
|
|
cc.v2(-90, -35),
|
|
cc.v2(90, -35),
|
|
];
|
|
let rows: JungleRewardConfig[][] = safeRewards.length <= 3
|
|
? [safeRewards]
|
|
: [safeRewards.slice(0, 3), safeRewards.slice(3)];
|
|
|
|
rows.forEach((row, rowIndex) => {
|
|
let frames = row.map((reward) => this.getRewardFrame(reward.type));
|
|
let iconScales = row.map((reward) => this.isToolReward(reward.type)
|
|
? 0.45 * (safeRewards.length === 1 ? 1 : iconScale)
|
|
: iconScale);
|
|
let widths = frames.map((frame, index) => frame ? frame.getOriginalSize().width * iconScales[index] : 0);
|
|
let gap = safeRewards.length === 1
|
|
? 8
|
|
: (safeRewards.length === 2 ? 35 : 12);
|
|
let totalWidth = widths.reduce((sum, width) => sum + width, 0) + gap * Math.max(0, row.length - 1);
|
|
let cursorX = -totalWidth * 0.5;
|
|
let rowItems: { group: cc.Node; label: cc.Label }[] = [];
|
|
|
|
row.forEach((reward, index) => {
|
|
let isToolReward = this.isToolReward(reward.type);
|
|
let frame = frames[index];
|
|
let originalSize = frame ? frame.getOriginalSize() : cc.size(0, 0);
|
|
let group = new cc.Node("reward_" + reward.type);
|
|
group.parent = parent;
|
|
let rowY = (rows.length === 1 ? 15 : (rowIndex === 0 ? 65 : -55)) + rewardYOffset;
|
|
if (safeRewards.length === 3) {
|
|
rowY -= 20;
|
|
}
|
|
else if (safeRewards.length === 1 && isToolReward) {
|
|
rowY -= 20;
|
|
}
|
|
let groupX = cursorX + widths[index] * 0.5;
|
|
if (safeRewards.length === 3) {
|
|
groupX = trianglePositions[index].x;
|
|
rowY += trianglePositions[index].y;
|
|
}
|
|
group.setPosition(groupX, rowY);
|
|
cursorX += widths[index] + gap;
|
|
|
|
let icon = this.createSpriteNode("icon", frame);
|
|
icon.parent = group;
|
|
let rewardIconScale = iconScales[index];
|
|
icon.setScale(rewardIconScale);
|
|
let labelText = reward.type === "infinite_health"
|
|
? this.formatInfiniteHealth(reward.count)
|
|
: (reward.type === "coin" ? reward.count.toString() : "x" + reward.count);
|
|
let baseFontSize = rows.length === 1 ? 36 : 29;
|
|
let rewardNumberScale = safeRewards.length === 1
|
|
? numberScale * 1.1
|
|
: numberScale;
|
|
if (isToolReward) {
|
|
rewardNumberScale *= 0.95;
|
|
}
|
|
let scaledWidth = originalSize.width * rewardIconScale;
|
|
let scaledHeight = originalSize.height * rewardIconScale;
|
|
let labelFontSize = Math.round(baseFontSize * rewardNumberScale);
|
|
let labelWidth = isToolReward
|
|
? Math.max(70, labelText.length * labelFontSize)
|
|
: Math.max(126, scaledWidth * rewardNumberScale, labelText.length * labelFontSize);
|
|
let label = this.createLabel(
|
|
group,
|
|
labelText,
|
|
labelFontSize,
|
|
cc.Color.WHITE,
|
|
labelWidth,
|
|
Math.round(44 * rewardNumberScale),
|
|
);
|
|
if (this.assets.countFont) {
|
|
label.font = this.assets.countFont;
|
|
}
|
|
if (isToolReward) {
|
|
label.node.setPosition(scaledWidth * 0.34, -scaledHeight * 0.34);
|
|
}
|
|
else {
|
|
label.node.setPosition(0, rows.length === 1 ? -scaledHeight * 0.5 - 10 : -scaledHeight * 0.5 + 17);
|
|
}
|
|
let outline = label.node.addComponent(cc.LabelOutline);
|
|
outline.color = cc.color(62, 47, 38);
|
|
outline.width = isToolReward ? 3 : 4;
|
|
rowItems.push({ group, label });
|
|
});
|
|
|
|
// 通过移动整个奖励组对齐数量文字底边,保持图标和 Label 的相对位置不变。
|
|
// 三角布局只对齐下方两个奖励;上方奖励保持独立高度。
|
|
let alignedItems = safeRewards.length === 3 ? rowItems.slice(1) : rowItems;
|
|
let labelBottoms = alignedItems.map((item) => {
|
|
return item.group.y + item.label.node.y
|
|
- item.label.node.height * item.label.node.anchorY;
|
|
});
|
|
let targetBottom = Math.min.apply(null, labelBottoms);
|
|
alignedItems.forEach((item, index) => {
|
|
item.group.y += targetBottom - labelBottoms[index];
|
|
});
|
|
});
|
|
}
|
|
|
|
private isToolReward(type: JungleRewardType): boolean {
|
|
return type === "freeze" || type === "hammer" || type === "magic_wand";
|
|
}
|
|
|
|
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 {
|
|
let node = new cc.Node(name);
|
|
let sprite = node.addComponent(cc.Sprite);
|
|
sprite.sizeMode = cc.Sprite.SizeMode.RAW;
|
|
sprite.spriteFrame = frame;
|
|
if (frame) {
|
|
node.setContentSize(frame.getOriginalSize());
|
|
}
|
|
return node;
|
|
}
|
|
|
|
public createLabel(
|
|
parent: cc.Node,
|
|
text: string,
|
|
fontSize: number,
|
|
color: cc.Color,
|
|
width: number,
|
|
height: number,
|
|
): cc.Label {
|
|
let node = new cc.Node("label");
|
|
node.parent = parent;
|
|
let label = node.addComponent(cc.Label);
|
|
label.overflow = cc.Label.Overflow.SHRINK;
|
|
label.string = text;
|
|
label.fontSize = fontSize;
|
|
label.lineHeight = fontSize + 8;
|
|
label.horizontalAlign = cc.Label.HorizontalAlign.CENTER;
|
|
label.verticalAlign = cc.Label.VerticalAlign.CENTER;
|
|
node.setContentSize(width, height);
|
|
node.color = color;
|
|
return label;
|
|
}
|
|
|
|
private renderButtonContent(content: cc.Node, tier: JungleTierConfig): void {
|
|
let freeImage = content.getChildByName("free_image");
|
|
let priceLabelNode = content.getChildByName("price_label");
|
|
let yuanImage = content.getChildByName("yuan_image");
|
|
let claimImage = content.getChildByName("claim_image");
|
|
let isFree = tier.price <= 0;
|
|
let isClaimablePaidTier = tier.price > 0 && tier.state === 1;
|
|
|
|
let claimSprite = claimImage.getComponent(cc.Sprite);
|
|
if (claimSprite && !claimSprite.spriteFrame && this.assets.claimFrame) {
|
|
claimSprite.spriteFrame = this.assets.claimFrame;
|
|
claimImage.setContentSize(this.assets.claimFrame.getOriginalSize());
|
|
}
|
|
|
|
freeImage.active = isFree;
|
|
priceLabelNode.active = !isFree && !isClaimablePaidTier;
|
|
yuanImage.active = !isFree && !isClaimablePaidTier;
|
|
claimImage.active = isClaimablePaidTier;
|
|
if (isFree || isClaimablePaidTier) {
|
|
return;
|
|
}
|
|
|
|
let priceText = Math.max(0, Math.round(Number(tier.price) || 0)).toString();
|
|
let digitWidth = Math.max(51, priceText.length * 51);
|
|
let yuanWidth = this.assets.yuanFrame
|
|
? this.assets.yuanFrame.getOriginalSize().width
|
|
: 51;
|
|
let gap = 5;
|
|
let totalWidth = digitWidth + gap + yuanWidth;
|
|
let left = -totalWidth * 0.5;
|
|
let priceLabel = priceLabelNode.getComponent(cc.Label);
|
|
priceLabel.string = priceText;
|
|
priceLabelNode.setContentSize(digitWidth, 64);
|
|
priceLabelNode.x = left + digitWidth * 0.5;
|
|
yuanImage.x = left + digitWidth + gap + yuanWidth * 0.5;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|