diff --git a/assets/Script/GameManager.ts b/assets/Script/GameManager.ts index 400de0b..0375649 100644 --- a/assets/Script/GameManager.ts +++ b/assets/Script/GameManager.ts @@ -254,6 +254,8 @@ export default class GameManager extends cc.Component { "layer_3": "layer_three" }; const groupMap = { + "1": 1, + "2": 2, "A": 1, "B": 2, "C": 3, diff --git a/assets/Script/ItemGuide.ts b/assets/Script/ItemGuide.ts index 91c42aa..af068c7 100644 --- a/assets/Script/ItemGuide.ts +++ b/assets/Script/ItemGuide.ts @@ -27,18 +27,18 @@ export default class ItemGuide extends cc.Component { this.itemList[1].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[1]; this.itemList[2].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[2]; this.itemList[3].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[12]; - } else if (cc.fx.GameConfig.GM_INFO.level + 1 == 11) { + } else if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("time")) { this.itemList[0].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[3]; this.itemList[1].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[4]; this.itemList[2].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[5]; this.itemList[3].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[12]; - } else if (cc.fx.GameConfig.GM_INFO.level + 1 == 16) { + } else if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("magic")) { this.itemList[0].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[6]; this.itemList[1].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[7]; this.itemList[2].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[8]; this.itemList[3].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[12]; } - else if (cc.fx.GameConfig.GM_INFO.winStreakFirst == true) { + else if (cc.fx.GameConfig.GM_INFO.winStreakFirst == true && cc.fx.GameConfig.isWinStreakUnlocked()) { this.itemList[0].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[9]; this.itemList[1].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[10]; this.itemList[2].getComponent(cc.Sprite).spriteFrame = this.itemSpriteList[11]; @@ -53,8 +53,8 @@ export default class ItemGuide extends cc.Component { const gameInfo = cc.fx.GameConfig.GM_INFO; const level = gameInfo.level + 1; const showPropGuide = (level === 8 && gameInfo.hammerFirst) - || (level === 11 && gameInfo.freezeFirst) - || (level === 16 && gameInfo.magicAFirst); + || (level === cc.fx.GameConfig.getPropUnlockLevel("time") && gameInfo.freezeFirst) + || (level === cc.fx.GameConfig.getPropUnlockLevel("magic") && gameInfo.magicAFirst); const map = MapConroler._instance; this.node.children.forEach((item) => { item.active = false; @@ -68,14 +68,14 @@ export default class ItemGuide extends cc.Component { if (cc.fx.GameConfig.GM_INFO.level + 1 == 8) { cc.fx.GameConfig.GM_INFO.hammerFirst = false; index = 0; - } else if (cc.fx.GameConfig.GM_INFO.level + 1 == 11) { + } else if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("time")) { cc.fx.GameConfig.GM_INFO.freezeFirst = false; index = 1; - } else if (cc.fx.GameConfig.GM_INFO.level + 1 == 16) { + } else if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("magic")) { cc.fx.GameConfig.GM_INFO.magicAFirst = false; index = 2; } - else if (cc.fx.GameConfig.GM_INFO.winStreakFirst == true) { + else if (cc.fx.GameConfig.GM_INFO.winStreakFirst == true && cc.fx.GameConfig.isWinStreakUnlocked()) { cc.fx.GameConfig.GM_INFO.winStreakFirst = false; index = 0; } diff --git a/assets/Script/JiaZai.ts b/assets/Script/JiaZai.ts index a260555..9f8a3c4 100644 --- a/assets/Script/JiaZai.ts +++ b/assets/Script/JiaZai.ts @@ -598,7 +598,7 @@ export default class JiaZai extends cc.Component { this.queueHomeAutomaticTask("订单补发检查", () => this.getOrder(), 2); } else if (cacheKey === "cachedMonthlyCardPrefab") { this.queueHomeAutomaticTask("月卡检查", () => { - if (cc.fx.GameConfig.GM_INFO.level >= 16 && cc.fx.GameConfig.GM_INFO.canIos) { + if (cc.fx.GameConfig.GM_INFO.level >= cc.fx.GameConfig.getFeatureUnlockLevel("month") && cc.fx.GameConfig.GM_INFO.canIos) { this.monthH(); } }, 1); @@ -1028,7 +1028,7 @@ export default class JiaZai extends cc.Component { } let top = this.node.getChildByName("Load").getChildByName("Top"); - if (cc.fx.GameConfig.GM_INFO.level >= 16 && cc.fx.GameConfig.GM_INFO.canIos) { + if (cc.fx.GameConfig.GM_INFO.level >= cc.fx.GameConfig.getFeatureUnlockLevel("month") && cc.fx.GameConfig.GM_INFO.canIos) { top.getChildByName("yicon").active = true; if (JiaZai.cachedMonthlyCardPrefab) { this.queueHomeAutomaticTask("已缓存月卡检查", () => this.monthH(), 1); @@ -1063,6 +1063,7 @@ export default class JiaZai extends cc.Component { top.getChildByName("gachaGuang").runAction(cc.rotateTo(15, 1080).repeatForever()); this.refreshGachaRed(); + this.refreshNewPlayerActivityButtons(); this.setAdaptation(); // 加载广告 @@ -1077,6 +1078,36 @@ export default class JiaZai extends cc.Component { this.initTopBarTouchFade(); } + private refreshNewPlayerActivityButtons(): void { + if (!cc.fx.GameConfig.isNewPlayerB()) return; + const top = this.node.getChildByName("Load").getChildByName("Top"); + for (const [name, level] of [["day", 36], ["hammer", 51]] as [string, number][]) { + const button = top.getChildByName(name); + button.active = cc.fx.GameConfig.GM_INFO.level >= 20; + this.setNewPlayerActivityLock(button, level); + } + if (cc.fx.GameConfig.GM_INFO.level < 40) this.setJungleTreasureHomeButtonVisible(false); + } + + private setNewPlayerActivityLock(button: cc.Node, unlockLevel: number): void { + const locked = cc.fx.GameConfig.GM_INFO.level + 1 < unlockLevel; + button.getComponent(cc.Button).interactable = !locked; + const labelName = button.name === "jungle" ? "time" : "unlockLevel"; + let labelNode = button.getChildByName(labelName); + if (locked && !labelNode) { + // 复用 Jungle 入口已有的文字样式。 + labelNode = cc.instantiate(this.getJungleTreasureHomeButton().getChildByName("time")); + labelNode.name = labelName; + button.addChild(labelNode); + } + if (labelNode) { + labelNode.active = locked || button.name === "jungle"; + if (locked) labelNode.getComponent(cc.Label).string = `${unlockLevel}关解锁`; + } + const red = button.getChildByName("red"); + if (locked && red) red.active = false; + } + // ============================================ // 初始化 Top 栏触摸滑动渐隐/渐显 // 功能: @@ -2154,6 +2185,7 @@ export default class JiaZai extends cc.Component { } //打开月卡 openMonthlyCard(event?: cc.Event, customEventData?: string) { + if (cc.fx.GameConfig.isNewPlayerB() && cc.fx.GameConfig.GM_INFO.level < 20) return; if (!this.ensureHomePopupPrefab("shop", "prefab/monthlyCard", "cachedMonthlyCardPrefab", () => this.openMonthlyCard(undefined, customEventData))) return; @@ -2257,6 +2289,7 @@ export default class JiaZai extends cc.Component { // - share:是否来自分享回调 // ============================================ openDailyQuests(event?: cc.Event, customEventData?: string, share?: boolean) { + if (cc.fx.GameConfig.isNewPlayerB() && cc.fx.GameConfig.GM_INFO.level < 35) return; this.vibrateButtonClick(event); let eventName = ""; @@ -3105,6 +3138,12 @@ export default class JiaZai extends cc.Component { private setJungleTreasureHomeButtonVisible(visible: boolean): void { let button = this.getJungleTreasureHomeButton(); if (button) { + if (cc.fx.GameConfig.isNewPlayerB()) { + if (cc.fx.GameConfig.GM_INFO.level < 40) { + visible = cc.fx.GameConfig.GM_INFO.level >= 20; + } + this.setNewPlayerActivityLock(button, 41); + } button.active = visible; } this.refreshHomeActivityEntryLayout(); @@ -3975,6 +4014,7 @@ export default class JiaZai extends cc.Component { // 2. 使用缓存控制每天只弹一次月卡弹窗 // ============================================ monthH() { + if (cc.fx.GameConfig.isNewPlayerB() && cc.fx.GameConfig.GM_INFO.level < 20) return; if (this.deferHomePopupWhileTransfer("monthlyCard", () => this.monthH())) { return; } @@ -4293,7 +4333,7 @@ export default class JiaZai extends cc.Component { if (cc.fx.GameConfig.GM_INFO.tasks.levelPass.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.share.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.useEnergy.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.useProp.state == 1 ) { - top.getChildByName("day").getChildByName("red").active = true; + top.getChildByName("day").getChildByName("red").active = !cc.fx.GameConfig.isNewPlayerB() || cc.fx.GameConfig.GM_INFO.level >= 35; } }) } @@ -4302,7 +4342,7 @@ export default class JiaZai extends cc.Component { if (cc.fx.GameConfig.GM_INFO.tasks.levelPass.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.share.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.useEnergy.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.useProp.state == 1 ) { - top.getChildByName("day").getChildByName("red").active = true; + top.getChildByName("day").getChildByName("red").active = !cc.fx.GameConfig.isNewPlayerB() || cc.fx.GameConfig.GM_INFO.level >= 35; } } @@ -4315,7 +4355,7 @@ export default class JiaZai extends cc.Component { // 触发条件:连胜显示开关打开且关卡>=17级 // ============================================ openWin() { - if (this.winStreakShow == false || cc.fx.GameConfig.GM_INFO.level < 17) { + if (this.winStreakShow == false || cc.fx.GameConfig.GM_INFO.level < cc.fx.GameConfig.getFeatureUnlockLevel("winStreak")) { return; } else { let date = new Date(); @@ -4345,6 +4385,7 @@ export default class JiaZai extends cc.Component { // 何时调用:点击连胜活动按钮时调用 // ============================================ openWinStreak(event?: cc.Event, customEventData?: string, fromQueue: boolean = false) { + if (!cc.fx.GameConfig.isWinStreakUnlocked()) return; if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; if (!event && customEventData === undefined && !fromQueue) { HomePopupQueue.get(this.node).enqueue("winStreak", 50, done => { @@ -4466,7 +4507,7 @@ export default class JiaZai extends cc.Component { popUpPassCheck(retry: number = 0) { if (this.deferHomePopupWhileTransfer("passCheck", () => this.popUpPassCheck(retry))) return; const info = cc.fx.GameConfig.GM_INFO; - if (info.level < 21 || !info.canIos) return; + if (info.level < cc.fx.GameConfig.getFeatureUnlockLevel("pass") || !info.canIos) return; const date = new Date(); const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const cacheKey = 'CACHE_PASS_CHECK:' + (info.uid || Utils.uid); @@ -4519,6 +4560,10 @@ export default class JiaZai extends cc.Component { } private openPassCheckIntro(onOpened?: () => void, onFinished?: () => void) { + if (cc.fx.GameConfig.isNewPlayerB() && cc.fx.GameConfig.GM_INFO.level < 20) { + if (onFinished) onFinished(); + return; + } if (this.passContentLoading || (this.passCheckNode && cc.isValid(this.passCheckNode))) { if (onFinished) onFinished(); return; } if (this.passIntroNode && cc.isValid(this.passIntroNode)) { if (onFinished) onFinished(); return; } if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) { if (onFinished) onFinished(); return; } @@ -4543,6 +4588,7 @@ export default class JiaZai extends cc.Component { } private openPassCheckContent(event?: cc.Event, customEventData?: string) { + if (cc.fx.GameConfig.isNewPlayerB() && cc.fx.GameConfig.GM_INFO.level < 20) return; if (this.passContentLoading || (this.passCheckNode && cc.isValid(this.passCheckNode))) return; this.vibrateButtonClick(event); if (!this.ensureHomePopupPrefab("shop", "prefab/passCheck", "cachedPassCheckPrefab", @@ -4708,7 +4754,7 @@ export default class JiaZai extends cc.Component { checkAndSetPlayerPassLevel() { const currentLevel = cc.fx.GameConfig.GM_INFO.level; // 判断是否已经记录过playerPassLevel,如果没有并且当前等级大于等于21级,则记录 - if (currentLevel >= 21 && cc.fx.GameConfig.GM_INFO.canIos) { + if (currentLevel >= cc.fx.GameConfig.getFeatureUnlockLevel("pass") && cc.fx.GameConfig.GM_INFO.canIos) { const getPassButton = (): cc.Node => { if (!cc.isValid(this, true) || !this.node || !cc.isValid(this.node, true)) { return null; diff --git a/assets/Script/Map.ts b/assets/Script/Map.ts index fb6443e..55e8e0e 100644 --- a/assets/Script/Map.ts +++ b/assets/Script/Map.ts @@ -709,7 +709,7 @@ export default class MapConroler extends cc.Component { this.SceneManager.updateNextMode(); } // 记录通行证关卡 - if (cc.fx.GameConfig.GM_INFO.level >= 21) { + if (cc.fx.GameConfig.GM_INFO.level >= cc.fx.GameConfig.getFeatureUnlockLevel("pass")) { this.setPassProgress(); } if (this.new_mode == 3) { @@ -836,7 +836,7 @@ export default class MapConroler extends cc.Component { this.iceNode.parent.getComponent(cc.Button).interactable = false; this.hammerNode.parent.getComponent(cc.Button).interactable = false; this.magicNode.parent.getComponent(cc.Button).interactable = false; - } else if (currentLevel >= 8 && currentLevel < 11) { + } else if (currentLevel >= 8 && currentLevel < cc.fx.GameConfig.getPropUnlockLevel("time")) { lock0Active = false; lock1Active = true; lock2Active = true; @@ -846,7 +846,7 @@ export default class MapConroler extends cc.Component { this.iceNode.parent.getComponent(cc.Button).interactable = false; this.hammerNode.parent.getComponent(cc.Button).interactable = true; this.magicNode.parent.getComponent(cc.Button).interactable = false; - } else if (currentLevel >= 11 && currentLevel < 16) { + } else if (currentLevel >= cc.fx.GameConfig.getPropUnlockLevel("time") && currentLevel < cc.fx.GameConfig.getPropUnlockLevel("magic")) { lock0Active = false; lock1Active = false; lock2Active = true; @@ -856,7 +856,7 @@ export default class MapConroler extends cc.Component { this.iceNode.parent.getComponent(cc.Button).interactable = true; this.hammerNode.parent.getComponent(cc.Button).interactable = true; this.magicNode.parent.getComponent(cc.Button).interactable = false; - } else if (currentLevel >= 16) { + } else if (currentLevel >= cc.fx.GameConfig.getPropUnlockLevel("magic")) { lock0Active = false; lock1Active = false; lock2Active = false; @@ -871,6 +871,10 @@ export default class MapConroler extends cc.Component { this.itemLock[0].active = lock0Active; this.itemLock[1].active = lock1Active; this.itemLock[2].active = lock2Active; + if (cc.fx.GameConfig.isNewPlayerB()) { + this.itemLock[1].children[1].getComponent(cc.Sprite).spriteFrame = NumberToImage.font.getSpriteFrame("coin_4"); + this.itemLock[2].children[1].getComponent(cc.Sprite).spriteFrame = NumberToImage.font.getSpriteFrame("coin_8"); + } if (cc.fx.GameConfig.GM_INFO.magicAFirst == true) { this.magicNode.children[0].active = true; @@ -894,7 +898,8 @@ export default class MapConroler extends cc.Component { public showPropGuideMask(level: number) { const gameInfo = cc.fx.GameConfig.GM_INFO; if (this.propGuideMask || gameInfo.otherLevel != 0 || gameInfo.level + 1 !== level) return; - const buttonName = { 8: 'destroyBtn', 11: 'timeBtn', 16: 'magicBtn' }[level]; + const buttonName = { 8: 'destroyBtn', [cc.fx.GameConfig.getPropUnlockLevel('time')]: 'timeBtn', + [cc.fx.GameConfig.getPropUnlockLevel('magic')]: 'magicBtn' }[level]; if (!buttonName) return; if (!this.guideMaskPrefab) { cc.error('GuideMask 预制体未绑定'); @@ -921,7 +926,7 @@ export default class MapConroler extends cc.Component { const clickEvent = new cc.Component.EventHandler(); clickEvent.target = this.node; clickEvent.component = cc.js.getClassName(this); - clickEvent.handler = level === 8 ? 'useHammer' : level === 11 ? 'useTimeProp' : 'useMagic'; + clickEvent.handler = level === 8 ? 'useHammer' : level === cc.fx.GameConfig.getPropUnlockLevel('time') ? 'useTimeProp' : 'useMagic'; clickEvent.customEventData = 'guide'; button.getComponent(cc.Button).clickEvents = [clickEvent]; @@ -1007,7 +1012,7 @@ export default class MapConroler extends cc.Component { this.guideNode.getComponent(Animation).stop(); this.guideNode.active = false; } - if (cc.fx.GameConfig.GM_INFO.winStreak >= 10) { + if (cc.fx.GameConfig.isWinStreakUnlocked() && cc.fx.GameConfig.GM_INFO.winStreak >= 10) { this.is_frenzy = true; } @@ -1048,7 +1053,7 @@ export default class MapConroler extends cc.Component { // ============================================ initMap() { - if (cc.fx.GameConfig.GM_INFO.winStreak < 10 || cc.fx.GameConfig.GM_INFO.otherLevel > 0) { + if (!cc.fx.GameConfig.isWinStreakUnlocked() || cc.fx.GameConfig.GM_INFO.winStreak < 10 || cc.fx.GameConfig.GM_INFO.otherLevel > 0) { if (this.winStreakBtn.node) { this.winStreakBtn.node.active = false; } @@ -1338,7 +1343,7 @@ export default class MapConroler extends cc.Component { this.guideItem.active = true; this.setPropNum(); } - if (cc.fx.GameConfig.GM_INFO.winStreakFirst == true) { + if (cc.fx.GameConfig.GM_INFO.winStreakFirst == true && cc.fx.GameConfig.isWinStreakUnlocked()) { this.guideItem.active = true; } @@ -4538,13 +4543,13 @@ export default class MapConroler extends cc.Component { cc.error("playWinSettlement skipped: Win prefab is not ready"); return; } - if (cc.fx.GameConfig.GM_INFO.addLevel >= 5) { + if (cc.fx.GameConfig.canSettleCareer() && cc.fx.GameConfig.GM_INFO.addLevel >= 5) { this.createRank(); } this.createCareer(); this.Settlement(); const winStreak = winRoot.getChildByName("WinStreak"); - if (cc.fx.GameConfig.GM_INFO.level >= 17) { + if (cc.fx.GameConfig.GM_INFO.level >= cc.fx.GameConfig.getFeatureUnlockLevel("winStreak")) { if (winStreak) winStreak.active = true; this.openWinStreak(); } @@ -4748,6 +4753,11 @@ export default class MapConroler extends cc.Component { cc.fx.AudioManager._instance.playEffect("anniu_Big", null); // console.log("下一关", cc.fx.GameConfig.GM_INFO.level); this.uploadToCloud(cc.fx.GameConfig.GM_INFO.level + 1); + if (cc.fx.GameConfig.isNewPlayerB() && cc.fx.GameConfig.GM_INFO.level === 15 + && !cc.fx.GameConfig.GM_INFO.otherLevel) { + this.returnHome(null, null); + return; + } if (cc.fx.GameTool.maxLevel()) { // 正常游戏到最后一关时 先弹出提示 无尽关暂时都是从外面进入 // if (cc.fx.GameConfig.GM_INFO.randomLevel == 0) { @@ -4807,10 +4817,10 @@ export default class MapConroler extends cc.Component { // console.log("新手引导开启"); cc.fx.GameConfig.GM_INFO.hammerFirst = true; } - if (cc.fx.GameConfig.GM_INFO.level + 1 == 11) { + if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("time")) { cc.fx.GameConfig.GM_INFO.freezeFirst = true; } - if (cc.fx.GameConfig.GM_INFO.level + 1 == 16) { + if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("magic")) { cc.fx.GameConfig.GM_INFO.magicAFirst = true; } @@ -5290,8 +5300,11 @@ export default class MapConroler extends cc.Component { cc.fx.GameTool.shushu_Track("hammer_frenzy", data2); } // alert(cc.fx.GameConfig.GM_INFO.winStreak); - if (cc.fx.GameConfig.GM_INFO.winStreak < 10 || cc.fx.GameConfig.GM_INFO.otherLevel > 0) { + if (!cc.fx.GameConfig.isWinStreakUnlocked() || cc.fx.GameConfig.GM_INFO.winStreak < 10 || cc.fx.GameConfig.GM_INFO.otherLevel > 0) { this.hideLoseReasonPanels(lose); + if (!cc.fx.GameConfig.isWinStreakUnlocked() && cc.fx.GameConfig.GM_INFO.otherLevel == 0) { + cc.fx.GameTool.setWinStreak("fail"); + } if (cc.fx.GameConfig.GM_INFO.revive == 0 && this.revive) { this.revive.getComponent("Revive").offShow(); if (this.node) { @@ -5373,10 +5386,10 @@ export default class MapConroler extends cc.Component { if (cc.fx.GameConfig.GM_INFO.level + 1 == 8) { cc.fx.GameConfig.GM_INFO.hammerFirst = true; } - if (cc.fx.GameConfig.GM_INFO.level + 1 == 11) { + if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("time")) { cc.fx.GameConfig.GM_INFO.freezeFirst = true; } - if (cc.fx.GameConfig.GM_INFO.level + 1 == 16) { + if (cc.fx.GameConfig.GM_INFO.level + 1 == cc.fx.GameConfig.getPropUnlockLevel("magic")) { cc.fx.GameConfig.GM_INFO.magicAFirst = true; } } @@ -5702,7 +5715,7 @@ export default class MapConroler extends cc.Component { this.noBlockClickDuration = (currentTime - this.lastBlockClickTime) / 1000; // 转换为秒 // 如果超过一定时间未点击方块 - if (cc.fx.GameConfig.GM_INFO.level + 1 >= 11) { + if (cc.fx.GameConfig.GM_INFO.level + 1 >= cc.fx.GameConfig.getPropUnlockLevel("time")) { if (this.noBlockClickDuration >= 5) { let rippleShrink = this.node.parent.getChildByName("Bottom").getChildByName("rippleShrink").getComponent('RippleShrink'); if (this.isRippleShrink && rippleShrink && rippleShrink.startRipple) { @@ -7303,6 +7316,13 @@ export default class MapConroler extends cc.Component { const t = (duration: number) => this.settlementAnimationTime(duration); const winRoot = this.getWinRoot(); if (!winRoot) return; + if (!cc.fx.GameConfig.canSettleCareer()) { + ["Career", "Rank", "Ruzhi"].forEach(name => { + const node = winRoot.getChildByName(name); + if (node) node.active = false; + }); + return; + } let max = false; if (cc.fx.GameConfig.GM_INFO.addLevel == 4) { let address = cc.fx.StorageMessage.getStorage("address"); @@ -7324,6 +7344,7 @@ export default class MapConroler extends cc.Component { } winRoot.getChildByName("Rank").active = false; let Career = winRoot.getChildByName("Career"); + Career.active = true; ["bg", "level", "maxlevel", "progress"].forEach((nodeName) => { Career.getChildByName(nodeName).active = false; Career.getChildByName(nodeName + "2").active = true; @@ -7781,6 +7802,7 @@ export default class MapConroler extends cc.Component { // 效果:进入特殊锤子模式,可直接消除任意方块 // ============================================ useHammerSpecial() { + if (!cc.fx.GameConfig.isWinStreakUnlocked()) return; if (this.gameOver == true || this.gameWin == true) { return; } diff --git a/assets/Script/module/Config/GameConfig.ts b/assets/Script/module/Config/GameConfig.ts index 9a62dec..b7248aa 100644 --- a/assets/Script/module/Config/GameConfig.ts +++ b/assets/Script/module/Config/GameConfig.ts @@ -61,8 +61,17 @@ export class GameConfig { static LEVEL_INFO: ({ id: string; map: number[]; risefall?: undefined; gap?: undefined; } | { id: string; map: number[]; risefall: { pos: cc.Vec2; color: number; }[]; gap?: undefined; } | { id: string; map: number[]; gap: cc.Vec2[]; risefall?: undefined; })[]; static WALL_INFO: (({ id: number; num: number; block: number; color: number; special: number; length: number; lock?: undefined; } | { id: number; num: number; block: number; color: number; special: number; length: number; lock: boolean; })[] | ({ id: number; num: number; block: number; color: number; special: number; length: number; freeze?: undefined; } | { id: number; num: number; block: number; color: number; special: number; length: number; freeze: number; })[])[]; static PROP_INFO: ({ pos1: { x: number; y: number; z: number; }; pos2: { x: number; y: number; z: number; }; pos3: { x: number; y: number; z: number; }; pos4: { x: number; y: number; z: number; }; pos5: { x: number; y: number; z: number; }; pos6: { x: number; y: number; z: number; }; } | { pos1: { x: number; y: number; z: number; }; pos2: { x: number; y: number; z: number; }; pos3: { x: number; y: number; z: number; }; pos4: { x: number; y: number; z: number; }; pos5: { x: number; y: number; z: number; }; pos6?: undefined; })[]; - static NEW_LEVEL: any; - static NEW_GUIDE: any; + private static defaultNewLevel: any[] = []; + private static newPlayerNewLevel: any[] = []; + private static defaultNewGuide: any[] = []; + static get NEW_LEVEL(): any[] { + return this.isNewPlayerB() ? this.newPlayerNewLevel : this.defaultNewLevel; + } + static get NEW_GUIDE(): any[] { + return this.isNewPlayerB() + ? this.defaultNewGuide.map(item => ({ ...item, level: this.getPropUnlockLevel(item.name) })) + : this.defaultNewGuide; + } static PASS_CHECK: any; static PASS_CHECK_OLD: any; static PASS_RATE: any; @@ -191,7 +200,7 @@ export class GameConfig { return Promise.resolve(false); } - return this.loadLevelFromBundle(levelNumber).then(jsonData => { + return this.loadLevelFromBundle(levelNumber, otherLevel === 0).then(jsonData => { if (!isCurrent()) return false; const blockInfo = jsonData && jsonData.BLOCK_INFO && jsonData.BLOCK_INFO[0]; const levelInfo = jsonData && jsonData.LEVEL_INFO && jsonData.LEVEL_INFO[0]; @@ -392,6 +401,39 @@ export class GameConfig { return this.JUNGLE_TREASURE_UNLOCK_LEVEL; } + static isNewPlayerB(): boolean { + const assignment = this.GM_INFO.abTestAssignments && this.GM_INFO.abTestAssignments.layer_1; + return !!assignment && assignment.name === "new1_50" && assignment.enabled !== false + && ["B", "2"].includes(String(assignment.group).toUpperCase()); + } + + static getLevelConfigPath(level: number, mainline: boolean = true): string { + const folder = mainline && this.isNewPlayerB() && level >= 1 && level <= 50 ? "Json2" : "Json"; + return `${folder}/level${level}`; + } + + static getPropUnlockLevel(prop: string): number { + if (prop === "hammer") return 8; + if (prop === "time") return this.isNewPlayerB() ? 14 : 11; + return this.isNewPlayerB() ? 18 : 16; + } + + // 以下功能门槛均使用已通关数。 + static getFeatureUnlockLevel(feature: string): number { + const levels = this.isNewPlayerB() + ? { month: 20, pass: 20, daily: 35, winStreak: 50 } + : { month: 16, pass: 21, daily: 0, winStreak: 17 }; + return levels[feature]; + } + + static isWinStreakUnlocked(): boolean { + return !this.isNewPlayerB() || this.GM_INFO.level >= 50; + } + + static canSettleCareer(): boolean { + return !this.isNewPlayerB() || this.GM_INFO.level > 15; + } + static TA: any; static GE: any; @@ -744,19 +786,26 @@ export class GameConfig { return; } const newLevelData = res.json; - this.NEW_LEVEL = newLevelData.NEW_LEVEL; + this.defaultNewLevel = newLevelData.NEW_LEVEL; // console.log("____________________配置", this.NEW_LEVEL); // 可以根据需求对 newLevelData 进行处理 // //console.log("NEW_LEVEL.JSON 加载成功:", newLevelData); }) + cc.resources.load("Json/NEW_LEVEL2", (err: any, res: cc.JsonAsset) => { + if (err) { + console.error("加载 NEW_LEVEL2.JSON 失败:", err); + return; + } + this.newPlayerNewLevel = res.json.NEW_LEVEL; + }); cc.resources.load("Json/NEW_GUIDE", (err: any, res: cc.JsonAsset) => { if (err) { console.error("加载 NEW_GUIDE.JSON 失败:", err); return; } const newLevelData = res.json; - this.NEW_GUIDE = newLevelData.NEW_GUIDE; + this.defaultNewGuide = newLevelData.NEW_GUIDE; // 可以根据需求对 newLevelData 进行处理 // //console.log("NEW_GUIDE.JSON 加载成功:", newLevelData); }) @@ -811,6 +860,7 @@ export class GameConfig { if (this.cancelBlockLoading) this.cancelBlockLoading(); let name = null; + let levelNumber: number; if (endless) { let level = 0; if (!cc.fx.GameConfig.GM_INFO.randomLevel || cc.fx.GameConfig.GM_INFO.randomLevel <= 0) { @@ -818,7 +868,8 @@ export class GameConfig { } level = cc.fx.GameConfig.GM_INFO.randomLevel; - name = "Json/level" + (level); + levelNumber = level; + name = this.getLevelConfigPath(levelNumber, false); console.log("无尽模式关卡", level); cc.fx.GameConfig.GM_INFO.GameplayType = 1; } else { @@ -829,11 +880,12 @@ export class GameConfig { return; } - name = "Json/level" + (cc.fx.GameConfig.GM_INFO.level + 1); + levelNumber = cc.fx.GameConfig.GM_INFO.level + 1; if (cc.fx.GameConfig.GM_INFO.otherLevel > 0) { console.log("进入特殊帮助关卡,可越过自己关卡等级"); - name = "Json/level" + cc.fx.GameConfig.GM_INFO.otherLevel; + levelNumber = cc.fx.GameConfig.GM_INFO.otherLevel; } + name = this.getLevelConfigPath(levelNumber, !cc.fx.GameConfig.GM_INFO.otherLevel); } const scene = cc.director.getScene(); @@ -877,7 +929,7 @@ export class GameConfig { return; } - self.applyLevelData(res.json, type, time, Number(name.substring("Json/level".length)), preparation); + self.applyLevelData(res.json, type, time, levelNumber, preparation); }); }); @@ -1158,10 +1210,10 @@ export class GameConfig { /** * 从本地bundle加载关卡数据 */ - static loadLevelFromBundle(level: number): Promise { + static loadLevelFromBundle(level: number, mainline: boolean = true): Promise { return new Promise((resolve, reject) => { const bundleName = "custom"; - const name = `Json/level${level}`; + const name = this.getLevelConfigPath(level, mainline); cc.assetManager.loadBundle(bundleName, { priority: 10 }, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { diff --git a/server/laf-cloud/functions/Utils.ts b/server/laf-cloud/functions/Utils.ts index e14b4f9..6925670 100644 --- a/server/laf-cloud/functions/Utils.ts +++ b/server/laf-cloud/functions/Utils.ts @@ -1,6 +1,16 @@ import cloud from '@lafjs/cloud'; const db = cloud.database(); export default class Utils { + static isNewPlayerB(user: any): boolean { + let assignments = user?.abTests; + if (typeof assignments === "string") { + try { assignments = JSON.parse(assignments); } catch { return false; } + } + const assignment = assignments?.layer_1; + return !!assignment && assignment.name === "new1_50" && assignment.enabled !== false + && ["B", "2"].includes(String(assignment.group).toUpperCase()); + } + static checkToken(token, token1) { if (token == token1) { //console.error("校验成功") diff --git a/server/laf-cloud/functions/userLevel.ts b/server/laf-cloud/functions/userLevel.ts index 123a1fc..f3c4313 100644 --- a/server/laf-cloud/functions/userLevel.ts +++ b/server/laf-cloud/functions/userLevel.ts @@ -52,7 +52,11 @@ export default async function (ctx: FunctionContext) { count = 1; return; } - let add = oldAdd + count; + // B组前15关只推进主线;补报跨过15关时也只计算门槛之后的胜利。 + const careerCount = Utils.isNewPlayerB(res1.data) + ? Math.max(levelAmount - 15, 0) - Math.max(oldlevelAmount - 15, 0) + : count; + let add = oldAdd + careerCount; let isWhite = res1.data?.isWhite; let careerAmount = res1.data?.careerAmount || 0; if (isWuXian == "true") { diff --git a/tools/new-player-ab-test-support.cjs b/tools/new-player-ab-test-support.cjs new file mode 100644 index 0000000..0edb68c --- /dev/null +++ b/tools/new-player-ab-test-support.cjs @@ -0,0 +1,46 @@ +const fs = require('node:fs'); +const vm = require('node:vm'); +const ts = require('typescript'); + +function loadMembers(file, names, globals = {}) { + const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true); + const declaration = source.statements.find(ts.isClassDeclaration); + const available = declaration ? declaration.members : source.statements.filter(ts.isVariableStatement) + .flatMap(statement => statement.declarationList.declarations) + .find(node => node.name.getText(source) === 'GameTool').initializer.properties; + const members = names.map(name => { + const member = available.find(node => node.name && node.name.getText(source) === name); + if (!member) throw new Error(`${file}: missing ${name}`); + return member.getText(source); + }); + return vm.runInNewContext(ts.transpileModule(`class Controller { ${members.join('\n')} }\nController;`, { + compilerOptions: { target: ts.ScriptTarget.ES2017 }, + }).outputText, globals); +} + +const Config = loadMembers('assets/Script/module/Config/GameConfig.ts', [ + 'isNewPlayerB', 'getLevelConfigPath', 'getPropUnlockLevel', 'getFeatureUnlockLevel', + 'isWinStreakUnlocked', 'canSettleCareer', 'defaultNewLevel', 'newPlayerNewLevel', + 'defaultNewGuide', 'NEW_LEVEL', 'NEW_GUIDE', +]); +function withNewPlayerConfig(config) { + for (const name of Object.getOwnPropertyNames(Config)) { + if (['length', 'name', 'prototype'].includes(name)) continue; + Object.defineProperty(config, name, Object.getOwnPropertyDescriptor(Config, name)); + } + return config; +} + +const ServerUtils = loadMembers('server/laf-cloud/functions/Utils.ts', ['isNewPlayerB']); +function loadModule(file, deps = {}, globals = {}) { + const exports = {}; + const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), { + compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS }, + }).outputText; + vm.runInNewContext(code, { exports, require: name => { + if (!(name in deps)) throw new Error(`${file}: missing dependency ${name}`); + return deps[name]; + }, Date, console, ...globals }); + return exports; +} +module.exports = { loadMembers, withNewPlayerConfig, ServerUtils, loadModule }; diff --git a/tools/test-battle-pass-ui.cjs b/tools/test-battle-pass-ui.cjs index 4c179b3..987e126 100644 --- a/tools/test-battle-pass-ui.cjs +++ b/tools/test-battle-pass-ui.cjs @@ -3,6 +3,7 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const ts = require('typescript'); +const { withNewPlayerConfig } = require('./new-player-ab-test-support.cjs'); const rows = JSON.parse(fs.readFileSync('assets/resources/Json/PASS_CHECK.json', 'utf8')).Sheet1; const quiet = { log() {}, warn() {}, error() {} }; function load(file, deps = {}, globals = {}) { @@ -42,6 +43,7 @@ function runtime() { find(path, root) { return path.split('/').reduce((node, name) => node && node.getChildByName(name), root); }, fx: { GameConfig: { PASS_CHECK: rows, GM_INFO: {}, isGameplayEntryPreparing: () => false }, GameTool: { adaptation: () => 1, shushu_Track() {} }, StorageMessage: { getStorage: () => 0, setStorage() {} } }, Tween: { stopAllByTarget() {} }, tween(target) { const chain = { delay: () => chain, to: (_, values) => { Object.assign(target, values); return chain; }, call: fn => { fn(); return chain; }, start() {} }; return chain; } }; + withNewPlayerConfig(cc.fx.GameConfig); const child = (parent, name) => { const node = new Node(name); parent.addChild(node); return node; }; const numbers = { default: { numberToImageNodesPassCheck(value, _w, _h, _prefix, node) { node.number = value; } } }; const Item = load('assets/shop/script/passCheckItem.ts', { '../../Script/NumberToImage': numbers }, { cc }).default; @@ -209,6 +211,18 @@ test('daily intro does not record failed loading or requests and duplicate repli assert.equal(local.size, 1); }); +test('B battle pass waits until clearing 20, then uses the existing daily introduction', () => { + const { home, cc, callbacks } = homeFixture(); + Object.assign(cc.fx.GameConfig.GM_INFO, { level: 19, canIos: true, uid: 'new-player', + abTestAssignments: { layer_1: { name: 'new1_50', group: 'B' } } }); + home.deferHomePopupWhileTransfer = () => false; + home.popUpPassCheck(); assert.equal(callbacks.length, 0); + cc.fx.GameConfig.GM_INFO.level = 20; + home.popUpPassCheck(); assert.equal(callbacks.length, 1); + callbacks.shift()({ code: 1, data: { upgradeReady: true, time: Date.now() - 1000, passCheck: 'null' } }); + assert.equal(home.node.children.filter(n => n.name === 'intro').length, 1); +}); + test('cold intro loading survives another home popup request and records only after showing', async () => { const { home } = homeFixture(); let ready = false, finishLoad, shown = 0; diff --git a/tools/test-first-game-entry.cjs b/tools/test-first-game-entry.cjs index 86afca9..6963647 100644 --- a/tools/test-first-game-entry.cjs +++ b/tools/test-first-game-entry.cjs @@ -90,6 +90,7 @@ function fixture() { Config.prepareBlockWindow = () => { state.events.push('blocks'); return state.blocks.promise; }; Config.warmupHomeBlockAssets = () => state.blocks.promise; Config.showBlockLoadError = (error, retry) => state.errors.push({ error, retry }); + require('./new-player-ab-test-support.cjs').withNewPlayerConfig(Config); cc.fx = { GameConfig: Config, GameTool }; return { state, cc, Config, BlockAssetManager }; } diff --git a/tools/test-new-player-ab.cjs b/tools/test-new-player-ab.cjs new file mode 100644 index 0000000..42d5f20 --- /dev/null +++ b/tools/test-new-player-ab.cjs @@ -0,0 +1,231 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const { loadMembers, withNewPlayerConfig, ServerUtils, loadModule } = require('./new-player-ab-test-support.cjs'); +const configFile = 'assets/Script/module/Config/GameConfig.ts'; +const mapFile = 'assets/Script/Map.ts'; +const homeFile = 'assets/Script/JiaZai.ts'; +const assignment = group => ({ layer_1: { name: 'new1_50', group, enabled: true } }); +const config = (level = 0, group = 'B') => withNewPlayerConfig({ GM_INFO: { + level, otherLevel: 0, abTestAssignments: assignment(group), canIos: true, addLevel: 0, winStreak: 0, +} }); +const json = file => JSON.parse(fs.readFileSync(file, 'utf8')); + +test('front and back agree on B/2; A, missing, disabled and unrelated experiments keep defaults', () => { + for (const [raw, expected] of [ + [assignment('B'), true], [assignment(2), true], [assignment('2'), true], + [assignment('A'), false], [assignment(1), false], [assignment('1'), false], + [assignment(0), false], [{}, false], [undefined, false], + [{ layer_1: { name: 'another', group: 'B' } }, false], + [{ layer_1: { name: 'new1_50', group: 'B', enabled: false } }, false], + ]) { + const c = config(); c.GM_INFO.abTestAssignments = raw; + assert.equal(c.isNewPlayerB(), expected); + assert.equal(ServerUtils.isNewPlayerB({ abTests: raw }), expected); + assert.equal(ServerUtils.isNewPlayerB({ abTests: JSON.stringify(raw) }), expected); + } + assert.equal(ServerUtils.isNewPlayerB({ abTests: 'invalid' }), false); +}); + +test('B mainline switches at 50/51; A, help and endless keep Json', async () => { + const requests = []; + const cc = { JsonAsset: 'json', assetManager: { + loadBundle(_name, _options, done) { done(null, {}); }, + loadAny(request, _options, done) { requests.push(request.path); done(null, { json: { path: request.path } }); }, + } }; + const Loader = loadMembers(configFile, ['loadLevelFromBundle'], { cc }); + for (const group of ['A', 'B', 0]) { + const c = config(0, group); withNewPlayerConfig(Loader); Loader.GM_INFO = c.GM_INFO; + for (const level of [1, 15, 50, 51, 52, 100]) { + const expected = group === 'B' && level <= 50 ? 'Json2' : 'Json'; + assert.equal(c.getLevelConfigPath(level), `${expected}/level${level}`); + await Loader.loadLevelFromBundle(level); + assert.equal(requests.at(-1), `${expected}/level${level}`); + await Loader.loadLevelFromBundle(level, false); + assert.equal(requests.at(-1), `Json/level${level}`); + } + } +}); + +test('both introduction tables select B after login and keep NEW_LEVEL2 beyond 50', () => { + const c = config(0, 'A'); + c.defaultNewLevel = json('assets/resources/Json/NEW_LEVEL.json').NEW_LEVEL; + c.newPlayerNewLevel = json('assets/resources/Json/NEW_LEVEL2.json').NEW_LEVEL; + c.defaultNewGuide = json('assets/resources/Json/NEW_GUIDE.json').NEW_GUIDE; + assert.deepEqual(Array.from(c.NEW_LEVEL.slice(0, 4), row => row.level), [7, 15, 25, 35]); + assert.deepEqual(Array.from(c.NEW_GUIDE, row => row.level), [8, 11, 16]); + c.GM_INFO.abTestAssignments = assignment('B'); + for (const level of [0, 49, 50, 100]) { + c.GM_INFO.level = level; + assert.equal(c.NEW_LEVEL, c.newPlayerNewLevel); + assert.deepEqual(Array.from(c.NEW_LEVEL.slice(0, 4), row => row.level), [6, 11, 24, 30]); + assert.deepEqual(Array.from(c.NEW_GUIDE, row => row.level), [8, 14, 18]); + } +}); + +class Node { + constructor(name) { Object.assign(this, { name, children: [], components: {}, active: true }); } + addChild(node) { node.parent = this; this.children.push(node); } + getChildByName(name) { return this.children.find(node => node.name === name); } + getComponent(type) { return this.components[type]; } +} +function inflate(data, id) { + const raw = data[id], node = new Node(raw._name); + node.active = raw._active; + for (const ref of raw._components || []) { + const component = data[ref.__id__]; + node.components[component.__type__] = { ...component, string: component._string }; + } + for (const ref of raw._children || []) node.addChild(inflate(data, ref.__id__)); + return node; +} +function runtime(c) { + const cc = { Button: 'cc.Button', Label: 'cc.Label', Sprite: 'cc.Sprite', + fx: { GameConfig: c, StorageMessage: { getStorage: () => ({ authorize: true }) }, GameTool: {} }, + instantiate(node) { const copy = new Node(node.name); copy.components = structuredClone(node.components); return copy; }, + tween() { const chain = { to() { return chain; }, delay() { return chain; }, start() {} }; return chain; }, + }; + return cc; +} + +test('home entries are hidden before 20, locked with labels at 20 and unlock at 35/40/50', () => { + const c = config(), cc = runtime(c); + const data = json('assets/Scene/HomeScene.fire'); + const top = inflate(data, data.findIndex(n => n._name === 'Top' && n.__type__ === 'cc.Node')); + const Home = loadMembers(homeFile, ['refreshNewPlayerActivityButtons', 'setNewPlayerActivityLock', + 'setJungleTreasureHomeButtonVisible'], { cc }); + const home = Object.assign(new Home(), { node: { getChildByName: () => ({ getChildByName: () => top }) }, + getJungleTreasureHomeButton: () => top.getChildByName('jungle'), refreshHomeActivityEntryLayout() {} }); + for (const level of [19, 20, 34, 35, 39, 40, 49, 50]) { + c.GM_INFO.level = level; + home.refreshNewPlayerActivityButtons(); + if (level >= 40) home.setJungleTreasureHomeButtonVisible(true); + for (const [name, unlock] of [['day', 36], ['jungle', 41], ['hammer', 51]]) { + const button = top.getChildByName(name), locked = level < unlock - 1; + assert.equal(button.active, level >= 20, `${level}: ${name} visible`); + assert.equal(button.getComponent(cc.Button).interactable, !locked, `${level}: ${name} clickable`); + if (locked) { + const label = button.getChildByName(name === 'jungle' ? 'time' : 'unlockLevel'); + assert.equal(label.getComponent(cc.Label).string, `${unlock}关解锁`); + } + } + } + home.setJungleTreasureHomeButtonVisible(false); + assert.equal(top.getChildByName('jungle').active, false, 'unlocked activity still follows the original visibility result'); +}); + +test('displaying a locked Jungle entry does not request or start the activity', () => { + const c = config(20), cc = runtime(c); + c.getJungleTreasureUnlockLevel = () => 40; + const Home = loadMembers(homeFile, ['initializeJungleTreasureEntryFromLogin', 'canOpenJungleTreasure'], { cc }); + let reads = 0; + const home = Object.assign(new Home(), { deferHomePopupWhileTransfer: () => false, + setJungleTreasureHomeButtonVisible() {}, refreshJungleTreasureHomeRedDot() {}, + requestJungleTreasureData() { reads++; } }); + for (const level of [20, 35, 39]) { c.GM_INFO.level = level; home.initializeJungleTreasureEntryFromLogin(); } + assert.equal(reads, 0); + c.GM_INFO.level = 40; home.initializeJungleTreasureEntryFromLogin(); assert.equal(reads, 1); +}); + +test('front-end Career is hidden without counting through 15 and reused node appears at 16', () => { + const c = config(), cc = runtime(c); + const data = json('assets/win/prefab/Win.prefab'); + const root = inflate(data, data.findIndex(n => n.__type__ === 'cc.Node' && n._children?.some(ref => data[ref.__id__]._name === 'Career'))); + const Map = loadMembers(mapFile, ['createCareer'], { cc, NumberToImage: { numberToImageNodes6() {} } }); + const map = Object.assign(new Map(), { getWinRoot: () => root, settlementAnimationTime: value => value }); + for (const level of [1, 14, 15]) { + c.GM_INFO.level = level; map.createCareer(); + assert.equal(c.GM_INFO.addLevel, 0); + for (const name of ['Career', 'Rank', 'Ruzhi']) assert.equal(root.getChildByName(name).active, false); + } + c.GM_INFO.level = 16; map.createCareer(); + assert.equal(c.GM_INFO.addLevel, 1); + assert.equal(root.getChildByName('Career').active, true); +}); + +test('server counts only victories after 15, including batches crossing the boundary', async () => { + for (const [group, oldLevel, level, expected] of [ + ['B', 0, 1, 0], ['B', 14, 15, 0], ['B', 15, 16, 1], ['B', 13, 18, 3], + ['B', 15, 20, 5], ['B', 18, 18, 0], ['B', 50, 51, 1], ['A', 14, 15, 1], [0, 0, 1, 1], + ]) { + const user = { _id: 'user', levelAmount: oldLevel, addLevel: 0, abTests: assignment(group), film: 100, isWhite: true, address: '浙江' }; + let update; + const cloud = { database: () => ({ collection: () => ({ where: () => ({ getOne: async () => ({ data: user }), + update: async payload => { update = payload; return { updated: 1 }; } }) }) }) }; + const api = loadModule('server/laf-cloud/functions/userLevel.ts', { + '@lafjs/cloud': { default: cloud }, '@/Utils': { default: ServerUtils }, + ip2region: { default: class { search() { return { province: '浙江省' }; } } }, + }, { process: { env: { SRANK_ID: 'rank' } } }).default; + const result = await api({ body: { uid: 'user', action: 'save', levelAmount: level }, headers: {} }); + assert.equal(result.code, 1); + assert.equal(update.addLevel, expected, `${group}: ${oldLevel} -> ${level}`); + assert.equal(update.film, 100 + level - oldLevel, 'film still follows all cleared levels'); + assert.equal(update.levelAmount, level); + } +}); + +test('seven-day activity uses B=15 while other players keep the configured trigger', () => { + const service = loadModule('server/laf-cloud/functions/SignInActivity.ts', { + '@lafjs/cloud': { default: { database: () => ({}) } }, '@/Utils': { default: ServerUtils }, crypto: require('node:crypto'), + }); + for (const group of ['A', 'B', 0, '2']) { + assert.equal(service.getUserSignInTriggerLevel({ abTests: assignment(group) }, { triggerLevel: 23 }), ['B', '2'].includes(group) ? 15 : 23); + } +}); + +test('B seven-day activity activates at cleared level 15 and retains the original duration', async () => { + const activity = json('server/laf-cloud/sign-in-activity-config.json'); + const cloud = { database: () => ({ collection: () => ({ where: () => ({ + get: async () => ({ data: [activity] }), update: async () => ({ updated: 1 }), + }) }) }) }; + const service = loadModule('server/laf-cloud/functions/SignInActivity.ts', { + '@lafjs/cloud': { default: cloud }, '@/Utils': { default: ServerUtils }, crypto: require('node:crypto'), + }); + for (const [group, level, expected] of [['B', 14, false], ['B', 15, true], ['A', 15, false], ['A', 23, true]]) { + const result = await service.activateSignInActivity({ _id: 'player', levelAmount: level, abTests: assignment(group) }); + assert.equal(result.activated, expected); + if (expected) assert.equal(result.state.endAt - result.state.startAt, activity.durationDays * 86400000); + } +}); + +test('only B clearing level 15 routes the next-level button home', () => { + for (const [group, level, help, expected] of [['B', 15, 0, 1], ['A', 15, 0, 0], ['B', 16, 0, 0], ['B', 15, 15, 0]]) { + const c = config(level, group), cc = runtime(c); c.GM_INFO.otherLevel = help; + cc.fx.GameTool.maxLevel = () => false; + cc.fx.AudioManager = { _instance: { playEffect() {} } }; + const Map = loadMembers(mapFile, ['winLevel'], { cc, sp: { Skeleton: 'skeleton' }, setTimeout() {} }); + const node = { getChildByName() { return node; }, getComponent() { return { setAnimation() {} }; } }; + node.parent = node; + let homes = 0; + const button = { _touch: true, setTouch(value) { this._touch = value; } }; + const map = Object.assign(new Map(), { node, isMapRuntimeAlive: () => true, clearLayerTwoWinCoinFly() {}, uploadToCloud() {}, + getSettlementNextButton: () => ({ getComponent: () => button }), returnHome() { homes++; } }); + map.winLevel(); assert.equal(homes, expected); + } +}); + +test('B special hammer remains unusable until level 51 despite ten accumulated wins', () => { + const c = config(49), cc = runtime(c); c.GM_INFO.winStreak = 10; + const Map = loadMembers(mapFile, ['useHammerSpecial'], { cc }); + let checks = 0; + const map = new Map(); + Object.defineProperty(map, 'gameOver', { get() { checks++; return true; } }); + map.useHammerSpecial(); assert.equal(checks, 0); + c.GM_INFO.level = 50; map.useHammerSpecial(); assert.equal(checks, 1); +}); + +test('10-win progress and failure reset retain their original behavior before unlock', () => { + const c = config(30), cc = runtime(c); + cc.fx.StorageMessage.setStorage = () => {}; + const Tool = loadMembers('assets/Script/module/Tool/GameTool.ts', ['setWinStreak'], { cc, Utils: { setWinStreak() {} } }); + const tool = new Tool(); + for (let i = 0; i < 10; i++) tool.setWinStreak('sucess'); + assert.equal(c.GM_INFO.winStreak, 10); + assert.equal(c.GM_INFO.winStreakFirst, true); + assert.equal(c.isWinStreakUnlocked(), false); + c.GM_INFO.level = 50; + assert.equal(c.isWinStreakUnlocked(), true); + c.GM_INFO.level = 30; + tool.setWinStreak('fail'); + assert.equal(c.GM_INFO.winStreak, 0); +}); diff --git a/tools/test-prop-guide.cjs b/tools/test-prop-guide.cjs index 410a6f8..a45ce18 100644 --- a/tools/test-prop-guide.cjs +++ b/tools/test-prop-guide.cjs @@ -5,6 +5,7 @@ const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const ts = require('typescript'); +const { withNewPlayerConfig } = require('./new-player-ab-test-support.cjs'); const root = path.resolve(__dirname, '..'); const read = file => JSON.parse(fs.readFileSync(path.join(root, file), 'utf8')); @@ -66,12 +67,13 @@ function inflate(data, id) { return node; } -function fixture(level) { +function fixture(level, group = 'A') { const prefab = read('assets/prefab/pop/GuideMask.prefab'); const scene = read('assets/Scene/GameScene.fire'); const gameNode = inflate(scene, scene.findIndex(node => node._name === 'GameNode')); const info = { level: level - 1, otherLevel: 0, hammerAmount: 3, freezeAmount: 3, magicAmount: 3, tasks: { useProp: { value: 0, target: 0 } } }; + info.abTestAssignments = { layer_1: { name: 'new1_50', group } }; const cc = { Node, Button: 'cc.Button', Widget: 'cc.Widget', Sprite: 'cc.Sprite', Component: { EventHandler: class {} }, js: { getClassName: () => 'MapConroler' }, error() {}, isValid: value => !!value && !value.destroyed, instantiate: () => inflate(prefab, 1), @@ -79,6 +81,7 @@ function fixture(level) { GameTool: { setUserProp() {}, shushu_Track() {} }, AudioManager: { _instance: { playEffect() {} } } }, }; + withNewPlayerConfig(cc.fx.GameConfig); const Map = loadClass('assets/Script/Map.ts', ['propGuideMask', 'guideHammerPending', 'showPropGuideMask', 'blockPropGuideTouch', 'setPropGuideTouchBlocked', 'destroyPropGuideMask', 'useHammer', 'cancleHammer', 'costHammer', 'useTimeProp', 'useMagic'], { cc, MiniGameSdk: { API: { showToast() {} } }, NumberToImage: { getTimeMargi3() {} }, sp: { Skeleton: 'sp.Skeleton' }, @@ -203,6 +206,19 @@ test('ordinary hammer is cancellable even on level 8', () => { assert.equal(info.hammerAmount, 3); }); +test('B guides move to 8/14/18 with the matching prop button', () => { + for (const [level, name, handler] of [[8, 'destroyBtn', 'useHammer'], [14, 'timeBtn', 'useTimeProp'], [18, 'magicBtn', 'useMagic']]) { + const { map } = fixture(level, 'B'); + map.showPropGuideMask(level); + const button = map.propGuideMask.getChildByName('bottom').getChildByName(name); + assert.equal(button.active, true); + assert.equal(button.getComponent('cc.Button').clickEvents[0].handler, handler); + } + for (const level of [11, 16]) { + const { map } = fixture(level, 'B'); map.showPropGuideMask(level); assert.equal(map.propGuideMask, null); + } +}); + test('ordinary levels, stale callbacks and friend levels do not show guide', () => { for (const level of [1, 7, 9, 10, 12, 15, 17]) { const { map } = fixture(level); map.showPropGuideMask(level); assert.equal(map.propGuideMask, null);