无限体力礼包第一版
This commit is contained in:
@@ -33,6 +33,9 @@ export default class GameManager extends cc.Component {
|
||||
text: string = 'hello';
|
||||
static _instance: GameManager = null;
|
||||
|
||||
@property(cc.Prefab)
|
||||
staminaPackPrefab: cc.Prefab = null;
|
||||
|
||||
@property({ type: [cc.Prefab], tooltip: "方块数组" })
|
||||
Block_Array: Array<cc.Prefab> = [];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { applyRainbowElimination } from "./RainbowCut";
|
||||
// - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html
|
||||
|
||||
import GameManager from "./GameManager";
|
||||
import StaminaPack from "./StaminaPack";
|
||||
import CloudRiseService from "./module/Config/CloudRiseService";
|
||||
import CloudRiseView from "./CloudRiseView";
|
||||
import { LQCollide } from "./lq_collide_system/lq_collide";
|
||||
@@ -5192,7 +5193,8 @@ export default class MapConroler extends cc.Component {
|
||||
if (cc.fx.GameConfig.GM_INFO.hp < 1 && MapConroler._instance.powerState == false) {
|
||||
MiniGameSdk.API.showToast("体力值不足");
|
||||
setTimeout(() => {
|
||||
this.openShop();
|
||||
if (!this.isMapRuntimeAlive()) return;
|
||||
if (!StaminaPack.tryOpen()) this.openShop();
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import GameManager from "./GameManager";
|
||||
import Utils from "./module/Pay/Utils";
|
||||
import { STAMINA_PACK } from "./module/Config/ShopCatalog";
|
||||
import { beginStaminaPackOffer, canShowStaminaPack, closeStaminaPackOffer, getStaminaPackRecord } from "./module/Config/StaminaPackState";
|
||||
import { MiniGameSdk } from "./Sdk/MiniGameSdk";
|
||||
|
||||
declare const wx: any;
|
||||
const { ccclass, property } = cc._decorator;
|
||||
|
||||
@ccclass
|
||||
export default class StaminaPack extends cc.Component {
|
||||
@property(cc.Label) timeLabel: cc.Label = null;
|
||||
@property(cc.Button) buyButton: cc.Button = null;
|
||||
@property(cc.Button) closeButton: cc.Button = null;
|
||||
|
||||
private paying = false;
|
||||
private uid = "";
|
||||
private onShowListener: () => void = null;
|
||||
|
||||
static tryOpen(): boolean {
|
||||
const canvas = cc.find("Canvas");
|
||||
if (!canvas) return false;
|
||||
if (canvas.getChildByName("heathpop2")) return true;
|
||||
if (!canShowStaminaPack()) return false;
|
||||
const prefab = GameManager._instance && GameManager._instance.staminaPackPrefab;
|
||||
if (!prefab) return false;
|
||||
const node = cc.instantiate(prefab);
|
||||
beginStaminaPackOffer();
|
||||
canvas.addChild(node);
|
||||
node.zIndex = 1001;
|
||||
return true;
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this.uid = cc.fx.GameConfig.GM_INFO.uid;
|
||||
this.node.addComponent(cc.BlockInputEvents);
|
||||
this.buyButton.node.on("click", this.buyProduct, this);
|
||||
this.closeButton.node.on("click", this.closePop, this);
|
||||
cc.director.on("stamina-pack-granted", this.closeAfterPurchase, this);
|
||||
this.refreshCountdown();
|
||||
this.schedule(this.refreshCountdown, 1);
|
||||
if (typeof wx !== "undefined") {
|
||||
this.onShowListener = () => this.checkIosOrder();
|
||||
wx.onShow(this.onShowListener);
|
||||
}
|
||||
this.checkIosOrder();
|
||||
}
|
||||
|
||||
private refreshCountdown() {
|
||||
const seconds = Math.max(0, Math.ceil((getStaminaPackRecord().offerEnd - Date.now()) / 1000));
|
||||
this.timeLabel.string = Math.floor(seconds / 60).toString().padStart(2, "0")
|
||||
+ ":" + (seconds % 60).toString().padStart(2, "0");
|
||||
}
|
||||
|
||||
closePop() {
|
||||
cc.fx.AudioManager._instance.playEffect("anniu_little", null);
|
||||
closeStaminaPackOffer();
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
private closeAfterPurchase() {
|
||||
this.node.active = false;
|
||||
this.node.destroy();
|
||||
}
|
||||
|
||||
private setPaying(value: boolean) {
|
||||
this.paying = value;
|
||||
if (cc.isValid(this, true)) this.buyButton.interactable = !value;
|
||||
}
|
||||
|
||||
buyProduct() {
|
||||
if (this.paying) return;
|
||||
cc.fx.AudioManager._instance.playEffect("anniu_little", null);
|
||||
if (cc.fx.GameConfig.GM_INFO.iosStaminaPackOrder) {
|
||||
this.checkIosOrder();
|
||||
return;
|
||||
}
|
||||
const systemType = typeof wx !== "undefined" && wx.getSystemInfoSync().platform === "ios" ? "ios" : "Android";
|
||||
this.setPaying(true);
|
||||
if (systemType === "ios" && !cc.fx.GameConfig.GM_INFO.iosCanPay) {
|
||||
this.iosOldPay();
|
||||
return;
|
||||
}
|
||||
Utils.buyProp(STAMINA_PACK.id, 1, STAMINA_PACK.price, systemType, STAMINA_PACK.id, res => {
|
||||
if (this.uid !== cc.fx.GameConfig.GM_INFO.uid) return;
|
||||
if (res?.errCode === 16) { this.iosOldPay(); return; }
|
||||
if (!res || res.err || !res.outTradeNo) {
|
||||
this.setPaying(false);
|
||||
MiniGameSdk.API.showToast(res?.errCode === -2 ? "取消充值" : "充值失败");
|
||||
return;
|
||||
}
|
||||
const order = res.outTradeNo;
|
||||
Utils.getPayInfo(data => {
|
||||
if (this.uid !== cc.fx.GameConfig.GM_INFO.uid) return;
|
||||
if (data?.code !== 1 || data.data?.pay_state !== 2) {
|
||||
this.setPaying(false);
|
||||
return;
|
||||
}
|
||||
Utils.setPayInfo(result => {
|
||||
if (this.uid !== cc.fx.GameConfig.GM_INFO.uid) return;
|
||||
this.setPaying(false);
|
||||
if (result?.code === 1) this.completePurchase(order);
|
||||
else MiniGameSdk.API.showToast("网络异常,充值奖励将在登录后再次发放");
|
||||
}, order);
|
||||
}, order);
|
||||
});
|
||||
}
|
||||
|
||||
private iosOldPay() {
|
||||
Utils.GoKEFu({ price: STAMINA_PACK.price, payment_name: STAMINA_PACK.id, payment_count: 1 }, result => {
|
||||
if (this.uid !== cc.fx.GameConfig.GM_INFO.uid) return;
|
||||
this.setPaying(false);
|
||||
if (result !== "success") {
|
||||
cc.fx.GameConfig.GM_INFO.iosStaminaPackOrder = "";
|
||||
MiniGameSdk.API.showToast("未能打开支付,请重试");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkIosOrder() {
|
||||
const order = cc.fx.GameConfig.GM_INFO.iosStaminaPackOrder;
|
||||
if (!order || this.paying) return;
|
||||
this.setPaying(true);
|
||||
Utils.getIosPayInfo(order, result => {
|
||||
if (this.uid !== cc.fx.GameConfig.GM_INFO.uid) return;
|
||||
this.setPaying(false);
|
||||
if (result?.code === 1 && result.data?.payment_name === STAMINA_PACK.id) {
|
||||
cc.fx.GameConfig.GM_INFO.iosStaminaPackOrder = "";
|
||||
this.completePurchase(order);
|
||||
} else if (result?.code === 0) {
|
||||
cc.fx.GameConfig.GM_INFO.iosStaminaPackOrder = "";
|
||||
MiniGameSdk.API.showToast(result.msg || "充值失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private completePurchase(order: string) {
|
||||
// 正常发货与登录补单共用 shopBuy,统一去重、计冷却并打开奖励窗。
|
||||
cc.fx.GameTool.shopBuy(STAMINA_PACK.id, false, { outTradeNo: order });
|
||||
MiniGameSdk.API.yinli_Pay(STAMINA_PACK.price, order, "购买金币道具:" + STAMINA_PACK.id);
|
||||
const canvas = cc.find("Canvas");
|
||||
const home = canvas && canvas.getComponent("JiaZai");
|
||||
if (home) { home.updateCoin(); home.setHealthInfo(false); }
|
||||
const scene = canvas && canvas.getComponent("SceneManager");
|
||||
if (scene) scene.refreshJungleRewardDisplay();
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
cc.director.off("stamina-pack-granted", this.closeAfterPurchase, this);
|
||||
if (this.onShowListener) wx.offShow(this.onShowListener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"ver": "1.1.0",
|
||||
"uuid": "6aa5f0f0-c22b-4dee-b311-c9ae0af032c3",
|
||||
"importer": "typescript",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import NumberToImage from "./NumberToImage";
|
||||
import JiaZai from "./JiaZai";
|
||||
import { MiniGameSdk } from "./Sdk/MiniGameSdk";
|
||||
import StaminaPack from "./StaminaPack";
|
||||
|
||||
const HEALTH_REFILL_PRICES = [1000, 2000, 4000, 8000];
|
||||
const HEALTH_REFILL_STORAGE_PREFIX = "home_health_refill_";
|
||||
@@ -30,6 +31,7 @@ export default class NewClass extends cc.Component {
|
||||
coin: cc.Node = null;
|
||||
|
||||
btn_Touch: boolean = true;
|
||||
private boughtHealth = false;
|
||||
|
||||
onLoad() {
|
||||
this.btn_Touch = true;
|
||||
@@ -118,6 +120,7 @@ export default class NewClass extends cc.Component {
|
||||
cc.fx.GameConfig.GM_INFO.hp = cc.fx.GameConfig.GM_INFO.hp_Max;
|
||||
cc.fx.GameConfig.GM_INFO.min_Time = 0;
|
||||
this.recordHealthRefill(refillRecord);
|
||||
this.boughtHealth = true;
|
||||
const parentNode = cc.find("Canvas"); // 假设 JiaZai 挂在 Canvas 节点
|
||||
if (parentNode) {
|
||||
const jiazaiComp = parentNode.getComponent(JiaZai);
|
||||
@@ -184,6 +187,7 @@ export default class NewClass extends cc.Component {
|
||||
console.warn("JiaZai 节点未找到");
|
||||
}
|
||||
this.heath.destroy();
|
||||
if (event && !this.boughtHealth) StaminaPack.tryOpen();
|
||||
}
|
||||
|
||||
// update (dt) {}
|
||||
|
||||
@@ -568,6 +568,7 @@ export class GameConfig {
|
||||
otherLevel: number; //其他被帮助用户等级
|
||||
helpLevel: number; //帮助用户等级
|
||||
iosShopOrder: string; //ios订单号商城
|
||||
iosStaminaPackOrder: string;
|
||||
iosJungleOrder: string; //iOS 客服旧支付的丛林宝藏订单号
|
||||
iosMonthOrder: string; //ios订单号月卡
|
||||
iosReviveOrder: string; //ios订单号复活
|
||||
@@ -770,7 +771,7 @@ export class GameConfig {
|
||||
vibrateOpen: true, //震动
|
||||
coinnum: 0, //每局的金币数
|
||||
paid_user: false, //是否是付费用户
|
||||
version: "1.0.82", //版本号
|
||||
version: "1.0.84", //版本号
|
||||
shushu_DistinctId: "", //数数访客ID
|
||||
shushu_AccountId: "", //数数账号ID
|
||||
uid: "", //用户和后端唯一id
|
||||
@@ -788,6 +789,7 @@ export class GameConfig {
|
||||
otherLevel: 0, //其他被帮助用户等级
|
||||
helpLevel: 0, //帮助用户等级
|
||||
iosShopOrder: "", //ios订单号商城
|
||||
iosStaminaPackOrder: "",
|
||||
iosJungleOrder: "", //iOS 客服旧支付的丛林宝藏订单号
|
||||
iosMonthOrder: "", //ios订单号月卡
|
||||
iosReviveOrder: "", //ios订单号复活
|
||||
|
||||
@@ -35,7 +35,10 @@ export const SHOP_B_BUNDLES: ShopProduct[] = [
|
||||
{ id: "new_unlimited_health_bundle_98", price: 9800, coin: 20000, hammer: 25, freeze: 25, magic: 25, infiniteHealth: 43200 },
|
||||
];
|
||||
|
||||
export const STAMINA_PACK: ShopProduct = { id: "StaminaPack", price: 800, coin: 1200, infiniteHealth: 3600 };
|
||||
|
||||
// 按订单商品 ID 查奖励,补单不能使用玩家当前的 AB 分组决定数值。
|
||||
export function getNewShopProduct(id: string): ShopProduct {
|
||||
if (id === STAMINA_PACK.id) return STAMINA_PACK;
|
||||
return SHOP_B_GOLD.concat(SHOP_B_BUNDLES).find(product => product.id === id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
const MINUTE = 60 * 1000;
|
||||
|
||||
export interface StaminaPackRecord {
|
||||
offerEnd: number;
|
||||
cooldownEnd: number;
|
||||
}
|
||||
|
||||
function storageKey(): string {
|
||||
return "StaminaPack_" + (cc.fx.GameConfig.GM_INFO.uid || "guest");
|
||||
}
|
||||
|
||||
export function getStaminaPackRecord(): StaminaPackRecord {
|
||||
return cc.fx.StorageMessage.getStorage(storageKey()) || { offerEnd: 0, cooldownEnd: 0 };
|
||||
}
|
||||
|
||||
export function canShowStaminaPack(now = Date.now()): boolean {
|
||||
const record = getStaminaPackRecord();
|
||||
return record.offerEnd > now || record.cooldownEnd <= now;
|
||||
}
|
||||
|
||||
export function beginStaminaPackOffer(now = Date.now()): void {
|
||||
const record = getStaminaPackRecord();
|
||||
if (record.offerEnd > now || record.cooldownEnd > now) return;
|
||||
cc.fx.StorageMessage.setStorage(storageKey(), {
|
||||
offerEnd: now + 10 * MINUTE,
|
||||
cooldownEnd: now + 40 * MINUTE,
|
||||
});
|
||||
}
|
||||
|
||||
export function closeStaminaPackOffer(now = Date.now()): void {
|
||||
const record = getStaminaPackRecord();
|
||||
// 发货后 offerEnd 为 0,不能被关闭回调覆盖八小时冷却。
|
||||
if (record.offerEnd > 0 && now >= record.offerEnd) {
|
||||
cc.fx.StorageMessage.setStorage(storageKey(), { ...record, cooldownEnd: now + 30 * MINUTE });
|
||||
}
|
||||
}
|
||||
|
||||
export function recordStaminaPackPurchase(now = Date.now()): void {
|
||||
cc.fx.StorageMessage.setStorage(storageKey(), { offerEnd: 0, cooldownEnd: now + 8 * 60 * MINUTE });
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"ver": "1.1.0",
|
||||
"uuid": "1034f13d-c1e0-4aa6-93c4-5f8e762f35e5",
|
||||
"importer": "typescript",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
||||
@@ -394,6 +394,9 @@ export default class Utils {
|
||||
else if (/^gold_miner(?:_\d+)?$/.test(String(iosPayInfo.payment_name || ""))) {
|
||||
// The activity persists its order separately; do not overwrite shop orders.
|
||||
}
|
||||
else if (iosPayInfo.payment_name === "StaminaPack") {
|
||||
cc.fx.GameConfig.GM_INFO.iosStaminaPackOrder = order;
|
||||
}
|
||||
else {
|
||||
cc.fx.GameConfig.GM_INFO.iosShopOrder = null;
|
||||
cc.fx.GameConfig.GM_INFO.iosShopOrder = order;
|
||||
|
||||
@@ -7,6 +7,7 @@ import SceneManager from "../../SceneManager";
|
||||
import { MiniGameSdk } from "../../Sdk/MiniGameSdk";
|
||||
import Utils from "../Pay/Utils";
|
||||
import { getNewShopProduct } from "../Config/ShopCatalog";
|
||||
import { recordStaminaPackPurchase } from "../Config/StaminaPackState";
|
||||
import BlockAssetManager from "./BlockAssetManager";
|
||||
import HomePopupQueue from "../../../home_popup_queue/HomePopupQueue";
|
||||
import {
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
//@ts-ignore
|
||||
//最大工具类 各种公共方法,以及处理上传,获取后端接口数据
|
||||
var GameTool = {
|
||||
MAX_NORMAL_LEVEL: 2000,
|
||||
MAX_NORMAL_LEVEL: 2020,
|
||||
starterPackReactivationUid: "",
|
||||
starterPackQueuedReason: "",
|
||||
MEMORY_WARNING_EVENT: "game-memory-warning",
|
||||
@@ -1073,6 +1074,10 @@ var GameTool = {
|
||||
rewardData.push({ type: "infinite_health", count: newProduct.infiniteHealth });
|
||||
}
|
||||
if (newOrderKey) cc.fx.StorageMessage.setStorage(newOrderKey, true);
|
||||
if (productId === "StaminaPack") {
|
||||
recordStaminaPackPurchase();
|
||||
cc.director.emit("stamina-pack-granted");
|
||||
}
|
||||
if (shopComp) {
|
||||
shopComp.initCoin();
|
||||
shopComp.refreshJungleRewardDisplay();
|
||||
|
||||
@@ -163,6 +163,9 @@
|
||||
"__id__": 16
|
||||
},
|
||||
"text": "hello",
|
||||
"staminaPackPrefab": {
|
||||
"__uuid__": "1cf82c85-a0c6-42f7-8a9d-e7ed164806d2"
|
||||
},
|
||||
"Block_Array": [],
|
||||
"Wall_Prefab": [],
|
||||
"_id": "de33dd/4JKb4L0+kEatmTv"
|
||||
|
||||
@@ -64,10 +64,13 @@
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 56
|
||||
},
|
||||
{
|
||||
"__id__": 57
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 57
|
||||
"__id__": 58
|
||||
},
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
@@ -1348,7 +1351,7 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 48,
|
||||
"width": 44,
|
||||
"height": 50
|
||||
},
|
||||
"_anchorPoint": {
|
||||
@@ -1401,7 +1404,7 @@
|
||||
"_srcBlendFactor": 770,
|
||||
"_dstBlendFactor": 771,
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "933f0942-c2ec-4c1e-b113-0183547628af"
|
||||
"__uuid__": "bc900d16-5289-4513-afcf-7fe0973dc79e"
|
||||
},
|
||||
"_type": 0,
|
||||
"_sizeMode": 1,
|
||||
@@ -2092,6 +2095,25 @@
|
||||
"_originalHeight": 1920,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "6aa5fDwwitN7rMRya4K8DLD",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"timeLabel": {
|
||||
"__id__": 19
|
||||
},
|
||||
"buyButton": {
|
||||
"__id__": 53
|
||||
},
|
||||
"closeButton": {
|
||||
"__id__": 12
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "1.1.3",
|
||||
"uuid": "d8e7122e-869b-4bf7-9676-50bb2ebe08b9",
|
||||
"uuid": "4d9cb8fa-c6ad-47b5-ad32-2fb2a7ca3f3f",
|
||||
"importer": "folder",
|
||||
"isBundle": false,
|
||||
"bundleName": "",
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const { loadMembers, loadModule } = require('./new-player-ab-test-support.cjs');
|
||||
const catalog = loadModule('assets/Script/module/Config/ShopCatalog.ts');
|
||||
const minute = 60000;
|
||||
const noon = Date.UTC(2026, 9, 12, 12);
|
||||
const plain = value => JSON.parse(JSON.stringify(value));
|
||||
|
||||
function fixture() {
|
||||
let now = noon;
|
||||
const storage = new Map(), listeners = new Map();
|
||||
const calls = { coins: [], health: [], rewards: [], paid: [], ios: [], acknowledgements: [] };
|
||||
const info = { uid: 'player', iosCanPay: true, iosStaminaPackOrder: '', shopDouble: {} };
|
||||
const cc = {
|
||||
fx: { GameConfig: { GM_INFO: info }, AudioManager: { _instance: { playEffect() {} } }, StorageMessage: {
|
||||
getStorage: key => storage.get(key), setStorage: (key, value) => storage.set(key, plain(value)),
|
||||
} },
|
||||
isValid: value => !!value && !value.destroyed,
|
||||
find: () => null,
|
||||
director: { emit: event => { const cb = listeners.get(event); if (cb) cb(); } },
|
||||
};
|
||||
const DateMock = { now: () => now };
|
||||
const state = loadModule('assets/Script/module/Config/StaminaPackState.ts', {}, { cc, Date: DateMock });
|
||||
const sdk = { API: { showToast() {}, yinli_Pay() {}, shushu_SetSuperProperties() {} } };
|
||||
const Tool = loadMembers('assets/Script/module/Tool/GameTool.ts', ['shopBuy'], {
|
||||
cc, ...catalog, ...state, MiniGameSdk: sdk,
|
||||
MapConroler: { _instance: { SceneManager: { openRewardWindow: data => calls.rewards.push(plain(data)) } } },
|
||||
});
|
||||
cc.fx.GameTool = Object.assign(new Tool(), {
|
||||
changeCoin: n => calls.coins.push(n), setUserPowerTime: (...args) => calls.health.push(args),
|
||||
shushu_Track() {}, tryReactivateStarterPack() {},
|
||||
});
|
||||
const Utils = {
|
||||
buyProp: (...args) => { calls.paid.push(args.slice(0, 5)); calls.pay = args[5]; },
|
||||
getPayInfo: (cb, order) => { calls.polledOrder = order; calls.poll = cb; },
|
||||
setPayInfo: (cb, order) => { calls.acknowledgements.push(order); calls.ack = cb; },
|
||||
GoKEFu: (data, cb) => { calls.ios.push(data); info.iosStaminaPackOrder = 'ios-order'; cb('success'); },
|
||||
getIosPayInfo: (order, cb) => { calls.polledOrder = order; calls.iosPoll = cb; },
|
||||
};
|
||||
const Panel = loadMembers('assets/Script/StaminaPack.ts', [
|
||||
'paying', 'uid', 'setPaying', 'refreshCountdown', 'closePop', 'closeAfterPurchase',
|
||||
'buyProduct', 'iosOldPay', 'checkIosOrder', 'completePurchase', 'tryOpen',
|
||||
], { cc, ...catalog, ...state, MiniGameSdk: sdk, Utils, Date: DateMock,
|
||||
wx: { getSystemInfoSync: () => ({ platform: calls.platform || 'android' }) },
|
||||
GameManager: { _instance: { staminaPackPrefab: {} } },
|
||||
});
|
||||
const panel = Object.assign(new Panel(), {
|
||||
uid: info.uid, buyButton: { interactable: true }, timeLabel: {},
|
||||
node: { active: true, destroy() { this.destroyed = true; } },
|
||||
});
|
||||
listeners.set('stamina-pack-granted', () => panel.closeAfterPurchase());
|
||||
return { state, info, calls, cc, panel, Panel, storage, setTime: t => { now = t; } };
|
||||
}
|
||||
|
||||
test('first display starts 10-minute offer; reopening preserves deadline and survives module reload', () => {
|
||||
const f = fixture();
|
||||
assert.equal(f.state.canShowStaminaPack(), true);
|
||||
f.state.beginStaminaPackOffer();
|
||||
f.setTime(noon + 5 * minute);
|
||||
f.state.beginStaminaPackOffer();
|
||||
assert.deepEqual(plain(f.state.getStaminaPackRecord()), { offerEnd: noon + 10 * minute, cooldownEnd: noon + 40 * minute });
|
||||
const reloaded = loadModule('assets/Script/module/Config/StaminaPackState.ts', {}, { cc: f.cc });
|
||||
assert.equal(reloaded.canShowStaminaPack(noon + 9 * minute), true);
|
||||
assert.equal(reloaded.canShowStaminaPack(noon + 10 * minute), false);
|
||||
});
|
||||
|
||||
test('early close preserves timestamps; late close replaces cooldown with close time plus 30 minutes', () => {
|
||||
const f = fixture(); f.state.beginStaminaPackOffer();
|
||||
f.state.closeStaminaPackOffer(noon + 5 * minute);
|
||||
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 40 * minute);
|
||||
f.state.closeStaminaPackOffer(noon + 25 * minute);
|
||||
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 55 * minute);
|
||||
assert.equal(f.state.canShowStaminaPack(noon + 54 * minute), false);
|
||||
assert.equal(f.state.canShowStaminaPack(noon + 55 * minute), true);
|
||||
});
|
||||
|
||||
test('after an absent close, the preset cooldown expires; next actual trigger starts a fresh offer', () => {
|
||||
const f = fixture(); f.state.beginStaminaPackOffer();
|
||||
f.setTime(noon + 60 * minute);
|
||||
assert.equal(f.state.canShowStaminaPack(), true);
|
||||
f.state.beginStaminaPackOffer();
|
||||
assert.equal(f.state.getStaminaPackRecord().offerEnd, noon + 70 * minute);
|
||||
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 100 * minute);
|
||||
});
|
||||
|
||||
test('zero countdown leaves the visible offer purchasable', () => {
|
||||
const f = fixture(); f.state.beginStaminaPackOffer(); f.setTime(noon + 25 * minute);
|
||||
f.panel.refreshCountdown();
|
||||
assert.equal(f.panel.timeLabel.string, '00:00');
|
||||
assert.equal(f.panel.node.destroyed, undefined);
|
||||
f.panel.buyProduct();
|
||||
assert.deepEqual(plain(f.calls.paid), [['StaminaPack', 1, 800, 'Android', 'StaminaPack']]);
|
||||
});
|
||||
|
||||
test('native payment waits for acknowledgement, then closes before RewardWindow and clears offer time', () => {
|
||||
const f = fixture(); f.state.beginStaminaPackOffer();
|
||||
f.panel.buyProduct(); f.panel.buyProduct();
|
||||
assert.equal(f.calls.paid.length, 1);
|
||||
assert.equal(f.calls.coins.length, 0);
|
||||
f.calls.pay({ outTradeNo: 'order-1' });
|
||||
assert.equal(f.calls.polledOrder, 'order-1');
|
||||
f.calls.poll({ code: 1, data: { pay_state: 2 } });
|
||||
assert.deepEqual(f.calls.acknowledgements, ['order-1']);
|
||||
assert.equal(f.calls.coins.length, 0);
|
||||
f.setTime(noon + 11 * minute);
|
||||
f.calls.ack({ code: 1 });
|
||||
assert.equal(f.panel.node.active, false);
|
||||
assert.equal(f.panel.node.destroyed, true);
|
||||
assert.deepEqual(f.calls.coins, [1200]);
|
||||
assert.deepEqual(f.calls.health, [[3600, 'StaminaPack', false]]);
|
||||
assert.deepEqual(f.calls.rewards, [[{ type: 'coin', count: 1200 }, { type: 'infinite_health', count: 3600 }]]);
|
||||
assert.deepEqual(plain(f.state.getStaminaPackRecord()), { offerEnd: 0, cooldownEnd: noon + 491 * minute });
|
||||
f.state.closeStaminaPackOffer(noon + 12 * minute);
|
||||
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 491 * minute);
|
||||
});
|
||||
|
||||
test('compensation starts eight hours at grant, duplicate grant cannot extend cooldown or grant twice', () => {
|
||||
const f = fixture(); f.setTime(noon + 60 * minute);
|
||||
f.cc.fx.GameTool.shopBuy('StaminaPack', true, { outTradeNo: 'pending' });
|
||||
f.setTime(noon + 120 * minute);
|
||||
f.cc.fx.GameTool.shopBuy('StaminaPack', true, { outTradeNo: 'pending' });
|
||||
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 540 * minute);
|
||||
assert.deepEqual(f.calls.coins, [1200]);
|
||||
assert.deepEqual(f.calls.health, [[3600, 'StaminaPack', true]]);
|
||||
assert.equal(f.calls.rewards.length, 1);
|
||||
assert.equal(f.state.canShowStaminaPack(noon + 539 * minute), false);
|
||||
assert.equal(f.state.canShowStaminaPack(noon + 540 * minute), true);
|
||||
});
|
||||
|
||||
test('cancel, unconfirmed payment and failed acknowledgement never start purchase cooldown', () => {
|
||||
for (const stage of ['cancel', 'unconfirmed', 'ack']) {
|
||||
const f = fixture(); f.state.beginStaminaPackOffer(); f.panel.buyProduct();
|
||||
if (stage === 'cancel') f.calls.pay({ err: true, errCode: -2 });
|
||||
else {
|
||||
f.calls.pay({ outTradeNo: 'failed' });
|
||||
f.calls.poll({ code: 1, data: { pay_state: stage === 'ack' ? 2 : 1 } });
|
||||
if (stage === 'ack') f.calls.ack({ code: 0 });
|
||||
}
|
||||
assert.equal(f.state.getStaminaPackRecord().offerEnd, noon + 10 * minute);
|
||||
assert.equal(f.calls.coins.length, 0);
|
||||
assert.equal(f.panel.buyButton.interactable, true);
|
||||
}
|
||||
});
|
||||
|
||||
test('closing before acknowledgement does not lose delivery or overwrite its eight-hour cooldown', () => {
|
||||
const f = fixture(); f.state.beginStaminaPackOffer(); f.panel.buyProduct();
|
||||
f.calls.pay({ outTradeNo: 'late' }); f.calls.poll({ code: 1, data: { pay_state: 2 } });
|
||||
f.setTime(noon + 25 * minute); f.panel.closePop();
|
||||
f.setTime(noon + 26 * minute); f.calls.ack({ code: 1 });
|
||||
assert.equal(f.state.getStaminaPackRecord().cooldownEnd, noon + 506 * minute);
|
||||
assert.deepEqual(f.calls.coins, [1200]);
|
||||
});
|
||||
|
||||
test('legacy iOS uses existing customer service and query; success grants the same product', () => {
|
||||
const f = fixture(); f.calls.platform = 'ios'; f.info.iosCanPay = false;
|
||||
f.state.beginStaminaPackOffer(); f.panel.buyProduct();
|
||||
assert.deepEqual(plain(f.calls.ios), [{ price: 800, payment_name: 'StaminaPack', payment_count: 1 }]);
|
||||
f.panel.checkIosOrder(); f.panel.checkIosOrder();
|
||||
f.calls.iosPoll({ code: 1, data: { payment_name: 'StaminaPack' } });
|
||||
assert.equal(f.info.iosStaminaPackOrder, '');
|
||||
assert.deepEqual(f.calls.coins, [1200]);
|
||||
assert.equal(f.state.getStaminaPackRecord().offerEnd, 0);
|
||||
});
|
||||
|
||||
test('a changed account ignores the outstanding purchase callback', () => {
|
||||
const f = fixture(); f.panel.buyProduct(); f.info.uid = 'other';
|
||||
f.calls.pay({ outTradeNo: 'old-account' });
|
||||
assert.equal(f.calls.polledOrder, undefined);
|
||||
assert.equal(f.calls.coins.length, 0);
|
||||
});
|
||||
|
||||
test('home close triggers only without successful coin refill; programmatic start-game close does not', () => {
|
||||
let opened = 0;
|
||||
const cc = { find: () => null, fx: { AudioManager: { _instance: { playEffect() {} } } } };
|
||||
const Health = loadMembers('assets/Script/heathnum.ts', ['boughtHealth', 'closePop'], {
|
||||
cc, StaminaPack: { tryOpen: () => opened++ }, console: { warn() {} },
|
||||
});
|
||||
const h = new Health(); h.heath = { destroy() {} };
|
||||
h.closePop(); assert.equal(opened, 0);
|
||||
h.closePop({}); assert.equal(opened, 1);
|
||||
h.boughtHealth = true; h.closePop({}); assert.equal(opened, 1);
|
||||
});
|
||||
|
||||
test('game retry keeps toast, selects pack when eligible, otherwise uses the original shop', async () => {
|
||||
for (const eligible of [true, false]) {
|
||||
let shops = 0, packs = 0, toasts = 0;
|
||||
const cc = { fx: { AudioManager: { _instance: { playEffect() {} } },
|
||||
GameTool: { getUserPowerTime: () => false }, GameConfig: { GM_INFO: { hp: 0 } } } };
|
||||
const MapConroler = { _instance: { powerState: false } };
|
||||
const Map = loadMembers('assets/Script/Map.ts', ['againLevel'], {
|
||||
cc, MapConroler, StaminaPack: { tryOpen() { packs++; return eligible; } },
|
||||
MiniGameSdk: { API: { showToast: () => toasts++ } }, setTimeout: cb => cb(),
|
||||
});
|
||||
const map = Object.assign(new Map(), { isMapRuntimeAlive: () => true, openShop: () => shops++ });
|
||||
await map.againLevel();
|
||||
assert.equal(toasts, 1); assert.equal(packs, 1); assert.equal(shops, eligible ? 0 : 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('prefab bindings resolve the authored countdown/buttons and startup references the prefab', () => {
|
||||
const prefab = JSON.parse(fs.readFileSync('assets/prefab/pop/heathpop2.prefab'));
|
||||
const component = prefab.find(v => v.timeLabel && v.buyButton && v.closeButton);
|
||||
assert.equal(prefab[component.timeLabel.__id__].__type__, 'cc.Label');
|
||||
assert.equal(prefab[component.buyButton.__id__].__type__, 'cc.Button');
|
||||
assert.equal(prefab[prefab[component.closeButton.__id__].node.__id__]._name, 'close');
|
||||
const scene = JSON.parse(fs.readFileSync('assets/StartScene/LoadScene.fire'));
|
||||
const meta = JSON.parse(fs.readFileSync('assets/prefab/pop/heathpop2.prefab.meta'));
|
||||
assert.equal(scene.find(v => v.staminaPackPrefab).staminaPackPrefab.__uuid__, meta.uuid);
|
||||
});
|
||||
Reference in New Issue
Block a user