无限体力礼包第一版

This commit is contained in:
COMPUTER\EDY
2026-10-12 17:44:35 +08:00
parent 8e5c271302
commit 5241738241
15 changed files with 476 additions and 7 deletions
+3
View File
@@ -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> = [];
+3 -1
View File
@@ -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;
}
+152
View File
@@ -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);
}
}
+10
View File
@@ -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": {}
}
+4
View File
@@ -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) {}
+3 -1
View File
@@ -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": {}
}
+3
View File
@@ -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;
+6 -1
View File
@@ -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();
+3
View File
@@ -163,6 +163,9 @@
"__id__": 16
},
"text": "hello",
"staminaPackPrefab": {
"__uuid__": "1cf82c85-a0c6-42f7-8a9d-e7ed164806d2"
},
"Block_Array": [],
"Wall_Prefab": [],
"_id": "de33dd/4JKb4L0+kEatmTv"
+25 -3
View File
@@ -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 -1
View File
@@ -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": "",
+210
View File
@@ -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);
});