MatchMaster/assets/seven_day_gift/SevenDayGift.ts

394 lines
17 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 { SevenDayActivityInfo, SevenDayReward, SevenDayRewardItem } from "./SevenDayGiftApi";
import SevenDayGiftRuntime from "./SevenDayGiftRuntime";
import ActivityPopupAnimator, { ActivityPopupAnimationType } from "../Script/ActivityPopupAnimator";
import RewardClaimEffect from "../reward_claim_effect/RewardClaimEffect";
import SevenDayHitTest from "./SevenDayHitTest";
const { ccclass, property } = cc._decorator;
const TOTAL_DAYS = 7;
type ClaimHandler = (callback: (success: boolean) => void) => void;
type CloseHandler = () => void;
/** 七日活动视图:卡片展示、点击判定和领取效果。 */
@ccclass
export default class SevenDayGift extends cc.Component {
@property(cc.SpriteFrame) cardBlue: cc.SpriteFrame = null;
@property(cc.SpriteFrame) cardYellow: cc.SpriteFrame = null;
@property(cc.SpriteFrame) cardSpecial: cc.SpriteFrame = null;
@property(cc.SpriteFrame) claimedOverlay: cc.SpriteFrame = null;
@property(cc.SpriteFrame) claimedBadge: cc.SpriteFrame = null;
@property(cc.SpriteFrame) catArt: cc.SpriteFrame = null;
@property(cc.SpriteFrame) catLocked: cc.SpriteFrame = null;
@property(cc.SpriteFrame) coinIcon: cc.SpriteFrame = null;
@property(cc.SpriteFrame) hammerIcon: cc.SpriteFrame = null;
@property(cc.SpriteFrame) freezeIcon: cc.SpriteFrame = null;
@property(cc.SpriteFrame) magicWandIcon: cc.SpriteFrame = null;
@property(cc.SpriteFrame) infiniteHealthIcon: cc.SpriteFrame = null;
@property({ type: [cc.SpriteFrame], tooltip: "数量图片:依次为 0~9、加号、乘号、分钟" })
rewardNumberFrames: cc.SpriteFrame[] = [];
@property({
type: cc.Enum(ActivityPopupAnimationType),
displayName: "弹窗动画",
tooltip: "活动页面打开和关闭时使用的动画"
})
popupAnimation: ActivityPopupAnimationType = ActivityPopupAnimationType.Q弹弹出;
private info: SevenDayActivityInfo = null;
private cards: cc.Node[] = [];
private claimHandler: ClaimHandler = null;
private claiming = false; // 等待领取回调,阻止重复提交。
private effectPlaying = false; // 也覆盖测试面板单独预览效果的情况。
private panel: cc.Node = null;
private mask: cc.Node = null;
private closeNode: cc.Node = null;
private closeEnabled = true;
private closeHandler: CloseHandler = null;
private runtime: SevenDayGiftRuntime = null;
// 缓存节点、绑定点击,再创建正式领取流程。
onLoad() {
if (!this.cacheView()) {
this.node.active = false;
return;
}
this.bindEvents();
this.runtime = new SevenDayGiftRuntime(this);
}
// 结束业务流程,忽略后续异步返回。
onDestroy() {
if (this.runtime) this.runtime.destroy();
this.runtime = null;
}
// 主页加载分包后,从这里传入正式活动数据。
public initializeRuntime(info: SevenDayActivityInfo, closeHandler: CloseHandler): boolean {
// Bundle 节点先以 inactive 挂入场景;激活后 onLoad 才会创建 Runtime。
this.node.active = true;
if (!this.runtime) {
cc.error("[SevenDayGift] Runtime 未初始化,请检查节点激活和 Prefab 结构");
return false;
}
this.runtime.initialize(info, closeHandler);
return true;
}
// 把新的活动查询结果交给业务层。
public updateRuntimeInfo(info: SevenDayActivityInfo) {
if (this.runtime) this.runtime.updateInfo(info);
}
// 共用视图入口,正式流程和本地模拟都可提供领取回调。
public setData(info: SevenDayActivityInfo, claimHandler: ClaimHandler) {
this.info = info;
this.claimHandler = claimHandler;
this.refreshView();
}
// 登记弹窗关闭后的处理,由主页负责释放资源。
public setCloseHandler(closeHandler: CloseHandler) {
this.closeHandler = closeHandler;
}
// 打开时只展示奖励,等待玩家点击领取。
public show(_autoClaim: boolean = false): boolean {
if (!this.info || !this.panel) return false;
this.node.active = true;
if (!this.node.activeInHierarchy) return false;
this.setCloseEnabled(true);
this.node.active = true;
this.refreshView();
this.node.opacity = 255;
this.node.scale = 1;
ActivityPopupAnimator.open(this.panel, this.mask, this.popupAnimation);
return true;
}
// 有奖励时本次只领取;无奖励时才关闭,忙碌期间忽略点击。
public close(event?: cc.Event) {
if (event) event.stopPropagation();
if (!this.closeEnabled) return;
if (this.claiming || this.effectPlaying) return;
if (this.info && this.info.canClaim && !this.info.todayClaimed) {
this.requestCurrentReward();
return;
}
this.setCloseEnabled(false);
ActivityPopupAnimator.close(this.panel, this.mask, this.popupAnimation, () => {
this.node.active = false;
if (this.closeHandler) this.closeHandler();
});
}
// 按 Prefab 约定找到卡片,关闭按钮始终隐藏。
private cacheView(): boolean {
this.panel = this.node.getChildByName("panel");
this.mask = this.node.getChildByName("mask");
this.closeNode = this.panel && this.panel.getChildByName("close");
const cards = this.panel && this.panel.getChildByName("cards");
const message = this.panel && this.panel.getChildByName("message");
if (!this.panel || !this.mask || !cards) {
cc.error("[SevenDayGift] prefab structure is incomplete");
return false;
}
if (message) message.active = false;
if (this.closeNode) this.closeNode.active = false;
for (let day = 1; day <= TOTAL_DAYS; day++) {
const card = cards.getChildByName("giftDay" + day);
if (!card) {
cc.error("[SevenDayGift] missing card for day " + day);
return false;
}
this.cards.push(card);
}
return true;
}
// 区分可见 UI 与外部点击,奖励卡片单独处理领取。
private bindEvents() {
// 所有点击共用一次可见性判断,透明处不会被矩形事件范围挡住。
const blockVisibleUI = (node: cc.Node) => {
if (node.getComponent(cc.Sprite) || node.getComponent(cc.Label)) {
node.on(cc.Node.EventType.TOUCH_END, this.onPanelTouch, this);
}
node.children.forEach(blockVisibleUI);
};
blockVisibleUI(this.panel);
this.mask.on(cc.Node.EventType.TOUCH_END, this.onPanelTouch, this);
}
// 透明处仅执行本次外部点击;命中奖励子图时沿父节点找到所属卡片。
private onPanelTouch(event: cc.Event.EventTouch) {
event.stopPropagation();
let hit = SevenDayHitTest.findVisible(this.panel, event.getLocation());
if (!hit) {
this.close();
return;
}
while (hit && hit !== this.panel) {
const index = this.cards.indexOf(hit);
if (index >= 0) {
this.onCardClicked(index + 1);
return;
}
hit = hit.parent;
}
}
// 按最新活动快照刷新七张卡片。
private refreshView() {
if (!this.info || this.cards.length !== TOTAL_DAYS) return;
for (let day = 1; day <= TOTAL_DAYS; day++) {
this.refreshCard(this.cards[day - 1], day, this.findReward(day));
}
}
// 设置当天高亮、已领印章和奖励图标。
private refreshCard(card: cc.Node, day: number, reward: SevenDayReward) {
const claimed = !!(reward && reward.claimed);
const available = !!this.info.canClaim && day === Number(this.info.nextRewardDay);
const special = day === TOTAL_DAYS;
const cardFrame = special ? this.cardSpecial : available ? this.cardYellow : this.cardBlue;
this.setSprite(card, cardFrame);
// 白色不改变贴图颜色,保留美术资源的原色。
card.color = cc.Color.WHITE;
card.opacity = 255;
const dayLabel = card.getChildByName("dayLabel").getComponent(cc.Label);
dayLabel.string = day === TOTAL_DAYS ? "第7天" : "第" + day + "天";
dayLabel.fontSize = 32;
dayLabel.lineHeight = 38;
dayLabel.node.color = cc.Color.WHITE;
const dayOutline = dayLabel.node.getComponent(cc.LabelOutline);
if (dayOutline) {
dayOutline.color = special
? cc.color(238, 91, 126)
: available ? cc.color(242, 151, 0) : cc.color(0, 170, 182);
}
const items = reward && Array.isArray(reward.items) ? reward.items : [];
this.refreshRewardItems(card, items, special, available || claimed);
// 保留原卡片,遮罩压暗日期和奖励,印章绘制在遮罩上方。
let overlay = card.getChildByName("claimedOverlay");
if (!overlay) {
overlay = new cc.Node("claimedOverlay");
const sprite = overlay.addComponent(cc.Sprite);
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
card.addChild(overlay);
}
this.setSprite(overlay, this.claimedOverlay);
overlay.setContentSize(card.width, card.height);
overlay.setPosition(0, 0);
overlay.zIndex = 9;
overlay.active = claimed;
let badge = card.getChildByName("claimedBadge");
if (!badge) {
badge = new cc.Node("claimedBadge");
badge.addComponent(cc.Sprite);
card.addChild(badge);
badge.zIndex = 10;
}
badge.setContentSize(118, 99);
badge.setPosition(0, 0);
badge.active = claimed;
this.setSprite(badge, this.claimedBadge);
card.stopAllActions();
card.scale = 1;
if (available) {
cc.tween(card).repeatForever(
cc.tween().to(0.65, { scale: 1.04 }).to(0.65, { scale: 1 })
).start();
}
}
// 只允许领取当前可领天数,其他卡片点击无效。
private onCardClicked(day: number) {
const reward = this.findReward(day);
if (reward && reward.claimed) {
return;
}
if (!this.info.canClaim || day !== Number(this.info.nextRewardDay)) {
return;
}
if (this.claiming || !this.claimHandler) return;
this.requestCurrentReward();
}
// 统一卡片和外部点击的领取入口,直到回调结束才解锁。
private requestCurrentReward() {
if (this.claiming || this.effectPlaying || !this.claimHandler || !this.info || !this.info.canClaim) return;
this.claiming = true;
this.claimHandler((success: boolean) => {
this.claiming = false;
this.setCloseEnabled(true);
if (!success) cc.warn("[SevenDayGift] 领取未完成,可点击当天奖励重试");
});
}
// 控制是否允许关闭,不再显示关闭按钮。
private setCloseEnabled(enabled: boolean) {
this.closeEnabled = enabled;
if (this.closeNode) this.closeNode.active = false;
}
// 从对应道具位置生成图标和数量,播放期间锁住交互。
public playRewardEffect(items: SevenDayRewardItem[], day: number, done: () => void) {
this.effectPlaying = true;
const card = this.cards[day - 1];
const container = card && card.getChildByName("rewardItems");
RewardClaimEffect.play(this.node, items.map((item, index) => {
const source = container && container.children[index] || card || this.panel;
return {
icon: this.getRewardIcon(item, true),
count: Math.max(0, Number(item.count !== undefined ? item.count : item.amount) || 0),
prefix: item.type === "infinite_health" || item.type === "coin" ? "" as const : "×" as const,
isTime: item.type === "infinite_health",
worldPosition: source.convertToWorldSpaceAR(cc.v2()),
worldPopPosition: source.convertToWorldSpaceAR(cc.v2(0, source.height)),
};
}), () => {
this.effectPlaying = false;
done();
}, this.rewardNumberFrames);
}
// 按天数寻找奖励,不依赖服务端数组顺序。
private findReward(day: number): SevenDayReward {
const rewards = Array.isArray(this.info.rewards) ? this.info.rewards : [];
for (let i = 0; i < rewards.length; i++) {
if (Number(rewards[i].day) === day) return rewards[i];
}
return null;
}
// 重建卡片道具图标,多奖励横向重叠显示。
private refreshRewardItems(card: cc.Node, items: SevenDayRewardItem[], special: boolean, unlocked: boolean) {
const originalIcon = card.getChildByName("rewardIcon");
if (originalIcon) originalIcon.active = false;
let container = card.getChildByName("rewardItems");
if (!container) {
container = new cc.Node("rewardItems");
container.zIndex = 2;
card.addChild(container);
}
container.children.slice().forEach(child => {
child.removeFromParent();
child.destroy();
});
container.active = items.length > 0;
if (!items.length) return;
const count = items.length;
const availableWidth = special ? 220 : 184;
// 图标略微缩小,为下方图片数量留出空间。
const iconSize = special ? 116 : 92;
const step = count === 1 ? 0 : Math.min(iconSize + 8, (availableWidth - iconSize) / (count - 1));
const totalWidth = iconSize + step * (count - 1);
const startX = -totalWidth / 2 + iconSize / 2;
const centerY = -4; // 道具和数量整体下移 12px。
container.setContentSize(availableWidth, special ? 210 : 130);
container.setPosition(0, special ? -8 : -4);
items.forEach((item, index) => {
const itemNode = new cc.Node("rewardItem" + index);
// 第七天突出小猫,爱心缩小;按实际尺寸限制位置,避免超出卡片。
const baseSize = special && item.type === "cat_skin" ? 140
: special && item.type === "infinite_health" ? 78 : iconSize;
const size = count === 1 ? Math.round(baseSize * 1.15) : baseSize;
const maxX = (availableWidth - size) / 2;
const x = Math.max(-maxX, Math.min(maxX, startX + step * index));
itemNode.setContentSize(size, size);
// 爱心略微下移,形成高低层次,让小猫成为视觉重点。
const y = special && item.type === "infinite_health" ? centerY - 24 : centerY;
itemNode.setPosition(special && item.type === "infinite_health" ? x - 12 : x, y);
itemNode.zIndex = index;
container.addChild(itemNode);
const sprite = itemNode.addComponent(cc.Sprite);
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
sprite.spriteFrame = this.getRewardIcon(item, unlocked);
// 按原图比例收进目标尺寸,避免非正方形道具被拉伸。
sprite.trim = false;
if (sprite.spriteFrame) {
const original = sprite.spriteFrame.getOriginalSize();
const scale = size / Math.max(original.width, original.height, 1);
itemNode.setContentSize(original.width * scale, original.height * scale);
}
// 数量紧跟各自图标;双奖励各占自己的宽度,避免文字相互挤压。
RewardClaimEffect.createAmount(itemNode, {
count: Math.max(0, Number(item.count !== undefined ? item.count : item.amount) || 0),
prefix: item.type === "coin" || item.type === "infinite_health" ? "" : "×",
isTime: item.type === "infinite_health",
}, this.rewardNumberFrames, count > 1 ? Math.min(88, step) : 100, 26, itemNode);
});
}
// 将奖励类型映射为活动图标,猫皮肤区分锁定状态。
private getRewardIcon(item: SevenDayRewardItem, unlocked: boolean): cc.SpriteFrame {
switch (item.type) {
case "coin": return this.coinIcon;
case "hammer": return this.hammerIcon;
case "freeze": return this.freezeIcon;
case "magic_wand": return this.magicWandIcon;
case "infinite_health": return this.infiniteHealthIcon;
case "cat_skin": return unlocked ? this.catArt : this.catLocked;
default: return this.coinIcon;
}
}
// 有可用图标时才替换节点图片。
private setSprite(node: cc.Node, frame: cc.SpriteFrame) {
if (!node || !frame) return;
const sprite = node.getComponent(cc.Sprite);
if (sprite) sprite.spriteFrame = frame;
}
}