feat: add new1_50 B onboarding gates

This commit is contained in:
COMPUTER\EDY 2026-09-10 19:45:03 +08:00
parent f7335a5262
commit 6b921eb978
12 changed files with 489 additions and 45 deletions

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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<any> {
static loadLevelFromBundle(level: number, mainline: boolean = true): Promise<any> {
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) {

View File

@ -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("校验成功")

View File

@ -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") {

View File

@ -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 };

View File

@ -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;

View File

@ -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 };
}

View File

@ -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);
});

View File

@ -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);