diff --git a/assets/Script/JiaZai.ts b/assets/Script/JiaZai.ts index c455ce7..83c2188 100644 --- a/assets/Script/JiaZai.ts +++ b/assets/Script/JiaZai.ts @@ -3947,6 +3947,8 @@ export default class JiaZai extends cc.Component { // 3. 处理月卡过期逻辑 // ============================================ rewarded() { + const canUpdateHome = () => this.isHomeRuntimeAlive() && !cc.fx.GameConfig.isGameplayEntryPreparing(); + if (!canUpdateHome()) return; if (this.deferHomePopupWhileTransfer("monthlyReward", () => this.rewarded())) { return; } @@ -3962,6 +3964,7 @@ export default class JiaZai extends cc.Component { }) } setTimeout(() => { + if (!canUpdateHome()) return; this.updateCoin(); this.setHealthInfo(false); this.startTimeCutDown(); @@ -3973,7 +3976,22 @@ export default class JiaZai extends cc.Component { // console.log("未到期已领取"); } if (data.code == 1) { + // 奖励处理独立于弹窗,主页销毁或队列尚未执行也不能漏发。 + cc.fx.GameTool.changeCoin(500); + const dataTemp = { + change_reason: "month", + id: "1001", + num: 500, + compensate: false + } + cc.fx.GameTool.shushu_Track("resource_get", dataTemp); + if (!canUpdateHome()) return; + this.updateCoin(); HomePopupQueue.get(this.node).enqueue("monthlyReward", 20, done => { + if (!canUpdateHome() || !this.getcard || !cc.isValid(this.getcard, true) + || !this.cardTime || !cc.isValid(this.cardTime, true)) { done(); return; } + const pauseNode = this.getcard.getChildByName("mcReward"); + if (!pauseNode || !cc.isValid(pauseNode, true)) { done(); return; } this.getcard.active = true; this.getcard.zIndex = 999; // 外层 getcard 整体弹入动画(三次回弹) @@ -3983,7 +4001,6 @@ export default class JiaZai extends cc.Component { .to(0.08, { scale: 0.95 }, { easing: 'sineIn' }) .to(0.08, { scale: 1.0 }, { easing: 'sineOut' }) .start(); - let pauseNode = this.getcard.getChildByName("mcReward"); pauseNode.scale = 0.3; cc.tween(pauseNode) .to(0.2, { scale: 1.05 }, { easing: 'backOut' }) @@ -3993,15 +4010,6 @@ export default class JiaZai extends cc.Component { for (let i = 0; i < this.cardTime.children.length; i++) { this.cardTime.children[i].color = cc.Color.RED; } - cc.fx.GameTool.changeCoin(500); - this.updateCoin(); - const dataTemp = { - change_reason: "month", - id: "1001", - num: 500, - compensate: false - } - cc.fx.GameTool.shushu_Track("resource_get", dataTemp); done(); }); } @@ -4018,6 +4026,8 @@ export default class JiaZai extends cc.Component { // 2. 使用缓存控制每天只弹一次月卡弹窗 // ============================================ monthH() { + const canUpdateHome = () => this.isHomeRuntimeAlive() && !cc.fx.GameConfig.isGameplayEntryPreparing(); + if (!canUpdateHome()) return; if (cc.fx.GameConfig.isNewPlayerB() && cc.fx.GameConfig.GM_INFO.level < 20) return; if (this.deferHomePopupWhileTransfer("monthlyCard", () => this.monthH())) { return; @@ -4025,6 +4035,7 @@ export default class JiaZai extends cc.Component { // return; //如果没有充值月卡,每天弹一次月卡弹窗用缓存实现 Utils.getMonthlyCard((data) => { + if (!canUpdateHome()) return; if (data.msg == "不在有效期") { let date = new Date(); let day = date.getDate(); // 当前日期(1-31) @@ -4038,6 +4049,7 @@ export default class JiaZai extends cc.Component { this.winStreakShow = false; // 更新缓存 HomePopupQueue.get(this.node).enqueue("monthlyCard", 30, done => { + if (!canUpdateHome()) { done(); return; } this.openMonthCard(); if (this.monthlyCardNode && this.monthlyCardNode.active) cc.fx.StorageMessage.setStorage("monthlyCardDate", dateStr); done(); diff --git a/assets/UI/UI/newUI.png.meta b/assets/UI/UI/newUI.png.meta index 120b96e..cc1d4e0 100644 --- a/assets/UI/UI/newUI.png.meta +++ b/assets/UI/UI/newUI.png.meta @@ -8,8 +8,8 @@ "premultiplyAlpha": false, "genMipmaps": false, "packable": true, - "width": 1084, - "height": 807, + "width": 782, + "height": 1024, "platformSettings": { "minigame": { "formats": [] diff --git a/assets/UI/UI/start/startUI2.png.meta b/assets/UI/UI/start/startUI2.png.meta index 583cb7c..b4bf140 100644 --- a/assets/UI/UI/start/startUI2.png.meta +++ b/assets/UI/UI/start/startUI2.png.meta @@ -8,8 +8,8 @@ "premultiplyAlpha": false, "genMipmaps": false, "packable": true, - "width": 547, - "height": 972, + "width": 957, + "height": 544, "platformSettings": {}, "subMetas": {} } \ No newline at end of file diff --git a/assets/jungle_treasure/prefab/jungleOver.prefab b/assets/jungle_treasure/prefab/jungleOver.prefab index 968a918..f868b79 100644 --- a/assets/jungle_treasure/prefab/jungleOver.prefab +++ b/assets/jungle_treasure/prefab/jungleOver.prefab @@ -999,8 +999,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 140, - "height": 144 + "width": 277, + "height": 308 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -1018,8 +1018,8 @@ 0, 0, 1, - 1, - 1, + 0.5, + 0.5, 1 ] }, @@ -1052,7 +1052,7 @@ "_srcBlendFactor": 770, "_dstBlendFactor": 771, "_spriteFrame": { - "__uuid__": "f6fd677c-4c67-40a6-97ba-1a6154d25b4d" + "__uuid__": "28d2a4a2-6112-4092-af82-ac2da3393401" }, "_type": 0, "_sizeMode": 1, @@ -1365,8 +1365,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 118, - "height": 141 + "width": 250, + "height": 292 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -1384,8 +1384,8 @@ 0, 0, 1, - 1, - 1, + 0.5, + 0.5, 1 ] }, @@ -1418,7 +1418,7 @@ "_srcBlendFactor": 770, "_dstBlendFactor": 771, "_spriteFrame": { - "__uuid__": "e3910b71-bd4f-4861-8092-0597ed9adffb" + "__uuid__": "ac60b2a5-c833-400a-b640-6eb478c96f1f" }, "_type": 0, "_sizeMode": 1, @@ -1731,8 +1731,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 133, - "height": 136 + "width": 264, + "height": 279 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -1750,8 +1750,8 @@ 0, 0, 1, - 1, - 1, + 0.5, + 0.5, 1 ] }, @@ -1784,7 +1784,7 @@ "_srcBlendFactor": 770, "_dstBlendFactor": 771, "_spriteFrame": { - "__uuid__": "cb2834e3-8bfa-4c1e-9558-115a065076d6" + "__uuid__": "88e305a3-bb71-410b-b067-7748e9696122" }, "_type": 0, "_sizeMode": 1, diff --git a/assets/pause/texture.meta b/assets/pause/texture.meta deleted file mode 100644 index 429c33b..0000000 --- a/assets/pause/texture.meta +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ver": "1.1.3", - "uuid": "ccd7701f-585a-411b-a8ef-a9f03f22189f", - "importer": "folder", - "isBundle": false, - "bundleName": "", - "priority": 1, - "compressionType": {}, - "optimizeHotUpdate": {}, - "inlineSpriteFrames": {}, - "isRemoteBundle": {}, - "subMetas": {} -} \ No newline at end of file diff --git a/tools/test-monthly-card-lifecycle.cjs b/tools/test-monthly-card-lifecycle.cjs new file mode 100644 index 0000000..7a54169 --- /dev/null +++ b/tools/test-monthly-card-lifecycle.cjs @@ -0,0 +1,150 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const ts = require(process.env.TYPESCRIPT_PATH || 'typescript'); + +const root = path.resolve(__dirname, '..'); +const read = file => fs.readFileSync(path.join(root, file), 'utf8'); +const source = ts.createSourceFile('JiaZai.ts', read('assets/Script/JiaZai.ts'), ts.ScriptTarget.Latest, true); +const homeClass = source.statements.find(ts.isClassDeclaration); +const methods = ['isHomeRuntimeAlive', 'rewarded', 'monthH'].map(name => { + const member = homeClass.members.find(item => item.name?.getText(source) === name); + assert.ok(member, name); + return member.getText(source); +}).join('\n'); + +function compile(code, context) { + return vm.runInContext(ts.transpileModule(code, { compilerOptions: { + target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS, experimentalDecorators: true, + } }).outputText, context); +} + +function fixture() { + const pending = {}, timers = [], stored = new Map(), tracked = []; + const state = { scene: 'HomeScene', preparing: false, coinRefreshes: 0, healthRefreshes: 0, opened: 0, wins: 0, healthWrites: 0 }; + const info = { level: 30, coin: 100, hp: 8, hp_Max: 8, monthTime: 10 }; + const node = () => ({ destroyed: false, active: false, children: [], getChildByName: () => null }); + const cc = { + _decorator: { ccclass: value => value }, Component: class {}, + isValid: value => !!value && !value.destroyed, + director: { getScene: () => ({ name: state.scene }) }, Color: { RED: 'red' }, + tween: () => { const tween = { to: () => tween, start: () => tween }; return tween; }, + fx: { + GameConfig: { GM_INFO: info, isNewPlayerB: () => false, isGameplayEntryPreparing: () => state.preparing }, + StorageMessage: { getStorage: key => stored.get(key), setStorage: (key, value) => stored.set(key, value) }, + GameTool: { + changeCoin: amount => { info.coin += amount; }, + shushu_Track: (...args) => tracked.push(args), + setUserHealth: (_, callback) => { state.healthWrites++; callback({}); }, + getHealth: () => {}, + }, + }, + }; + const context = vm.createContext({ + exports: {}, cc, setTimeout: callback => timers.push(callback), + Utils: { + monthGetReward: callback => { pending.reward = callback; }, + getMonthlyCard: callback => { pending.card = callback; }, + }, + NumberToImage: { numberToImageNodes: () => {} }, + }); + compile(read('assets/home_popup_queue/HomePopupQueue.ts'), context); + context.HomePopupQueue = context.exports.default; + const Home = compile(`class Home { ${methods} }; Home;`, context); + const home = new Home(), queue = new context.HomePopupQueue(); + home.node = node(); + home.node.getComponent = () => queue; + queue.node = home.node; + home.getcard = node(); + const reward = node(); + home.getcard.getChildByName = name => name === 'mcReward' ? reward : null; + home.cardTime = node(); + home.deferHomePopupWhileTransfer = () => false; + home.updateCoin = () => state.coinRefreshes++; + home.setHealthInfo = () => state.healthRefreshes++; + home.startTimeCutDown = () => state.healthRefreshes++; + home.openMonthCard = () => { state.opened++; home.monthlyCardNode = { active: true }; }; + home.openWin = () => state.wins++; + return { home, queue, state, info, pending, timers, stored, tracked }; +} + +const invalidate = { + 'node cleared': f => { f.home.node = null; }, + 'node destroyed': f => { f.home.node.destroyed = true; }, + 'component destroyed': f => { f.home.destroyed = true; }, + 'entering gameplay': f => { f.state.preparing = true; }, + 'scene changed': f => { f.state.scene = 'GameScene'; }, +}; +const rewardResponse = { code: 1, data: { monthCardReward: 1 } }; + +for (const [name, leave] of Object.entries(invalidate)) { + test(`late monthly callbacks: ${name}`, () => { + const f = fixture(); + f.home.rewarded(); f.home.monthH(); leave(f); + f.pending.reward(rewardResponse); + f.pending.card({ msg: '不在有效期' }); + f.pending.card({ msg: '有效期内' }); + assert.equal(f.info.coin, 600); + assert.equal(f.tracked.length, 1); + assert.equal(f.queue.tasks.length, 0); + assert.equal(f.state.coinRefreshes + f.state.opened + f.state.wins, 0); + }); + + test(`queued monthly popups: ${name}`, () => { + const f = fixture(); + f.home.rewarded(); f.home.monthH(); + f.pending.reward(rewardResponse); f.pending.card({ msg: '不在有效期' }); + assert.equal(f.info.coin, 600); + assert.equal(f.queue.tasks.length, 2); + leave(f); + let finished = 0; + for (const task of f.queue.tasks) task.run(() => finished++); + assert.equal(finished, 2); + assert.equal(f.info.coin, 600); + assert.equal(f.home.getcard.active, false); + assert.equal(f.state.opened, 0); + assert.equal(f.stored.has('monthlyCardDate'), false); + }); + + test(`expired monthly UI timer: ${name}`, () => { + const f = fixture(); f.home.rewarded(); + f.pending.reward({ code: 0, data: null }); leave(f); + f.timers.forEach(callback => callback()); + assert.equal(f.info.hp_Max, 5); + assert.equal(f.info.hp, 5); + assert.equal(f.state.healthWrites, 1); + assert.equal(f.state.coinRefreshes + f.state.healthRefreshes, 0); + }); +} + +test('normal reward is credited before popup and is not credited again when displayed', () => { + const f = fixture(); f.home.rewarded(); f.pending.reward(rewardResponse); + assert.equal(f.info.coin, 600); + assert.equal(f.state.coinRefreshes, 1); + assert.equal(f.home.getcard.active, false); + let finished = 0; + f.queue.tasks[0].run(() => finished++); + assert.equal(f.home.getcard.active, true); + assert.equal(f.info.coin, 600); + assert.equal(f.tracked.length, 1); + assert.equal(finished, 1); +}); + +test('normal monthly popup records display date; already claimed reward does not credit coins', () => { + const f = fixture(); f.home.rewarded(); f.pending.reward({ code: 0, data: {} }); + assert.equal(f.info.coin, 100); + f.home.monthH(); f.pending.card({ msg: '不在有效期' }); + f.queue.tasks[0].run(() => {}); + assert.equal(f.state.opened, 1); + assert.ok(f.stored.get('monthlyCardDate')); +}); + +test('missing reward UI does not prevent credit or block the queue', () => { + const f = fixture(); f.home.rewarded(); f.pending.reward(rewardResponse); + f.home.getcard = null; + let finished = 0; f.queue.tasks[0].run(() => finished++); + assert.equal(f.info.coin, 600); + assert.equal(finished, 1); +});