MatchMaster/assets/seven_day_gift/SevenDayGift.ts

359 lines
16 KiB
TypeScript
Raw Permalink 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 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");
const cards = this.panel && this.panel.getChildByName("cards");
if (!this.panel || !this.mask || !cards) {
cc.error("[SevenDayGift] prefab structure is incomplete");
return 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);
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, available || claimed);
if (special) {
const glow = card.getChildByName("catGlow");
glow.active = !claimed && items.some(item => item.type === "cat_skin");
const animation = glow.getComponent(cc.Animation);
if (glow.active) {
const state = animation.getAnimationState("CatRewardGlow");
if (!state || !state.isPlaying) animation.play("CatRewardGlow");
} else {
animation.stop();
}
}
// 保留原卡片,遮罩压暗日期和奖励,印章绘制在遮罩上方。
const overlay = card.getChildByName("claimedOverlay");
overlay.active = claimed;
const badge = card.getChildByName("claimedBadge");
badge.active = claimed;
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;
}
// 从对应道具位置生成图标和数量,播放期间锁住交互。
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[], unlocked: boolean) {
const container = card.getChildByName("rewardItems");
if (items.length > container.childrenCount) cc.warn("[SevenDayGift] 奖励数量超出预制体槽位,请更新预制体");
container.children.forEach((icon, index) => {
const item = items[index];
icon.active = !!item;
if (!item) return;
this.setSprite(icon, this.getRewardIcon(item, unlocked));
this.refreshAmount(icon, item);
});
}
// 只更新预制体中已有的图片字位,无文字兜底。
private refreshAmount(icon: cc.Node, item: SevenDayRewardItem) {
const amount = icon.getChildByName("amount");
const count = Math.max(0, Number(item.count !== undefined ? item.count : item.amount) || 0);
const isTime = item.type === "infinite_health";
const prefix = isTime || item.type === "coin" ? "" : "×";
const value = isTime ? Number((count / 60).toFixed(2)) : count;
const text = prefix + value;
const glyphs = text.split("").map(char => this.rewardNumberFrames[char === "×" ? 11 : Number(char)]);
if (isTime) glyphs.push(this.rewardNumberFrames[12]);
const useImages = /^×?\d+$/.test(text) && glyphs.length <= amount.childrenCount && glyphs.every(Boolean);
amount.children.forEach((digit, index) => digit.active = useImages && index < glyphs.length);
if (!useImages) {
cc.warn("[SevenDayGift] 数量图片或字位不足,请更新预制体:" + text);
return;
}
const numberFrames = isTime ? glyphs.slice(0, -1) : glyphs;
const scale = amount.height / Math.max(...numberFrames.map(frame => frame.getOriginalSize().height));
// 分钟贴图原始高度较小,单独按数字的可见高度等比放大。
const numberHeight = Math.max(...numberFrames.map(frame => frame.getRect().height)) * scale;
const scales = glyphs.map((frame, index) => isTime && index === glyphs.length - 1
? numberHeight / frame.getRect().height : scale);
const widths = glyphs.map((frame, index) => frame.getOriginalSize().width * scales[index]);
const total = widths.reduce((sum, width) => sum + width, 0) + glyphs.length - 1;
const fit = Math.min(1, amount.width / total);
let x = -total / 2;
glyphs.forEach((frame, index) => {
const digit = amount.getChildByName("digit" + index);
this.setSprite(digit, frame);
digit.setContentSize(widths[index] * fit, frame.getOriginalSize().height * scales[index] * fit);
digit.setPosition((x + widths[index] / 2) * fit, 0);
x += widths[index] + 1;
});
}
// 将奖励类型映射为活动图标,猫皮肤区分锁定状态。
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;
}
}