diff --git a/dist/index.html b/dist/index.html index 85fd3d2..1a8fb26 100644 --- a/dist/index.html +++ b/dist/index.html @@ -799,6 +799,7 @@ var _openScorePanel = require("./openScorePanel"); var _monitorLevelsUnlocks = require("./monitorLevelsUnlocks"); var _levelEditor = require("./levelEditor"); var _upgrades = require("./upgrades"); +var _getLevelUnlockCondition = require("./get_level_unlock_condition"); async function play() { if (await applyFullScreenChoice()) return; if (gameState.running) return; @@ -1457,7 +1458,7 @@ async function openUnlockedLevelsList() { const hintField = (0, _options.isOptionOn)("mobile-mode") ? "help" : "tooltip"; const unlockedBefore = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", [])); const levelActions = (0, _loadGameData.allLevels).map((l, li)=>{ - const lockedBecause = unlockedBefore.has(l.name) ? null : (0, _gameUtils.reasonLevelIsLocked)(li, (0, _gameOver.getHistory)(), true); + const lockedBecause = unlockedBefore.has(l.name) ? null : (0, _getLevelUnlockCondition.reasonLevelIsLocked)(li, l.name, (0, _gameOver.getHistory)(), true); const percentUnlocked = lockedBecause?.reached ? `` : ""; return { text: l.name + percentUnlocked, @@ -1587,7 +1588,7 @@ tick(); (0, _tooltip.setupTooltips)(); document.getElementById("menu")?.setAttribute("data-tooltip", (0, _i18N.t)("play.menu_tooltip")); -},{"./loadGameData":"l1B4x","./sounds":"dQKPV","./game_utils":"cEeac","./PWA/sw_loader":"2n0gK","./i18n/i18n":"eNPRm","./settings":"5blfu","./gameStateMutators":"9ZeQl","./render":"9AS2t","./recording":"godmD","./newGameState":"aQN6X","./asyncAlert":"rSqLY","./options":"d5NoS","./pure_functions":"6pQh7","./help":"bqkdF","./creative":"63kYJ","./tooltip":"3RWxb","./startingPerks":"lv30m","./migrations":"a9qdY","./gameOver":"caCAf","./generateSaveFileContent":"iEcoB","./runHistoryViewer":"b80Ki","./openScorePanel":"aHTmD","./monitorLevelsUnlocks":"jjD0P","./levelEditor":"cirX1","./upgrades":"1u3Dx","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l1B4x":[function(require,module,exports,__globalThis) { +},{"./loadGameData":"l1B4x","./sounds":"dQKPV","./game_utils":"cEeac","./PWA/sw_loader":"2n0gK","./i18n/i18n":"eNPRm","./settings":"5blfu","./gameStateMutators":"9ZeQl","./render":"9AS2t","./recording":"godmD","./newGameState":"aQN6X","./asyncAlert":"rSqLY","./options":"d5NoS","./pure_functions":"6pQh7","./help":"bqkdF","./creative":"63kYJ","./tooltip":"3RWxb","./startingPerks":"lv30m","./migrations":"a9qdY","./gameOver":"caCAf","./generateSaveFileContent":"iEcoB","./runHistoryViewer":"b80Ki","./openScorePanel":"aHTmD","./monitorLevelsUnlocks":"jjD0P","./levelEditor":"cirX1","./upgrades":"1u3Dx","./get_level_unlock_condition":"a0fq0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l1B4x":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "upgrades", ()=>upgrades); @@ -3443,9 +3444,6 @@ parcelHelpers.export(exports, "shouldPierceByColor", ()=>shouldPierceByColor); parcelHelpers.export(exports, "isMovingWhilePassiveIncome", ()=>isMovingWhilePassiveIncome); parcelHelpers.export(exports, "getHighScore", ()=>getHighScore); parcelHelpers.export(exports, "highScoreText", ()=>highScoreText); -parcelHelpers.export(exports, "getLevelUnlockCondition", ()=>getLevelUnlockCondition); -parcelHelpers.export(exports, "getBestScoreMatching", ()=>getBestScoreMatching); -parcelHelpers.export(exports, "reasonLevelIsLocked", ()=>reasonLevelIsLocked); parcelHelpers.export(exports, "getCoinRenderColor", ()=>getCoinRenderColor); parcelHelpers.export(exports, "getCornerOffset", ()=>getCornerOffset); parcelHelpers.export(exports, "isInWebView", ()=>isInWebView); @@ -3453,7 +3451,6 @@ parcelHelpers.export(exports, "hoursSpentPlaying", ()=>hoursSpentPlaying); var _loadGameData = require("./loadGameData"); var _i18N = require("./i18n/i18n"); var _pureFunctions = require("./pure_functions"); -var _getLevelBackground = require("./getLevelBackground"); var _settings = require("./settings"); var _options = require("./options"); function describeLevel(level) { @@ -3669,66 +3666,6 @@ function highScoreText() { }); return ""; } -let excluded; -function isExcluded(id) { - if (!excluded) { - excluded = new Set([ - "extra_levels", - "extra_life", - "one_more_choice", - "shunt", - "slow_down" - ]); - // Avoid excluding a perk that's needed for the required one - (0, _loadGameData.upgrades).forEach((u)=>{ - if (u.requires) excluded.add(u.requires); - }); - } - return excluded.has(id); -} -function getLevelUnlockCondition(levelIndex) { - let required = [], forbidden = [], minScore = Math.max(-1000 + 100 * levelIndex, 0); - if (levelIndex > 20) { - const possibletargets = [ - ...(0, _loadGameData.upgrades) - ].slice(0, Math.floor(levelIndex / 2)).filter((u)=>!isExcluded(u.id)).sort((a, b)=>(0, _getLevelBackground.hashCode)(levelIndex + a.id) - (0, _getLevelBackground.hashCode)(levelIndex + b.id)); - const length = Math.min(3, Math.ceil(levelIndex / 30)); - required = possibletargets.slice(0, length); - forbidden = possibletargets.slice(length, length + length); - } - return { - required, - forbidden, - minScore - }; -} -function getBestScoreMatching(history, required = [], forbidden = []) { - return Math.max(0, ...history.filter((r)=>!required.find((u)=>!r?.perks?.[u.id]) && !forbidden.find((u)=>r?.perks?.[u.id])).map((r)=>r.score)); -} -function reasonLevelIsLocked(levelIndex, history, mentionBestScore) { - const { required, forbidden, minScore } = getLevelUnlockCondition(levelIndex); - const reached = getBestScoreMatching(history, required, forbidden); - let reachedText = reached && mentionBestScore ? (0, _i18N.t)("unlocks.reached", { - reached - }) : ""; - if (reached >= minScore) return null; - else if (!required.length && !forbidden.length) return { - reached, - minScore, - text: (0, _i18N.t)("unlocks.minScore", { - minScore - }) + reachedText - }; - else return { - reached, - minScore, - text: (0, _i18N.t)("unlocks.minScoreWithPerks", { - minScore, - required: required.map((u)=>u.name).join(", "), - forbidden: forbidden.map((u)=>u.name).join(", ") - }) + reachedText - }; -} function getCoinRenderColor(gameState, coin) { if (gameState.perks.metamorphosis || (0, _options.isOptionOn)("colorful_coins") || gameState.perks.sticky_coins || gameState.perks.rainbow) return coin.color; return "#ffd300"; @@ -3746,7 +3683,7 @@ function hoursSpentPlaying() { } } -},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./pure_functions":"6pQh7","./getLevelBackground":"7OIPf","./settings":"5blfu","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) { +},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./pure_functions":"6pQh7","./settings":"5blfu","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); if ("serviceWorker" in navigator && window.location.href.endsWith("/index.html?isPWA=true")) { @@ -5527,6 +5464,7 @@ var _recording = require("./recording"); var _asyncAlert = require("./asyncAlert"); var _levelEditor = require("./levelEditor"); var _creative = require("./creative"); +var _getLevelUnlockCondition = require("./get_level_unlock_condition"); function addToTotalPlayTime(ms) { (0, _settings.setSettingValue)("breakout_71_total_play_time", (0, _settings.getSettingValue)("breakout_71_total_play_time", 0) + ms); } @@ -5616,7 +5554,7 @@ function getHistograms(gameState) { const locked = (0, _loadGameData.allLevels).map((l, li)=>({ li, l, - r: (0, _gameUtils.reasonLevelIsLocked)(li, runsHistory, false)?.text + r: (0, _getLevelUnlockCondition.reasonLevelIsLocked)(li, l.name, runsHistory, false)?.text })).filter((l)=>l.r); gameState.runStatistics.runTime = Math.round(gameState.runStatistics.runTime); const perks = { @@ -5628,7 +5566,7 @@ function getHistograms(gameState) { perks, appVersion: (0, _loadGameData.appVersion) }); - const unlocked = locked.filter(({ li })=>!(0, _gameUtils.reasonLevelIsLocked)(li, runsHistory, true)); + const unlocked = locked.filter(({ li, l })=>!(0, _getLevelUnlockCondition.isLevelLocked)(li, l.name, runsHistory)); if (unlocked.length) unlockedLevels = `

${unlocked.length === 1 ? (0, _i18N.t)("unlocks.just_unlocked") : (0, _i18N.t)("unlocks.just_unlocked_plural", { @@ -5708,7 +5646,7 @@ function getHistograms(gameState) { return unlockedLevels + runStats; } -},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./game":"edeGs","./game_utils":"cEeac","./settings":"5blfu","./recording":"godmD","./asyncAlert":"rSqLY","./levelEditor":"cirX1","./creative":"63kYJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"godmD":[function(require,module,exports,__globalThis) { +},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./game":"edeGs","./game_utils":"cEeac","./settings":"5blfu","./recording":"godmD","./asyncAlert":"rSqLY","./levelEditor":"cirX1","./creative":"63kYJ","./get_level_unlock_condition":"a0fq0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"godmD":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "recordOneFrame", ()=>recordOneFrame); @@ -6276,7 +6214,7 @@ async function editRawLevelList(nth, color = "W") { editRawLevelList(nth, color); } -},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./asyncAlert":"rSqLY","./levelIcon":"6rQoT","./data/palette.json":"ktRBU","./game":"edeGs","./game_utils":"cEeac","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./toast":"nAuvo"}],"63kYJ":[function(require,module,exports,__globalThis) { +},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./asyncAlert":"rSqLY","./levelIcon":"6rQoT","./data/palette.json":"ktRBU","./game":"edeGs","./game_utils":"cEeac","./pure_functions":"6pQh7","./toast":"nAuvo","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"63kYJ":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "creativeMode", ()=>creativeMode); @@ -6290,6 +6228,7 @@ var _gameUtils = require("./game_utils"); var _gameOver = require("./gameOver"); var _upgrades = require("./upgrades"); var _levelIcon = require("./levelIcon"); +var _getLevelUnlockCondition = require("./get_level_unlock_condition"); function creativeMode(gameState) { return { icon: (0, _loadGameData.icons)["icon:creative"], @@ -6309,7 +6248,7 @@ async function openCreativeModePerksPicker() { while(true){ const levelOptions = [ ...(0, _loadGameData.allLevels).map((l, li)=>{ - const problem = (0, _gameUtils.reasonLevelIsLocked)(li, (0, _gameOver.getHistory)(), true)?.text || ""; + const problem = (0, _getLevelUnlockCondition.reasonLevelIsLocked)(li, l.name, (0, _gameOver.getHistory)(), true)?.text || ""; return { icon: (0, _loadGameData.icons)[l.name], text: l.name, @@ -6383,7 +6322,93 @@ async function openCreativeModePerksPicker() { } } -},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./game":"edeGs","./asyncAlert":"rSqLY","./game_utils":"cEeac","./gameOver":"caCAf","./upgrades":"1u3Dx","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"ka4dG":[function(require,module,exports,__globalThis) { +},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./game":"edeGs","./asyncAlert":"rSqLY","./game_utils":"cEeac","./gameOver":"caCAf","./upgrades":"1u3Dx","./levelIcon":"6rQoT","./get_level_unlock_condition":"a0fq0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"a0fq0":[function(require,module,exports,__globalThis) { +var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); +parcelHelpers.defineInteropFlag(exports); +parcelHelpers.export(exports, "getLevelUnlockCondition", ()=>getLevelUnlockCondition); +parcelHelpers.export(exports, "getBestScoreMatching", ()=>getBestScoreMatching); +parcelHelpers.export(exports, "isLevelLocked", ()=>isLevelLocked); +parcelHelpers.export(exports, "reasonLevelIsLocked", ()=>reasonLevelIsLocked); +parcelHelpers.export(exports, "upgradeName", ()=>upgradeName); +var _loadGameData = require("./loadGameData"); +var _getLevelBackground = require("./getLevelBackground"); +var _i18N = require("./i18n/i18n"); +var _unlockConditionsJson = require("./data/unlockConditions.json"); +var _unlockConditionsJsonDefault = parcelHelpers.interopDefault(_unlockConditionsJson); +const hardCodedCondition = (0, _unlockConditionsJsonDefault.default); +let excluded; +function isExcluded(id) { + if (!excluded) { + excluded = new Set([ + "extra_levels", + "extra_life", + "one_more_choice", + "shunt", + "slow_down" + ]); + // Avoid excluding a perk that's needed for the required one + (0, _loadGameData.upgrades).forEach((u)=>{ + if (u.requires) excluded.add(u.requires); + }); + } + return excluded.has(id); +} +function getLevelUnlockCondition(levelIndex, levelName) { + if (hardCodedCondition[levelName]) return hardCodedCondition[levelName]; + const result = { + required: [], + forbidden: [], + minScore: Math.max(-1000 + 100 * levelIndex, 0) + }; + if (levelIndex > 20) { + const possibletargets = [ + ...(0, _loadGameData.upgrades) + ].slice(0, Math.floor(levelIndex / 2)).filter((u)=>!isExcluded(u.id)).sort((a, b)=>(0, _getLevelBackground.hashCode)(levelIndex + a.id) - (0, _getLevelBackground.hashCode)(levelIndex + b.id)).map((u)=>u.id); + const length = Math.min(3, Math.ceil(levelIndex / 30)); + result.required = possibletargets.slice(0, length); + result.forbidden = possibletargets.slice(length, length + length); + } + return result; +} +function getBestScoreMatching(history, required = [], forbidden = []) { + return Math.max(0, ...history.filter((r)=>!required.find((id)=>!r?.perks?.[id]) && !forbidden.find((id)=>r?.perks?.[id])).map((r)=>r.score)); +} +function isLevelLocked(levelIndex, levelName, history) { + const { required, forbidden, minScore } = getLevelUnlockCondition(levelIndex, levelName); + return getBestScoreMatching(history, required, forbidden) < minScore; +} +function reasonLevelIsLocked(levelIndex, levelName, history, mentionBestScore) { + const { required, forbidden, minScore } = getLevelUnlockCondition(levelIndex, levelName); + const reached = getBestScoreMatching(history, required, forbidden); + let reachedText = reached && mentionBestScore ? (0, _i18N.t)("unlocks.reached", { + reached + }) : ""; + if (reached >= minScore) return null; + else if (!required.length && !forbidden.length) return { + reached, + minScore, + text: (0, _i18N.t)("unlocks.minScore", { + minScore + }) + reachedText + }; + else return { + reached, + minScore, + text: (0, _i18N.t)("unlocks.minScoreWithPerks", { + minScore, + required: required.map((u)=>upgradeName(u)).join(", "), + forbidden: forbidden.map((u)=>upgradeName(u)).join(", ") + }) + reachedText + }; +} +function upgradeName(id) { + return (0, _loadGameData.upgrades).find((u)=>u.id == id).name; +} + +},{"./loadGameData":"l1B4x","./getLevelBackground":"7OIPf","./i18n/i18n":"eNPRm","./data/unlockConditions.json":"glZU2","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"glZU2":[function(require,module,exports,__globalThis) { +module.exports = JSON.parse('{"71 mini":{"minScore":0,"required":[],"forbidden":[]},"Butterfly":{"minScore":0,"required":[],"forbidden":[]},"Castle":{"minScore":0,"required":[],"forbidden":[]},"Eyes":{"minScore":0,"required":[],"forbidden":[]},"Creeper":{"minScore":0,"required":[],"forbidden":[]},"Stairs":{"minScore":0,"required":[],"forbidden":[]},"Dots":{"minScore":0,"required":[],"forbidden":[]},"Lines":{"minScore":0,"required":[],"forbidden":[]},"Heart":{"minScore":0,"required":[],"forbidden":[]},"Swiss":{"minScore":0,"required":[],"forbidden":[]},"Germany":{"minScore":0,"required":[],"forbidden":[]},"France":{"minScore":100,"required":[],"forbidden":[]},"Smiley":{"minScore":200,"required":[],"forbidden":[]},"Labyrinthe":{"minScore":300,"required":[],"forbidden":[]},"Temple":{"minScore":400,"required":[],"forbidden":[]},"Pacman":{"minScore":500,"required":[],"forbidden":[]},"Ship":{"minScore":600,"required":[],"forbidden":[]},"We come in peace":{"minScore":700,"required":[],"forbidden":[]},"Space mushroom":{"minScore":800,"required":[],"forbidden":[]},"Wololo":{"minScore":900,"required":[],"forbidden":[]},"Small heart":{"minScore":1000,"required":[],"forbidden":[]},"Eye":{"minScore":1100,"required":["streak_shots"],"forbidden":["base_combo"]},"Enderman":{"minScore":1200,"required":["streak_shots"],"forbidden":["viscosity"]},"Mushroom":{"minScore":1300,"required":["streak_shots"],"forbidden":["base_combo"]},"Tulip":{"minScore":1400,"required":["viscosity"],"forbidden":["left_is_lava"]},"Chain":{"minScore":1500,"required":["left_is_lava"],"forbidden":["right_is_lava"]},"Marion":{"minScore":1600,"required":["viscosity"],"forbidden":["left_is_lava"]},"Renan":{"minScore":1700,"required":["viscosity"],"forbidden":["skip_last"]},"Violet Pairs":{"minScore":1800,"required":["skip_last"],"forbidden":["base_combo"]},"Red Cups":{"minScore":1900,"required":["skip_last"],"forbidden":["viscosity"]},"Cactus":{"minScore":2000,"required":["right_is_lava"],"forbidden":["skip_last"]},"Sunny Face":{"minScore":2100,"required":["streak_shots","base_combo"],"forbidden":["smaller_puck","skip_last"]},"Mountain":{"minScore":2200,"required":["smaller_puck","streak_shots"],"forbidden":["left_is_lava","skip_last"]},"Dollar":{"minScore":2300,"required":["pierce","smaller_puck"],"forbidden":["left_is_lava","base_combo"]},"Waves":{"minScore":2400,"required":["left_is_lava","smaller_puck"],"forbidden":["picky_eater","pierce"]},"Box":{"minScore":2500,"required":["left_is_lava","picky_eater"],"forbidden":["smaller_puck","base_combo"]},"Rose":{"minScore":2600,"required":["compound_interest","picky_eater"],"forbidden":["left_is_lava","base_combo"]},"Time":{"minScore":2700,"required":["picky_eater","right_is_lava"],"forbidden":["pierce","left_is_lava"]},"Watermelon":{"minScore":2800,"required":["hot_start","base_combo"],"forbidden":["pierce","right_is_lava"]},"Worms":{"minScore":2900,"required":["picky_eater","hot_start"],"forbidden":["right_is_lava","pierce"]},"Ocean Sunrise":{"minScore":3000,"required":["smaller_puck","hot_start"],"forbidden":["streak_shots","telekinesis"]},"Crosses":{"minScore":3100,"required":["pierce","sapper"],"forbidden":["smaller_puck","compound_interest"]},"Negative space":{"minScore":3200,"required":["left_is_lava","smaller_puck"],"forbidden":["right_is_lava","hot_start"]},"UK":{"minScore":3300,"required":["right_is_lava","left_is_lava"],"forbidden":["base_combo","smaller_puck"]},"Greece":{"minScore":3400,"required":["left_is_lava","right_is_lava"],"forbidden":["telekinesis","hot_start"]},"Russia":{"minScore":3500,"required":["compound_interest","bigger_explosions"],"forbidden":["sapper","pierce"]},"Ukraine":{"minScore":3600,"required":["pierce","sapper"],"forbidden":["base_combo","bigger_explosions"]},"Poland":{"minScore":3700,"required":["viscosity","picky_eater"],"forbidden":["skip_last","pierce"]},"Yellow 71":{"minScore":3800,"required":["base_combo","viscosity"],"forbidden":["picky_eater","skip_last"]},"71 on white":{"minScore":3900,"required":["viscosity","picky_eater"],"forbidden":["compound_interest","skip_last"]},"Blue 71":{"minScore":4000,"required":["compound_interest","pierce_color"],"forbidden":["left_is_lava","viscosity"]},"Seventy one":{"minScore":4100,"required":["viscosity","base_combo"],"forbidden":["left_is_lava","pierce_color"]},"B71":{"minScore":4200,"required":["skip_last","viscosity"],"forbidden":["telekinesis","left_is_lava"]},"Pig":{"minScore":4300,"required":["skip_last","viscosity"],"forbidden":["ball_repulse_ball","telekinesis"]},"Big Pig":{"minScore":4400,"required":["pierce","sapper"],"forbidden":["skip_last","compound_interest"]},"Donkey Kong":{"minScore":4500,"required":["ball_attract_ball","right_is_lava"],"forbidden":["bigger_explosions","skip_last"]},"Banana":{"minScore":4600,"required":["right_is_lava","soft_reset"],"forbidden":["base_combo","skip_last"]},"Fox":{"minScore":4700,"required":["ball_repulse_ball","puck_repulse_ball"],"forbidden":["right_is_lava","skip_last"]},"Wiki":{"minScore":4800,"required":["base_combo","sapper"],"forbidden":["compound_interest","pierce"]},"Baby Dog":{"minScore":4900,"required":["bigger_explosions","pierce"],"forbidden":["sapper","compound_interest"]},"dog 21":{"minScore":5000,"required":["ball_attract_ball","respawn"],"forbidden":["telekinesis","right_is_lava"]},"A":{"minScore":5100,"required":["telekinesis","base_combo","sturdy_bricks"],"forbidden":["hot_start","top_is_lava","bigger_puck"]},"B":{"minScore":5200,"required":["pierce","hot_start","telekinesis"],"forbidden":["sapper","ball_repulse_ball","puck_repulse_ball"]},"C":{"minScore":5300,"required":["hot_start","telekinesis","compound_interest"],"forbidden":["top_is_lava","bigger_puck","bigger_explosions"]},"D":{"minScore":5400,"required":["hot_start","bigger_explosions","ball_attract_ball"],"forbidden":["telekinesis","soft_reset","compound_interest"]},"e":{"minScore":5500,"required":["respawn","hot_start","telekinesis"],"forbidden":["ball_attract_ball","top_is_lava","bigger_puck"]},"Elephant":{"minScore":5600,"required":["ball_repulse_ball","puck_repulse_ball","soft_reset"],"forbidden":["base_combo","sapper","telekinesis"]},"Orca":{"minScore":5700,"required":["pierce","sapper","respawn"],"forbidden":["concave_puck","telekinesis","compound_interest"]},"Shark":{"minScore":5800,"required":["bigger_explosions","compound_interest","base_combo"],"forbidden":["right_is_lava","concave_puck","ball_attract_ball"]},"Bird":{"minScore":5900,"required":["right_is_lava","concave_puck","sturdy_bricks"],"forbidden":["ball_attract_ball","streak_shots","soft_reset"]},"Tux":{"minScore":6000,"required":["pierce","coin_magnet","bigger_puck"],"forbidden":["sapper","top_is_lava","helium"]},"Armenia":{"minScore":6100,"required":["top_is_lava","respawn","bigger_puck"],"forbidden":["base_combo","coin_magnet","ball_repulse_ball"]},"Austria":{"minScore":6200,"required":["top_is_lava","telekinesis","bigger_puck"],"forbidden":["coin_magnet","viscosity","unbounded"]},"Benin":{"minScore":6300,"required":["telekinesis","right_is_lava","bigger_explosions"],"forbidden":["viscosity","top_is_lava","unbounded"]},"Botswana":{"minScore":6400,"required":["viscosity","telekinesis","unbounded"],"forbidden":["sturdy_bricks","soft_reset","right_is_lava"]},"Bulgaria":{"minScore":6500,"required":["helium","puck_repulse_ball","unbounded"],"forbidden":["ball_repulse_ball","viscosity","pierce"]},"Canada":{"minScore":6600,"required":["skip_last","respawn","base_combo"],"forbidden":["asceticism","unbounded","telekinesis"]},"Chad":{"minScore":6700,"required":["compound_interest","bigger_explosions","skip_last"],"forbidden":["concave_puck","streak_shots","telekinesis"]},"China":{"minScore":6800,"required":["nbricks","concave_puck","streak_shots"],"forbidden":["ball_attract_ball","skip_last","base_combo"]},"Colombia":{"minScore":6900,"required":["streak_shots","smaller_puck","concave_puck"],"forbidden":["puck_repulse_ball","ball_repulse_ball","sapper"]},"Republic of the Congo":{"minScore":7000,"required":["ball_repulse_ball","puck_repulse_ball","coin_magnet"],"forbidden":["bigger_puck","top_is_lava","skip_last"]},"C\xf4te d\'Ivoire":{"minScore":7100,"required":["coin_magnet","bigger_puck","top_is_lava"],"forbidden":["base_combo","etherealcoins","telekinesis"]},"Denmark":{"minScore":7200,"required":["bigger_explosions","coin_magnet","etherealcoins"],"forbidden":["respawn","bigger_puck","top_is_lava"]},"El Salvador":{"minScore":7300,"required":["bigger_puck","top_is_lava","helium"],"forbidden":["pierce","coin_magnet","sapper"]},"Egypt":{"minScore":7400,"required":["zen","top_is_lava","soft_reset"],"forbidden":["bigger_puck","telekinesis","puck_repulse_ball"]},"Estonia":{"minScore":7500,"required":["zen","telekinesis","hot_start"],"forbidden":["top_is_lava","bigger_puck","concave_puck"]},"Finland":{"minScore":7600,"required":["concave_puck","hot_start","telekinesis"],"forbidden":["zen","compound_interest","base_combo"]},"Gabon":{"minScore":7700,"required":["respawn","ball_attract_ball","zen"],"forbidden":["streak_shots","sturdy_bricks","hot_start"]},"Georgia":{"minScore":7800,"required":["helium","zen","smaller_puck"],"forbidden":["pierce","telekinesis","sapper"]},"Guinea":{"minScore":7900,"required":["zen","nbricks","smaller_puck"],"forbidden":["pierce_color","left_is_lava","ball_repulse_ball"]},"Indonesia":{"minScore":8000,"required":["trampoline","zen","right_is_lava"],"forbidden":["nbricks","coin_magnet","hot_start"]},"Pingwin":{"minScore":8100,"required":["zen","compound_interest","pierce"],"forbidden":["right_is_lava","sturdy_bricks","helium"]},"Dog 8":{"minScore":8200,"required":["zen","ball_attract_ball","coin_magnet"],"forbidden":["sacrifice","sturdy_bricks","bigger_puck"]},"Sunglasses":{"minScore":8300,"required":["zen","sacrifice","coin_magnet"],"forbidden":["respawn","bigger_puck","top_is_lava"]},"Balloon":{"minScore":8400,"required":["soft_reset","coin_magnet","zen"],"forbidden":["concave_puck","sacrifice","bigger_puck"]},"Opening":{"minScore":8500,"required":["streak_shots","bigger_puck","top_is_lava"],"forbidden":["zen","etherealcoins","coin_magnet"]},"Stripes":{"minScore":8600,"required":["helium","base_combo","zen"],"forbidden":["top_is_lava","ball_attract_ball","bigger_puck"]},"You are here":{"minScore":8700,"required":["zen","forgiving","telekinesis"],"forbidden":["smaller_puck","viscosity","top_is_lava"]},"Gear":{"minScore":8800,"required":["pierce_color","telekinesis","left_is_lava"],"forbidden":["unbounded","respawn","zen"]},"Play":{"minScore":8900,"required":["zen","right_is_lava","skip_last"],"forbidden":["ball_attracts_coins","telekinesis","unbounded"]},"City":{"minScore":9000,"required":["passive_income","asceticism","soft_reset"],"forbidden":["hot_start","ball_repulse_ball","base_combo"]},"Wiggle":{"minScore":9100,"required":["right_is_lava","trampoline","ball_attracts_coins"],"forbidden":["sturdy_bricks","hot_start","compound_interest"]},"Graph":{"minScore":9200,"required":["hot_start","shocks","sapper"],"forbidden":["pierce","ball_attract_ball","ball_attracts_coins"]},"Lightbulb":{"minScore":9300,"required":["hot_start","passive_income","helium"],"forbidden":["trampoline","ball_attracts_coins","pierce"]},"Note":{"minScore":9400,"required":["ball_repulse_ball","puck_repulse_ball","nbricks"],"forbidden":["hot_start","respawn","etherealcoins"]},"Rocket":{"minScore":9500,"required":["etherealcoins","soft_reset","asceticism"],"forbidden":["coin_magnet","hot_start","bigger_puck"]},"Abstract":{"minScore":9600,"required":["bigger_explosions","compound_interest","etherealcoins"],"forbidden":["coin_magnet","passive_income","nbricks"]},"Fingerprint":{"minScore":9700,"required":["pierce","sapper","shocks"],"forbidden":["base_combo","implosions","helium"]},"Leaf":{"minScore":9800,"required":["concave_puck","sacrifice","puck_repulse_ball"],"forbidden":["coin_magnet","trampoline","ball_repulse_ball"]},"Abstract 2":{"minScore":9900,"required":["coin_magnet","streak_shots","sacrifice"],"forbidden":["bigger_puck","top_is_lava","right_is_lava"]},"Abstract 3":{"minScore":10000,"required":["sacrifice","nbricks","etherealcoins"],"forbidden":["shocks","sapper","asceticism"]},"Abstract 4":{"minScore":10100,"required":["trampoline","bigger_explosions","sacrifice"],"forbidden":["ball_attracts_coins","ghost_coins","ball_attract_ball"]},"Abstract 5":{"minScore":10200,"required":["ball_attracts_coins","implosions","forgiving"],"forbidden":["viscosity","base_combo","unbounded"]},"Abstract 6":{"minScore":10300,"required":["puck_repulse_ball","ball_repulse_ball","forgiving"],"forbidden":["viscosity","unbounded","passive_income"]},"Hemiola":{"minScore":10400,"required":["limitless","unbounded","viscosity"],"forbidden":["right_is_lava","forgiving","sturdy_bricks"]},"Obigre":{"minScore":10500,"required":["sapper","shocks","soft_reset"],"forbidden":["bigger_explosions","pierce","skip_last"]},"Noodlemire":{"minScore":10600,"required":["skip_last","concave_puck","passive_income"],"forbidden":["ball_attract_ball","side_flip","side_kick"]},"Bearded axe":{"minScore":10700,"required":["base_combo","streak_shots","side_flip"],"forbidden":["side_kick","implosions","concave_puck"]},"Lebanon":{"minScore":10800,"required":["side_kick","side_flip","etherealcoins"],"forbidden":["smaller_puck","streak_shots","trampoline"]},"Spain":{"minScore":10900,"required":["smaller_puck","passive_income","compound_interest"],"forbidden":["fountain_toss","side_kick","side_flip"]},"Uzbekistan":{"minScore":11000,"required":["picky_eater","ghost_coins","bigger_explosions"],"forbidden":["base_combo","clairvoyant","implosions"]},"Pakistan":{"minScore":11100,"required":["nbricks","ghost_coins","trampoline"],"forbidden":["picky_eater","clairvoyant","corner_shot"]},"Korea":{"minScore":11200,"required":["puck_repulse_ball","ball_attracts_coins","ball_repulse_ball"],"forbidden":["ghost_coins","picky_eater","etherealcoins"]},"Chile":{"minScore":11300,"required":["shocks","sapper","pierce"],"forbidden":["etherealcoins","ball_attracts_coins","ghost_coins"]},"T\xfcrkiye":{"minScore":11400,"required":["compound_interest","fountain_toss","concave_puck"],"forbidden":["bigger_explosions","superhot","respawn"]},"Taj Mahal":{"minScore":11500,"required":["asceticism","soft_reset","streak_shots"],"forbidden":["concave_puck","ball_attract_ball","hot_start"]},"Abstract 7":{"minScore":11600,"required":["hot_start","nbricks","streak_shots"],"forbidden":["trampoline","smaller_puck","superhot"]},"Abstract 9":{"minScore":11700,"required":["implosions","smaller_puck","right_is_lava"],"forbidden":["base_combo","sturdy_bricks","hot_start"]},"Crosshair":{"minScore":11800,"required":["pierce","pierce_color","left_is_lava"],"forbidden":["sapper","transparency","shocks"]},"Abstract 10":{"minScore":11900,"required":["transparency","ball_attract_ball","left_is_lava"],"forbidden":["pierce_color","rainbow","passive_income"]},"Face":{"minScore":12000,"required":["rainbow","corner_shot","bricks_attract_coins"],"forbidden":["base_combo","clairvoyant","respawn"]},"Eiffel tower":{"minScore":12100,"required":["shocks","sapper","clairvoyant"],"forbidden":["passive_income","picky_eater","pierce"]},"Abstract 11":{"minScore":12200,"required":["picky_eater","nbricks","addiction"],"forbidden":["minefield","sturdy_bricks","ghost_coins"]},"Abstract 12":{"minScore":12300,"required":["ghost_coins","sacrifice","ball_attracts_coins"],"forbidden":["concave_puck","picky_eater","compound_interest"]},"Abstract 13":{"minScore":12400,"required":["ball_attract_ball","streak_shots","ghost_coins"],"forbidden":["passive_income","sacrifice","picky_eater"]},"Abstract 14":{"minScore":12500,"required":["smaller_puck","asceticism","rainbow"],"forbidden":["bricks_attract_ball","soft_reset","respawn"]},"S":{"minScore":12600,"required":["pierce","sapper","shocks"],"forbidden":["etherealcoins","trampoline","pierce_color"]},"Abstract 15":{"minScore":12700,"required":["forgiving","viscosity"],"forbidden":["fountain_toss","transparency","left_is_lava"]},"Mario!":{"minScore":12800,"required":["unbounded","limitless","viscosity"],"forbidden":["forgiving","skip_last","ball_attract_ball"]},"Minesweeper":{"minScore":12900,"required":["ottawa_treaty","skip_last","unbounded"],"forbidden":["limitless","viscosity","forgiving"]},"Target":{"minScore":13000,"required":["skip_last","implosions","base_combo"],"forbidden":["etherealcoins","reach","three_cushion"]},"The Boys":{"minScore":13100,"required":["concave_puck","respawn","rainbow"],"forbidden":["reach","corner_shot","skip_last"]},"A Very Dangerous High-Five":{"minScore":13200,"required":["side_kick","side_flip","corner_shot"],"forbidden":["streak_shots","reach","clairvoyant"]},"Blinky":{"required":["clairvoyant","reach","double_or_nothing"],"forbidden":["nbricks","corner_shot","smaller_puck"],"minScore":13300}}'); + +},{}],"ka4dG":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "addToTotalScore", ()=>addToTotalScore); @@ -6418,10 +6443,11 @@ var _options = require("./options"); var _gameOver = require("./gameOver"); var _settings = require("./settings"); var _startingPerks = require("./startingPerks"); +var _getLevelUnlockCondition = require("./get_level_unlock_condition"); function getRunLevels(params, randomGift) { const unlockedBefore = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", [])); const history = (0, _gameOver.getHistory)(); - const unlocked = (0, _loadGameData.allLevels).filter((l, li)=>unlockedBefore.has(l.name) || !(0, _gameUtils.reasonLevelIsLocked)(li, history, false)); + const unlocked = (0, _loadGameData.allLevels).filter((l, li)=>unlockedBefore.has(l.name) || !(0, _getLevelUnlockCondition.isLevelLocked)(li, l.name, history)); const firstLevel = params?.level ? [ params.level ] : (0, _loadGameData.allLevelsAndIcons).filter((l)=>l.name == "icon:" + randomGift); @@ -6545,7 +6571,7 @@ function newGameState(params) { return gameState; } -},{"./loadGameData":"l1B4x","./game_utils":"cEeac","./gameStateMutators":"9ZeQl","./options":"d5NoS","./gameOver":"caCAf","./settings":"5blfu","./startingPerks":"lv30m","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"lv30m":[function(require,module,exports,__globalThis) { +},{"./loadGameData":"l1B4x","./game_utils":"cEeac","./gameStateMutators":"9ZeQl","./options":"d5NoS","./gameOver":"caCAf","./settings":"5blfu","./startingPerks":"lv30m","./get_level_unlock_condition":"a0fq0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"lv30m":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "startingPerkMenuButton", ()=>startingPerkMenuButton); @@ -6670,9 +6696,9 @@ var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var _versionJson = require("./data/version.json"); var _versionJsonDefault = parcelHelpers.interopDefault(_versionJson); var _generateSaveFileContent = require("./generateSaveFileContent"); -var _gameUtils = require("./game_utils"); var _loadGameData = require("./loadGameData"); var _toast = require("./toast"); +var _getLevelUnlockCondition = require("./get_level_unlock_condition"); // The page will be reloaded if any migrations were run let migrationsRun = 0; function migrate(name, cb) { @@ -6747,7 +6773,7 @@ migrate("set_breakout_71_unlocked_levels" + (0, _versionJsonDefault.default), () // We want to lock any level unlocked by an app upgrade too let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]"); let breakout_71_unlocked_levels = JSON.parse(localStorage.getItem("breakout_71_unlocked_levels") || "[]"); - (0, _loadGameData.allLevels).filter((l, li)=>!(0, _gameUtils.reasonLevelIsLocked)(li, runsHistory, false)).forEach((l)=>{ + (0, _loadGameData.allLevels).filter((l, li)=>!(0, _getLevelUnlockCondition.isLevelLocked)(li, l.name, runsHistory)).forEach((l)=>{ if (!breakout_71_unlocked_levels.includes(l.name)) breakout_71_unlocked_levels.push(l.name); }); localStorage.setItem("breakout_71_unlocked_levels", JSON.stringify(breakout_71_unlocked_levels)); @@ -6766,7 +6792,7 @@ migrate("set_user_id", ()=>{ }); afterMigration(); -},{"./data/version.json":"iyP6E","./generateSaveFileContent":"iEcoB","./game_utils":"cEeac","./loadGameData":"l1B4x","./toast":"nAuvo","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iEcoB":[function(require,module,exports,__globalThis) { +},{"./data/version.json":"iyP6E","./generateSaveFileContent":"iEcoB","./loadGameData":"l1B4x","./toast":"nAuvo","./get_level_unlock_condition":"a0fq0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iEcoB":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "generateSaveFileContent", ()=>generateSaveFileContent); @@ -6884,6 +6910,7 @@ var _game = require("./game"); var _loadGameData = require("./loadGameData"); var _pureFunctions = require("./pure_functions"); var _settings = require("./settings"); +var _getLevelUnlockCondition = require("./get_level_unlock_condition"); async function openScorePanel(gameState) { (0, _game.pause)(true); await (0, _asyncAlert.asyncAlert)({ @@ -6909,12 +6936,12 @@ function getNearestUnlockHTML(gameState) { const unlocked = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", [])); const firstUnlockable = (0, _pureFunctions.firstWhere)((0, _loadGameData.allLevels), (l, li)=>{ if (unlocked.has(l.name)) return; - const reason = (0, _gameUtils.reasonLevelIsLocked)(li, (0, _gameOver.getHistory)(), false); + const reason = (0, _getLevelUnlockCondition.reasonLevelIsLocked)(li, l.name, (0, _gameOver.getHistory)(), false); if (!reason) return; - const { minScore, forbidden, required } = (0, _gameUtils.getLevelUnlockCondition)(li); - const missing = required.filter((u)=>!gameState?.perks?.[u.id]); + const { minScore, forbidden, required } = (0, _getLevelUnlockCondition.getLevelUnlockCondition)(li, l.name); + const missing = required.filter((id)=>!gameState?.perks?.[id]); // we can't have a forbidden perk - if (forbidden.find((u)=>gameState?.perks?.[u.id])) return; + if (forbidden.find((id)=>gameState?.perks?.[id])) return; // All required upgrades need to be unlocked if (missing.find((u)=>u.threshold > (0, _settings.getTotalScore)())) return; return { @@ -6929,7 +6956,7 @@ function getNearestUnlockHTML(gameState) { }); if (!firstUnlockable) return ""; let missingPoints = Math.max(0, firstUnlockable.minScore - gameState.score); - let missingUpgrades = firstUnlockable.missing.map((u)=>u.name).join(", "); + let missingUpgrades = firstUnlockable.missing.map((id)=>(0, _getLevelUnlockCondition.upgradeName)(id)).join(", "); const title = missingUpgrades && (0, _i18N.t)("score_panel.get_upgrades_to_unlock", { missingUpgrades, points: missingPoints, @@ -6951,25 +6978,26 @@ function getNearestUnlockHTML(gameState) { `; } -},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./game_utils":"cEeac","./gameOver":"caCAf","./game":"edeGs","./loadGameData":"l1B4x","./pure_functions":"6pQh7","./settings":"5blfu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"jjD0P":[function(require,module,exports,__globalThis) { +},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./game_utils":"cEeac","./gameOver":"caCAf","./game":"edeGs","./loadGameData":"l1B4x","./pure_functions":"6pQh7","./settings":"5blfu","./get_level_unlock_condition":"a0fq0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"jjD0P":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "monitorLevelsUnlocks", ()=>monitorLevelsUnlocks); var _settings = require("./settings"); var _loadGameData = require("./loadGameData"); -var _gameUtils = require("./game_utils"); var _i18N = require("./i18n/i18n"); var _toast = require("./toast"); var _gameStateMutators = require("./gameStateMutators"); +var _getLevelUnlockCondition = require("./get_level_unlock_condition"); let list; -let unlocked = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", [])); +let unlocked = null; function monitorLevelsUnlocks(gameState) { + if (!unlocked) unlocked = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", [])); if (gameState.creative) return; if (!list) list = (0, _loadGameData.allLevels).map((l, li)=>({ name: l.name, li, l, - ...(0, _gameUtils.getLevelUnlockCondition)(li) + ...(0, _getLevelUnlockCondition.getLevelUnlockCondition)(li, l.name) })); list.forEach(({ name, minScore, forbidden, required, l })=>{ // Already unlocked @@ -6992,7 +7020,7 @@ function monitorLevelsUnlocks(gameState) { }); } -},{"./settings":"5blfu","./loadGameData":"l1B4x","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./toast":"nAuvo","./gameStateMutators":"9ZeQl","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["iivkY"], "iivkY", "parcelRequire94c2") +},{"./settings":"5blfu","./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./toast":"nAuvo","./gameStateMutators":"9ZeQl","./get_level_unlock_condition":"a0fq0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["iivkY"], "iivkY", "parcelRequire94c2") diff --git a/src/creative.ts b/src/creative.ts index 75e5487..d114b00 100644 --- a/src/creative.ts +++ b/src/creative.ts @@ -12,12 +12,12 @@ import { asyncAlert, requiredAsyncAlert } from "./asyncAlert"; import { describeLevel, highScoreText, - reasonLevelIsLocked, sumOfValues, } from "./game_utils"; import { getHistory } from "./gameOver"; import { noCreative } from "./upgrades"; import { levelIconHTML } from "./levelIcon"; +import {reasonLevelIsLocked} from "./get_level_unlock_condition"; export function creativeMode(gameState: GameState) { return { @@ -46,7 +46,7 @@ export async function openCreativeModePerksPicker() { while (true) { const levelOptions = [ ...allLevels.map((l, li) => { - const problem = reasonLevelIsLocked(li, getHistory(), true)?.text || ""; + const problem = reasonLevelIsLocked(li, l.name,getHistory(), true)?.text || ""; return { icon: icons[l.name], text: l.name, diff --git a/src/data/unlockConditions.json b/src/data/unlockConditions.json new file mode 100644 index 0000000..00fe175 --- /dev/null +++ b/src/data/unlockConditions.json @@ -0,0 +1,1605 @@ +{ + "71 mini": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Butterfly": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Castle": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Eyes": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Creeper": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Stairs": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Dots": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Lines": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Heart": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Swiss": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "Germany": { + "minScore": 0, + "required": [], + "forbidden": [] + }, + "France": { + "minScore": 100, + "required": [], + "forbidden": [] + }, + "Smiley": { + "minScore": 200, + "required": [], + "forbidden": [] + }, + "Labyrinthe": { + "minScore": 300, + "required": [], + "forbidden": [] + }, + "Temple": { + "minScore": 400, + "required": [], + "forbidden": [] + }, + "Pacman": { + "minScore": 500, + "required": [], + "forbidden": [] + }, + "Ship": { + "minScore": 600, + "required": [], + "forbidden": [] + }, + "We come in peace": { + "minScore": 700, + "required": [], + "forbidden": [] + }, + "Space mushroom": { + "minScore": 800, + "required": [], + "forbidden": [] + }, + "Wololo": { + "minScore": 900, + "required": [], + "forbidden": [] + }, + "Small heart": { + "minScore": 1000, + "required": [], + "forbidden": [] + }, + "Eye": { + "minScore": 1100, + "required": [ + "streak_shots" + ], + "forbidden": [ + "base_combo" + ] + }, + "Enderman": { + "minScore": 1200, + "required": [ + "streak_shots" + ], + "forbidden": [ + "viscosity" + ] + }, + "Mushroom": { + "minScore": 1300, + "required": [ + "streak_shots" + ], + "forbidden": [ + "base_combo" + ] + }, + "Tulip": { + "minScore": 1400, + "required": [ + "viscosity" + ], + "forbidden": [ + "left_is_lava" + ] + }, + "Chain": { + "minScore": 1500, + "required": [ + "left_is_lava" + ], + "forbidden": [ + "right_is_lava" + ] + }, + "Marion": { + "minScore": 1600, + "required": [ + "viscosity" + ], + "forbidden": [ + "left_is_lava" + ] + }, + "Renan": { + "minScore": 1700, + "required": [ + "viscosity" + ], + "forbidden": [ + "skip_last" + ] + }, + "Violet Pairs": { + "minScore": 1800, + "required": [ + "skip_last" + ], + "forbidden": [ + "base_combo" + ] + }, + "Red Cups": { + "minScore": 1900, + "required": [ + "skip_last" + ], + "forbidden": [ + "viscosity" + ] + }, + "Cactus": { + "minScore": 2000, + "required": [ + "right_is_lava" + ], + "forbidden": [ + "skip_last" + ] + }, + "Sunny Face": { + "minScore": 2100, + "required": [ + "streak_shots", + "base_combo" + ], + "forbidden": [ + "smaller_puck", + "skip_last" + ] + }, + "Mountain": { + "minScore": 2200, + "required": [ + "smaller_puck", + "streak_shots" + ], + "forbidden": [ + "left_is_lava", + "skip_last" + ] + }, + "Dollar": { + "minScore": 2300, + "required": [ + "pierce", + "smaller_puck" + ], + "forbidden": [ + "left_is_lava", + "base_combo" + ] + }, + "Waves": { + "minScore": 2400, + "required": [ + "left_is_lava", + "smaller_puck" + ], + "forbidden": [ + "picky_eater", + "pierce" + ] + }, + "Box": { + "minScore": 2500, + "required": [ + "left_is_lava", + "picky_eater" + ], + "forbidden": [ + "smaller_puck", + "base_combo" + ] + }, + "Rose": { + "minScore": 2600, + "required": [ + "compound_interest", + "picky_eater" + ], + "forbidden": [ + "left_is_lava", + "base_combo" + ] + }, + "Time": { + "minScore": 2700, + "required": [ + "picky_eater", + "right_is_lava" + ], + "forbidden": [ + "pierce", + "left_is_lava" + ] + }, + "Watermelon": { + "minScore": 2800, + "required": [ + "hot_start", + "base_combo" + ], + "forbidden": [ + "pierce", + "right_is_lava" + ] + }, + "Worms": { + "minScore": 2900, + "required": [ + "picky_eater", + "hot_start" + ], + "forbidden": [ + "right_is_lava", + "pierce" + ] + }, + "Ocean Sunrise": { + "minScore": 3000, + "required": [ + "smaller_puck", + "hot_start" + ], + "forbidden": [ + "streak_shots", + "telekinesis" + ] + }, + "Crosses": { + "minScore": 3100, + "required": [ + "pierce", + "sapper" + ], + "forbidden": [ + "smaller_puck", + "compound_interest" + ] + }, + "Negative space": { + "minScore": 3200, + "required": [ + "left_is_lava", + "smaller_puck" + ], + "forbidden": [ + "right_is_lava", + "hot_start" + ] + }, + "UK": { + "minScore": 3300, + "required": [ + "right_is_lava", + "left_is_lava" + ], + "forbidden": [ + "base_combo", + "smaller_puck" + ] + }, + "Greece": { + "minScore": 3400, + "required": [ + "left_is_lava", + "right_is_lava" + ], + "forbidden": [ + "telekinesis", + "hot_start" + ] + }, + "Russia": { + "minScore": 3500, + "required": [ + "compound_interest", + "bigger_explosions" + ], + "forbidden": [ + "sapper", + "pierce" + ] + }, + "Ukraine": { + "minScore": 3600, + "required": [ + "pierce", + "sapper" + ], + "forbidden": [ + "base_combo", + "bigger_explosions" + ] + }, + "Poland": { + "minScore": 3700, + "required": [ + "viscosity", + "picky_eater" + ], + "forbidden": [ + "skip_last", + "pierce" + ] + }, + "Yellow 71": { + "minScore": 3800, + "required": [ + "base_combo", + "viscosity" + ], + "forbidden": [ + "picky_eater", + "skip_last" + ] + }, + "71 on white": { + "minScore": 3900, + "required": [ + "viscosity", + "picky_eater" + ], + "forbidden": [ + "compound_interest", + "skip_last" + ] + }, + "Blue 71": { + "minScore": 4000, + "required": [ + "compound_interest", + "pierce_color" + ], + "forbidden": [ + "left_is_lava", + "viscosity" + ] + }, + "Seventy one": { + "minScore": 4100, + "required": [ + "viscosity", + "base_combo" + ], + "forbidden": [ + "left_is_lava", + "pierce_color" + ] + }, + "B71": { + "minScore": 4200, + "required": [ + "skip_last", + "viscosity" + ], + "forbidden": [ + "telekinesis", + "left_is_lava" + ] + }, + "Pig": { + "minScore": 4300, + "required": [ + "skip_last", + "viscosity" + ], + "forbidden": [ + "ball_repulse_ball", + "telekinesis" + ] + }, + "Big Pig": { + "minScore": 4400, + "required": [ + "pierce", + "sapper" + ], + "forbidden": [ + "skip_last", + "compound_interest" + ] + }, + "Donkey Kong": { + "minScore": 4500, + "required": [ + "ball_attract_ball", + "right_is_lava" + ], + "forbidden": [ + "bigger_explosions", + "skip_last" + ] + }, + "Banana": { + "minScore": 4600, + "required": [ + "right_is_lava", + "soft_reset" + ], + "forbidden": [ + "base_combo", + "skip_last" + ] + }, + "Fox": { + "minScore": 4700, + "required": [ + "ball_repulse_ball", + "puck_repulse_ball" + ], + "forbidden": [ + "right_is_lava", + "skip_last" + ] + }, + "Wiki": { + "minScore": 4800, + "required": [ + "base_combo", + "sapper" + ], + "forbidden": [ + "compound_interest", + "pierce" + ] + }, + "Baby Dog": { + "minScore": 4900, + "required": [ + "bigger_explosions", + "pierce" + ], + "forbidden": [ + "sapper", + "compound_interest" + ] + }, + "dog 21": { + "minScore": 5000, + "required": [ + "ball_attract_ball", + "respawn" + ], + "forbidden": [ + "telekinesis", + "right_is_lava" + ] + }, + "A": { + "minScore": 5100, + "required": [ + "telekinesis", + "base_combo", + "sturdy_bricks" + ], + "forbidden": [ + "hot_start", + "top_is_lava", + "bigger_puck" + ] + }, + "B": { + "minScore": 5200, + "required": [ + "pierce", + "hot_start", + "telekinesis" + ], + "forbidden": [ + "sapper", + "ball_repulse_ball", + "puck_repulse_ball" + ] + }, + "C": { + "minScore": 5300, + "required": [ + "hot_start", + "telekinesis", + "compound_interest" + ], + "forbidden": [ + "top_is_lava", + "bigger_puck", + "bigger_explosions" + ] + }, + "D": { + "minScore": 5400, + "required": [ + "hot_start", + "bigger_explosions", + "ball_attract_ball" + ], + "forbidden": [ + "telekinesis", + "soft_reset", + "compound_interest" + ] + }, + "e": { + "minScore": 5500, + "required": [ + "respawn", + "hot_start", + "telekinesis" + ], + "forbidden": [ + "ball_attract_ball", + "top_is_lava", + "bigger_puck" + ] + }, + "Elephant": { + "minScore": 5600, + "required": [ + "ball_repulse_ball", + "puck_repulse_ball", + "soft_reset" + ], + "forbidden": [ + "base_combo", + "sapper", + "telekinesis" + ] + }, + "Orca": { + "minScore": 5700, + "required": [ + "pierce", + "sapper", + "respawn" + ], + "forbidden": [ + "concave_puck", + "telekinesis", + "compound_interest" + ] + }, + "Shark": { + "minScore": 5800, + "required": [ + "bigger_explosions", + "compound_interest", + "base_combo" + ], + "forbidden": [ + "right_is_lava", + "concave_puck", + "ball_attract_ball" + ] + }, + "Bird": { + "minScore": 5900, + "required": [ + "right_is_lava", + "concave_puck", + "sturdy_bricks" + ], + "forbidden": [ + "ball_attract_ball", + "streak_shots", + "soft_reset" + ] + }, + "Tux": { + "minScore": 6000, + "required": [ + "pierce", + "coin_magnet", + "bigger_puck" + ], + "forbidden": [ + "sapper", + "top_is_lava", + "helium" + ] + }, + "Armenia": { + "minScore": 6100, + "required": [ + "top_is_lava", + "respawn", + "bigger_puck" + ], + "forbidden": [ + "base_combo", + "coin_magnet", + "ball_repulse_ball" + ] + }, + "Austria": { + "minScore": 6200, + "required": [ + "top_is_lava", + "telekinesis", + "bigger_puck" + ], + "forbidden": [ + "coin_magnet", + "viscosity", + "unbounded" + ] + }, + "Benin": { + "minScore": 6300, + "required": [ + "telekinesis", + "right_is_lava", + "bigger_explosions" + ], + "forbidden": [ + "viscosity", + "top_is_lava", + "unbounded" + ] + }, + "Botswana": { + "minScore": 6400, + "required": [ + "viscosity", + "telekinesis", + "unbounded" + ], + "forbidden": [ + "sturdy_bricks", + "soft_reset", + "right_is_lava" + ] + }, + "Bulgaria": { + "minScore": 6500, + "required": [ + "helium", + "puck_repulse_ball", + "unbounded" + ], + "forbidden": [ + "ball_repulse_ball", + "viscosity", + "pierce" + ] + }, + "Canada": { + "minScore": 6600, + "required": [ + "skip_last", + "respawn", + "base_combo" + ], + "forbidden": [ + "asceticism", + "unbounded", + "telekinesis" + ] + }, + "Chad": { + "minScore": 6700, + "required": [ + "compound_interest", + "bigger_explosions", + "skip_last" + ], + "forbidden": [ + "concave_puck", + "streak_shots", + "telekinesis" + ] + }, + "China": { + "minScore": 6800, + "required": [ + "nbricks", + "concave_puck", + "streak_shots" + ], + "forbidden": [ + "ball_attract_ball", + "skip_last", + "base_combo" + ] + }, + "Colombia": { + "minScore": 6900, + "required": [ + "streak_shots", + "smaller_puck", + "concave_puck" + ], + "forbidden": [ + "puck_repulse_ball", + "ball_repulse_ball", + "sapper" + ] + }, + "Republic of the Congo": { + "minScore": 7000, + "required": [ + "ball_repulse_ball", + "puck_repulse_ball", + "coin_magnet" + ], + "forbidden": [ + "bigger_puck", + "top_is_lava", + "skip_last" + ] + }, + "Côte d'Ivoire": { + "minScore": 7100, + "required": [ + "coin_magnet", + "bigger_puck", + "top_is_lava" + ], + "forbidden": [ + "base_combo", + "etherealcoins", + "telekinesis" + ] + }, + "Denmark": { + "minScore": 7200, + "required": [ + "bigger_explosions", + "coin_magnet", + "etherealcoins" + ], + "forbidden": [ + "respawn", + "bigger_puck", + "top_is_lava" + ] + }, + "El Salvador": { + "minScore": 7300, + "required": [ + "bigger_puck", + "top_is_lava", + "helium" + ], + "forbidden": [ + "pierce", + "coin_magnet", + "sapper" + ] + }, + "Egypt": { + "minScore": 7400, + "required": [ + "zen", + "top_is_lava", + "soft_reset" + ], + "forbidden": [ + "bigger_puck", + "telekinesis", + "puck_repulse_ball" + ] + }, + "Estonia": { + "minScore": 7500, + "required": [ + "zen", + "telekinesis", + "hot_start" + ], + "forbidden": [ + "top_is_lava", + "bigger_puck", + "concave_puck" + ] + }, + "Finland": { + "minScore": 7600, + "required": [ + "concave_puck", + "hot_start", + "telekinesis" + ], + "forbidden": [ + "zen", + "compound_interest", + "base_combo" + ] + }, + "Gabon": { + "minScore": 7700, + "required": [ + "respawn", + "ball_attract_ball", + "zen" + ], + "forbidden": [ + "streak_shots", + "sturdy_bricks", + "hot_start" + ] + }, + "Georgia": { + "minScore": 7800, + "required": [ + "helium", + "zen", + "smaller_puck" + ], + "forbidden": [ + "pierce", + "telekinesis", + "sapper" + ] + }, + "Guinea": { + "minScore": 7900, + "required": [ + "zen", + "nbricks", + "smaller_puck" + ], + "forbidden": [ + "pierce_color", + "left_is_lava", + "ball_repulse_ball" + ] + }, + "Indonesia": { + "minScore": 8000, + "required": [ + "trampoline", + "zen", + "right_is_lava" + ], + "forbidden": [ + "nbricks", + "coin_magnet", + "hot_start" + ] + }, + "Pingwin": { + "minScore": 8100, + "required": [ + "zen", + "compound_interest", + "pierce" + ], + "forbidden": [ + "right_is_lava", + "sturdy_bricks", + "helium" + ] + }, + "Dog 8": { + "minScore": 8200, + "required": [ + "zen", + "ball_attract_ball", + "coin_magnet" + ], + "forbidden": [ + "sacrifice", + "sturdy_bricks", + "bigger_puck" + ] + }, + "Sunglasses": { + "minScore": 8300, + "required": [ + "zen", + "sacrifice", + "coin_magnet" + ], + "forbidden": [ + "respawn", + "bigger_puck", + "top_is_lava" + ] + }, + "Balloon": { + "minScore": 8400, + "required": [ + "soft_reset", + "coin_magnet", + "zen" + ], + "forbidden": [ + "concave_puck", + "sacrifice", + "bigger_puck" + ] + }, + "Opening": { + "minScore": 8500, + "required": [ + "streak_shots", + "bigger_puck", + "top_is_lava" + ], + "forbidden": [ + "zen", + "etherealcoins", + "coin_magnet" + ] + }, + "Stripes": { + "minScore": 8600, + "required": [ + "helium", + "base_combo", + "zen" + ], + "forbidden": [ + "top_is_lava", + "ball_attract_ball", + "bigger_puck" + ] + }, + "You are here": { + "minScore": 8700, + "required": [ + "zen", + "forgiving", + "telekinesis" + ], + "forbidden": [ + "smaller_puck", + "viscosity", + "top_is_lava" + ] + }, + "Gear": { + "minScore": 8800, + "required": [ + "pierce_color", + "telekinesis", + "left_is_lava" + ], + "forbidden": [ + "unbounded", + "respawn", + "zen" + ] + }, + "Play": { + "minScore": 8900, + "required": [ + "zen", + "right_is_lava", + "skip_last" + ], + "forbidden": [ + "ball_attracts_coins", + "telekinesis", + "unbounded" + ] + }, + "City": { + "minScore": 9000, + "required": [ + "passive_income", + "asceticism", + "soft_reset" + ], + "forbidden": [ + "hot_start", + "ball_repulse_ball", + "base_combo" + ] + }, + "Wiggle": { + "minScore": 9100, + "required": [ + "right_is_lava", + "trampoline", + "ball_attracts_coins" + ], + "forbidden": [ + "sturdy_bricks", + "hot_start", + "compound_interest" + ] + }, + "Graph": { + "minScore": 9200, + "required": [ + "hot_start", + "shocks", + "sapper" + ], + "forbidden": [ + "pierce", + "ball_attract_ball", + "ball_attracts_coins" + ] + }, + "Lightbulb": { + "minScore": 9300, + "required": [ + "hot_start", + "passive_income", + "helium" + ], + "forbidden": [ + "trampoline", + "ball_attracts_coins", + "pierce" + ] + }, + "Note": { + "minScore": 9400, + "required": [ + "ball_repulse_ball", + "puck_repulse_ball", + "nbricks" + ], + "forbidden": [ + "hot_start", + "respawn", + "etherealcoins" + ] + }, + "Rocket": { + "minScore": 9500, + "required": [ + "etherealcoins", + "soft_reset", + "asceticism" + ], + "forbidden": [ + "coin_magnet", + "hot_start", + "bigger_puck" + ] + }, + "Abstract": { + "minScore": 9600, + "required": [ + "bigger_explosions", + "compound_interest", + "etherealcoins" + ], + "forbidden": [ + "coin_magnet", + "passive_income", + "nbricks" + ] + }, + "Fingerprint": { + "minScore": 9700, + "required": [ + "pierce", + "sapper", + "shocks" + ], + "forbidden": [ + "base_combo", + "implosions", + "helium" + ] + }, + "Leaf": { + "minScore": 9800, + "required": [ + "concave_puck", + "sacrifice", + "puck_repulse_ball" + ], + "forbidden": [ + "coin_magnet", + "trampoline", + "ball_repulse_ball" + ] + }, + "Abstract 2": { + "minScore": 9900, + "required": [ + "coin_magnet", + "streak_shots", + "sacrifice" + ], + "forbidden": [ + "bigger_puck", + "top_is_lava", + "right_is_lava" + ] + }, + "Abstract 3": { + "minScore": 10000, + "required": [ + "sacrifice", + "nbricks", + "etherealcoins" + ], + "forbidden": [ + "shocks", + "sapper", + "asceticism" + ] + }, + "Abstract 4": { + "minScore": 10100, + "required": [ + "trampoline", + "bigger_explosions", + "sacrifice" + ], + "forbidden": [ + "ball_attracts_coins", + "ghost_coins", + "ball_attract_ball" + ] + }, + "Abstract 5": { + "minScore": 10200, + "required": [ + "ball_attracts_coins", + "implosions", + "forgiving" + ], + "forbidden": [ + "viscosity", + "base_combo", + "unbounded" + ] + }, + "Abstract 6": { + "minScore": 10300, + "required": [ + "puck_repulse_ball", + "ball_repulse_ball", + "forgiving" + ], + "forbidden": [ + "viscosity", + "unbounded", + "passive_income" + ] + }, + "Hemiola": { + "minScore": 10400, + "required": [ + "limitless", + "unbounded", + "viscosity" + ], + "forbidden": [ + "right_is_lava", + "forgiving", + "sturdy_bricks" + ] + }, + "Obigre": { + "minScore": 10500, + "required": [ + "sapper", + "shocks", + "soft_reset" + ], + "forbidden": [ + "bigger_explosions", + "pierce", + "skip_last" + ] + }, + "Noodlemire": { + "minScore": 10600, + "required": [ + "skip_last", + "concave_puck", + "passive_income" + ], + "forbidden": [ + "ball_attract_ball", + "side_flip", + "side_kick" + ] + }, + "Bearded axe": { + "minScore": 10700, + "required": [ + "base_combo", + "streak_shots", + "side_flip" + ], + "forbidden": [ + "side_kick", + "implosions", + "concave_puck" + ] + }, + "Lebanon": { + "minScore": 10800, + "required": [ + "side_kick", + "side_flip", + "etherealcoins" + ], + "forbidden": [ + "smaller_puck", + "streak_shots", + "trampoline" + ] + }, + "Spain": { + "minScore": 10900, + "required": [ + "smaller_puck", + "passive_income", + "compound_interest" + ], + "forbidden": [ + "fountain_toss", + "side_kick", + "side_flip" + ] + }, + "Uzbekistan": { + "minScore": 11000, + "required": [ + "picky_eater", + "ghost_coins", + "bigger_explosions" + ], + "forbidden": [ + "base_combo", + "clairvoyant", + "implosions" + ] + }, + "Pakistan": { + "minScore": 11100, + "required": [ + "nbricks", + "ghost_coins", + "trampoline" + ], + "forbidden": [ + "picky_eater", + "clairvoyant", + "corner_shot" + ] + }, + "Korea": { + "minScore": 11200, + "required": [ + "puck_repulse_ball", + "ball_attracts_coins", + "ball_repulse_ball" + ], + "forbidden": [ + "ghost_coins", + "picky_eater", + "etherealcoins" + ] + }, + "Chile": { + "minScore": 11300, + "required": [ + "shocks", + "sapper", + "pierce" + ], + "forbidden": [ + "etherealcoins", + "ball_attracts_coins", + "ghost_coins" + ] + }, + "Türkiye": { + "minScore": 11400, + "required": [ + "compound_interest", + "fountain_toss", + "concave_puck" + ], + "forbidden": [ + "bigger_explosions", + "superhot", + "respawn" + ] + }, + "Taj Mahal": { + "minScore": 11500, + "required": [ + "asceticism", + "soft_reset", + "streak_shots" + ], + "forbidden": [ + "concave_puck", + "ball_attract_ball", + "hot_start" + ] + }, + "Abstract 7": { + "minScore": 11600, + "required": [ + "hot_start", + "nbricks", + "streak_shots" + ], + "forbidden": [ + "trampoline", + "smaller_puck", + "superhot" + ] + }, + "Abstract 9": { + "minScore": 11700, + "required": [ + "implosions", + "smaller_puck", + "right_is_lava" + ], + "forbidden": [ + "base_combo", + "sturdy_bricks", + "hot_start" + ] + }, + "Crosshair": { + "minScore": 11800, + "required": [ + "pierce", + "pierce_color", + "left_is_lava" + ], + "forbidden": [ + "sapper", + "transparency", + "shocks" + ] + }, + "Abstract 10": { + "minScore": 11900, + "required": [ + "transparency", + "ball_attract_ball", + "left_is_lava" + ], + "forbidden": [ + "pierce_color", + "rainbow", + "passive_income" + ] + }, + "Face": { + "minScore": 12000, + "required": [ + "rainbow", + "corner_shot", + "bricks_attract_coins" + ], + "forbidden": [ + "base_combo", + "clairvoyant", + "respawn" + ] + }, + "Eiffel tower": { + "minScore": 12100, + "required": [ + "shocks", + "sapper", + "clairvoyant" + ], + "forbidden": [ + "passive_income", + "picky_eater", + "pierce" + ] + }, + "Abstract 11": { + "minScore": 12200, + "required": [ + "picky_eater", + "nbricks", + "addiction" + ], + "forbidden": [ + "minefield", + "sturdy_bricks", + "ghost_coins" + ] + }, + "Abstract 12": { + "minScore": 12300, + "required": [ + "ghost_coins", + "sacrifice", + "ball_attracts_coins" + ], + "forbidden": [ + "concave_puck", + "picky_eater", + "compound_interest" + ] + }, + "Abstract 13": { + "minScore": 12400, + "required": [ + "ball_attract_ball", + "streak_shots", + "ghost_coins" + ], + "forbidden": [ + "passive_income", + "sacrifice", + "picky_eater" + ] + }, + "Abstract 14": { + "minScore": 12500, + "required": [ + "smaller_puck", + "asceticism", + "rainbow" + ], + "forbidden": [ + "bricks_attract_ball", + "soft_reset", + "respawn" + ] + }, + "S": { + "minScore": 12600, + "required": [ + "pierce", + "sapper", + "shocks" + ], + "forbidden": [ + "etherealcoins", + "trampoline", + "pierce_color" + ] + }, + "Abstract 15": { + "minScore": 12700, + "required": [ + "forgiving", + "viscosity" + ], + "forbidden": [ + "fountain_toss", + "transparency", + "left_is_lava" + ] + }, + "Mario!": { + "minScore": 12800, + "required": [ + "unbounded", + "limitless", + "viscosity" + ], + "forbidden": [ + "forgiving", + "skip_last", + "ball_attract_ball" + ] + }, + "Minesweeper": { + "minScore": 12900, + "required": [ + "ottawa_treaty", + "skip_last", + "unbounded" + ], + "forbidden": [ + "limitless", + "viscosity", + "forgiving" + ] + }, + "Target": { + "minScore": 13000, + "required": [ + "skip_last", + "implosions", + "base_combo" + ], + "forbidden": [ + "etherealcoins", + "reach", + "three_cushion" + ] + }, + "The Boys": { + "minScore": 13100, + "required": [ + "concave_puck", + "respawn", + "rainbow" + ], + "forbidden": [ + "reach", + "corner_shot", + "skip_last" + ] + }, + "A Very Dangerous High-Five": { + "minScore": 13200, + "required": [ + "side_kick", + "side_flip", + "corner_shot" + ], + "forbidden": [ + "streak_shots", + "reach", + "clairvoyant" + ] + }, + "Blinky": { + "required": [ + "clairvoyant", + "reach", + "double_or_nothing" + ], + "forbidden": [ + "nbricks", + "corner_shot", + "smaller_puck" + ], + "minScore": 13300 + } +} \ No newline at end of file diff --git a/src/data/unlockConditions.test.ts b/src/data/unlockConditions.test.ts new file mode 100644 index 0000000..002c805 --- /dev/null +++ b/src/data/unlockConditions.test.ts @@ -0,0 +1,38 @@ +import conditions from "./unlockConditions.json" +import levels from "./levels.json" +import {rawUpgrades} from "../upgrades"; +import {getLevelUnlockCondition} from "../get_level_unlock_condition"; +import {UnlockCondition} from "../types"; + +describe("conditions", () => { + it("defines conditions for existing levels only", () => { + const conditionForMissingLevel=Object.keys(conditions).filter(levelName=>!levels.find(l=>l.name===levelName)) + expect(conditionForMissingLevel).toEqual([]); + }); + it("defines conditions with existing upgrades only", () => { + + const existingIds :Set= new Set(rawUpgrades.map(u=>u.id)); + const missing:Set=new Set(); + Object.values(conditions).forEach(({required,forbidden})=>{ + [...required,...forbidden].forEach(id=> { + if(!existingIds.has(id)) + missing.add(id) + }) + }) + + expect([...missing]).toEqual([]); + }); + + it("defines conditions for all levels", () => { + const toAdd : Record= {} + levels.filter(l=>!l.name.startsWith('icon:')).forEach((l,li)=> { + if(l.name in conditions) return + toAdd[l.name]= getLevelUnlockCondition(li, l.name) + }) + if(Object.keys(toAdd).length){ + console.debug('Missing hardcoded conditons\n\n'+ JSON.stringify(toAdd).slice(1,-1)+'\n\n') + } + expect(Object.keys(toAdd)).toEqual([]); + }); + +}) \ No newline at end of file diff --git a/src/game.ts b/src/game.ts index 8b01de0..3ea86d9 100644 --- a/src/game.ts +++ b/src/game.ts @@ -27,7 +27,6 @@ import { levelsListHTMl, max_levels, pickedUpgradesHTMl, - reasonLevelIsLocked, sample, sumOfValues, } from "./game_utils"; @@ -99,6 +98,7 @@ import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel"; import { monitorLevelsUnlocks } from "./monitorLevelsUnlocks"; import { levelEditorMenuEntry } from "./levelEditor"; import { categories } from "./upgrades"; +import {reasonLevelIsLocked} from "./get_level_unlock_condition"; export async function play() { if (await applyFullScreenChoice()) return; @@ -967,7 +967,7 @@ async function openUnlockedLevelsList() { const levelActions = allLevels.map((l, li) => { const lockedBecause = unlockedBefore.has(l.name) ? null - : reasonLevelIsLocked(li, getHistory(), true); + : reasonLevelIsLocked(li, l.name, getHistory(), true); const percentUnlocked = lockedBecause?.reached ? `` : ""; diff --git a/src/gameOver.ts b/src/gameOver.ts index 4f258c3..c0fb636 100644 --- a/src/gameOver.ts +++ b/src/gameOver.ts @@ -6,7 +6,7 @@ import { currentLevelInfo, describeLevel, pickedUpgradesHTMl, - reasonLevelIsLocked, + } from "./game_utils"; import { askForPersistentStorage, @@ -18,6 +18,7 @@ import { stopRecording } from "./recording"; import { asyncAlert } from "./asyncAlert"; import { editRawLevelList } from "./levelEditor"; import { openCreativeModePerksPicker } from "./creative"; +import {isLevelLocked, reasonLevelIsLocked} from "./get_level_unlock_condition"; export function addToTotalPlayTime(ms: number) { setSettingValue( @@ -139,7 +140,7 @@ export function getHistograms(gameState: GameState) { .map((l, li) => ({ li, l, - r: reasonLevelIsLocked(li, runsHistory, false)?.text, + r: reasonLevelIsLocked(li,l.name, runsHistory, false)?.text, })) .filter((l) => l.r); @@ -159,7 +160,7 @@ export function getHistograms(gameState: GameState) { }); const unlocked = locked.filter( - ({ li }) => !reasonLevelIsLocked(li, runsHistory, true), + ({ li ,l}) => !isLevelLocked(li, l.name, runsHistory), ); if (unlocked.length) { unlockedLevels = ` diff --git a/src/game_utils.ts b/src/game_utils.ts index a48d442..2cb0390 100644 --- a/src/game_utils.ts +++ b/src/game_utils.ts @@ -1,19 +1,9 @@ -import { - Ball, - Coin, - GameState, - Level, - PerkId, - PerksMap, - RunHistoryItem, - UpgradeLike, -} from "./types"; -import { icons, upgrades } from "./loadGameData"; -import { t } from "./i18n/i18n"; -import { clamp } from "./pure_functions"; -import { hashCode } from "./getLevelBackground"; -import { getSettingValue, getTotalScore } from "./settings"; -import { isOptionOn } from "./options"; +import {Ball, Coin, GameState, Level, PerkId, PerksMap,} from "./types"; +import {icons, upgrades} from "./loadGameData"; +import {t} from "./i18n/i18n"; +import {clamp} from "./pure_functions"; +import {getSettingValue, getTotalScore} from "./settings"; +import {isOptionOn} from "./options"; export function describeLevel(level: Level) { let bricks = 0, @@ -300,97 +290,6 @@ export function highScoreText() { return ""; } -let excluded: Set; -function isExcluded(id: PerkId) { - if (!excluded) { - excluded = new Set([ - "extra_levels", - "extra_life", - "one_more_choice", - "shunt", - "slow_down", - ]); - // Avoid excluding a perk that's needed for the required one - upgrades.forEach((u) => { - if (u.requires) excluded.add(u.requires); - }); - } - return excluded.has(id); -} - -export function getLevelUnlockCondition(levelIndex: number) { - let required: UpgradeLike[] = [], - forbidden: UpgradeLike[] = [], - minScore = Math.max(-1000 + 100 * levelIndex, 0); - - if (levelIndex > 20) { - const possibletargets = [...upgrades] - .slice(0, Math.floor(levelIndex / 2)) - .filter((u) => !isExcluded(u.id)) - .sort( - (a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id), - ); - - const length = Math.min(3, Math.ceil(levelIndex / 30)); - required = possibletargets.slice(0, length); - forbidden = possibletargets.slice(length, length + length); - } - return { - required, - forbidden, - minScore, - }; -} - -export function getBestScoreMatching( - history: RunHistoryItem[], - required: UpgradeLike[] = [], - forbidden: UpgradeLike[] = [], -) { - return Math.max( - 0, - ...history - .filter( - (r) => - !required.find((u) => !r?.perks?.[u.id]) && - !forbidden.find((u) => r?.perks?.[u.id]), - ) - .map((r) => r.score), - ); -} - -export function reasonLevelIsLocked( - levelIndex: number, - history: RunHistoryItem[], - mentionBestScore: boolean, -): null | { reached: number; minScore: number; text: string } { - const { required, forbidden, minScore } = getLevelUnlockCondition(levelIndex); - - const reached = getBestScoreMatching(history, required, forbidden); - let reachedText = - reached && mentionBestScore ? t("unlocks.reached", { reached }) : ""; - if (reached >= minScore) { - return null; - } else if (!required.length && !forbidden.length) { - return { - reached, - minScore, - text: t("unlocks.minScore", { minScore }) + reachedText, - }; - } else { - return { - reached, - minScore, - text: - t("unlocks.minScoreWithPerks", { - minScore, - required: required.map((u) => u.name).join(", "), - forbidden: forbidden.map((u) => u.name).join(", "), - }) + reachedText, - }; - } -} - export function getCoinRenderColor(gameState: GameState, coin: Coin) { if ( gameState.perks.metamorphosis || diff --git a/src/get_level_unlock_condition.ts b/src/get_level_unlock_condition.ts new file mode 100644 index 0000000..22cbc28 --- /dev/null +++ b/src/get_level_unlock_condition.ts @@ -0,0 +1,113 @@ +import {PerkId, RunHistoryItem, UnlockCondition} from "./types"; +import {upgrades} from "./loadGameData"; +import {hashCode} from "./getLevelBackground"; +import {t} from "./i18n/i18n"; + +import _hardCodedCondition from './data/unlockConditions.json' + +const hardCodedCondition = _hardCodedCondition as Record + +let excluded: Set; + +function isExcluded(id: PerkId) { + if (!excluded) { + excluded = new Set([ + "extra_levels", + "extra_life", + "one_more_choice", + "shunt", + "slow_down", + ]); + // Avoid excluding a perk that's needed for the required one + upgrades.forEach((u) => { + if (u.requires) excluded.add(u.requires); + }); + } + return excluded.has(id); +} + +export function getLevelUnlockCondition(levelIndex: number, levelName:string):UnlockCondition { + + if(hardCodedCondition[levelName]) return hardCodedCondition[levelName] + const result :UnlockCondition = { + required:[], + forbidden:[], + minScore : Math.max(-1000 + 100 * levelIndex, 0) + } + + if (levelIndex > 20) { + const possibletargets = [...upgrades] + .slice(0, Math.floor(levelIndex / 2)) + .filter((u) => !isExcluded(u.id)) + .sort( + (a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id), + ).map(u => u.id); + + const length = Math.min(3, Math.ceil(levelIndex / 30)); + result.required = possibletargets.slice(0, length); + result.forbidden = possibletargets.slice(length, length + length); + } + return result +} + +export function getBestScoreMatching( + history: RunHistoryItem[], + required: PerkId[] = [], + forbidden: PerkId[] = [], +) { + return Math.max( + 0, + ...history + .filter( + (r) => + !required.find((id) => !r?.perks?.[id]) && + !forbidden.find((id) => r?.perks?.[id]), + ) + .map((r) => r.score), + ); +} + +export function isLevelLocked( + levelIndex: number, + levelName:string, + history: RunHistoryItem[]){ + const {required, forbidden, minScore} = getLevelUnlockCondition(levelIndex, levelName); + return getBestScoreMatching(history, required, forbidden) < minScore + +} + +export function reasonLevelIsLocked( + levelIndex: number,levelName:string, + history: RunHistoryItem[], + mentionBestScore: boolean, +): null | { reached: number; minScore: number; text: string } { + + const {required, forbidden, minScore} = getLevelUnlockCondition(levelIndex, levelName); + const reached = getBestScoreMatching(history, required, forbidden); + let reachedText = + reached && mentionBestScore ? t("unlocks.reached", {reached}) : ""; + if (reached >= minScore) { + return null; + } else if (!required.length && !forbidden.length) { + return { + reached, + minScore, + text: t("unlocks.minScore", {minScore}) + reachedText, + }; + } else { + return { + reached, + minScore, + text: + t("unlocks.minScoreWithPerks", { + minScore, + required: required.map((u) => upgradeName(u)).join(", "), + forbidden: forbidden.map((u) => upgradeName(u)).join(", "), + }) + reachedText, + }; + } +} + +export function upgradeName(id:PerkId){ + return upgrades.find(u=>u.id==id)!.name +} \ No newline at end of file diff --git a/src/migrations.ts b/src/migrations.ts index 7b374e1..5b3fa02 100644 --- a/src/migrations.ts +++ b/src/migrations.ts @@ -2,9 +2,9 @@ import { RunHistoryItem } from "./types"; import _appVersion from "./data/version.json"; import { generateSaveFileContent } from "./generateSaveFileContent"; -import { reasonLevelIsLocked } from "./game_utils"; import { allLevels } from "./loadGameData"; import { toast } from "./toast"; +import {isLevelLocked, reasonLevelIsLocked} from "./get_level_unlock_condition"; // The page will be reloaded if any migrations were run let migrationsRun = 0; @@ -128,7 +128,7 @@ migrate("set_breakout_71_unlocked_levels" + _appVersion, () => { ) as string[]; allLevels - .filter((l, li) => !reasonLevelIsLocked(li, runsHistory, false)) + .filter((l, li) => !isLevelLocked(li,l.name, runsHistory)) .forEach((l) => { if (!breakout_71_unlocked_levels.includes(l.name)) { breakout_71_unlocked_levels.push(l.name); diff --git a/src/monitorLevelsUnlocks.ts b/src/monitorLevelsUnlocks.ts index f0b3d5d..c861a1b 100644 --- a/src/monitorLevelsUnlocks.ts +++ b/src/monitorLevelsUnlocks.ts @@ -1,22 +1,26 @@ -import { GameState, UpgradeLike } from "./types"; +import {GameState, PerkId} from "./types"; import { getSettingValue, setSettingValue } from "./settings"; import { allLevels, icons } from "./loadGameData"; -import { getLevelUnlockCondition } from "./game_utils"; import { t } from "./i18n/i18n"; import { toast } from "./toast"; import { schedulGameSound } from "./gameStateMutators"; +import {getLevelUnlockCondition} from "./get_level_unlock_condition"; let list: { minScore: number; - forbidden: UpgradeLike[]; - required: UpgradeLike[]; + forbidden: PerkId[]; + required: PerkId[]; }[]; -let unlocked = new Set( - getSettingValue("breakout_71_unlocked_levels", []) as string[], -); +let unlocked : Set |null = null export function monitorLevelsUnlocks(gameState: GameState) { + if(!unlocked){ + unlocked = new Set( + getSettingValue("breakout_71_unlocked_levels", []) as string[], + ); + } + if (gameState.creative) return; if (!list) { @@ -24,13 +28,13 @@ export function monitorLevelsUnlocks(gameState: GameState) { name: l.name, li, l, - ...getLevelUnlockCondition(li), + ...getLevelUnlockCondition(li, l.name), })); } list.forEach(({ name, minScore, forbidden, required, l }) => { // Already unlocked - if (unlocked.has(name)) return; + if (unlocked!.has(name)) return; // Score not reached yet if (gameState.score < minScore) return; if (!minScore) return; @@ -41,7 +45,7 @@ export function monitorLevelsUnlocks(gameState: GameState) { // We have a forbidden perk if (forbidden.find((id) => gameState.perks[id])) return; // Level just got unlocked - unlocked.add(name); + unlocked!.add(name); setSettingValue( "breakout_71_unlocked_levels", getSettingValue("breakout_71_unlocked_levels", []).concat([name]), diff --git a/src/newGameState.ts b/src/newGameState.ts index e8aad65..5512a34 100644 --- a/src/newGameState.ts +++ b/src/newGameState.ts @@ -5,7 +5,6 @@ import { getHighScore, getPossibleUpgrades, highScoreText, - reasonLevelIsLocked, makeEmptyPerksMap, sumOfValues, } from "./game_utils"; @@ -14,6 +13,7 @@ import { isOptionOn } from "./options"; import { getHistory } from "./gameOver"; import { getSettingValue, getTotalScore } from "./settings"; import { isBlackListedForStart, isStartingPerk } from "./startingPerks"; +import {isLevelLocked, reasonLevelIsLocked} from "./get_level_unlock_condition"; export function getRunLevels( params: RunParams, @@ -26,7 +26,7 @@ export function getRunLevels( const history = getHistory(); const unlocked = allLevels.filter( (l, li) => - unlockedBefore.has(l.name) || !reasonLevelIsLocked(li, history, false), + unlockedBefore.has(l.name) || !isLevelLocked(li, l.name, history), ); const firstLevel = params?.level ? [params.level] diff --git a/src/openScorePanel.ts b/src/openScorePanel.ts index b5f1368..a7dc8db 100644 --- a/src/openScorePanel.ts +++ b/src/openScorePanel.ts @@ -2,17 +2,16 @@ import { GameState } from "./types"; import { asyncAlert } from "./asyncAlert"; import { t } from "./i18n/i18n"; import { - getLevelUnlockCondition, levelsListHTMl, max_levels, pickedUpgradesHTMl, - reasonLevelIsLocked, } from "./game_utils"; import { getCreativeModeWarning, getHistory } from "./gameOver"; import { pause } from "./game"; import { allLevels, icons } from "./loadGameData"; import { firstWhere } from "./pure_functions"; import { getSettingValue, getTotalScore } from "./settings"; +import {getLevelUnlockCondition, reasonLevelIsLocked, upgradeName} from "./get_level_unlock_condition"; export async function openScorePanel(gameState: GameState) { pause(true); @@ -42,13 +41,13 @@ export function getNearestUnlockHTML(gameState: GameState) { const unlocked = new Set(getSettingValue("breakout_71_unlocked_levels", [])); const firstUnlockable = firstWhere(allLevels, (l, li) => { if (unlocked.has(l.name)) return; - const reason = reasonLevelIsLocked(li, getHistory(), false); + const reason = reasonLevelIsLocked(li, l.name, getHistory(), false); if (!reason) return; - const { minScore, forbidden, required } = getLevelUnlockCondition(li); - const missing = required.filter((u) => !gameState?.perks?.[u.id]); + const { minScore, forbidden, required } = getLevelUnlockCondition(li, l.name); + const missing = required.filter((id) => !gameState?.perks?.[id]); // we can't have a forbidden perk - if (forbidden.find((u) => gameState?.perks?.[u.id])) { + if (forbidden.find((id) => gameState?.perks?.[id])) { return; } @@ -70,7 +69,7 @@ export function getNearestUnlockHTML(gameState: GameState) { if (!firstUnlockable) return ""; let missingPoints = Math.max(0, firstUnlockable.minScore - gameState.score); - let missingUpgrades = firstUnlockable.missing.map((u) => u.name).join(", "); + let missingUpgrades = firstUnlockable.missing.map((id) => upgradeName(id)).join(", "); const title = (missingUpgrades && diff --git a/src/types.d.ts b/src/types.d.ts index c7d1d27..4e666fa 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,5 +1,5 @@ -import { rawUpgrades } from "./upgrades"; -import { options } from "./options"; +import {rawUpgrades} from "./upgrades"; +import {options} from "./options"; export type colorString = string; @@ -307,3 +307,8 @@ export type UpgradeLike = { requires: string; threshold: number; }; +export type UnlockCondition = { + required: PerkId[]; + forbidden: PerkId[]; + minScore: number; +} \ No newline at end of file