diff --git a/.gitignore b/.gitignore index 6c6dd93..9e81429 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ *.iml .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +/.idea .DS_Store /build /captures diff --git a/dist/index.html b/dist/index.html index 353bb7a..a007bab 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,103 +1,2858 @@ -Breakout 71 \ No newline at end of file +

`, + actions, + allowClose: true + }); + if (tryOn) { + if (!currentLevel || await asyncAlert({ + title: "Restart run to try this item?", + text: "You're about to start a new run with the selected unlocked item, is that really what you wanted ? ", + actions: [ + { + value: true, + text: "Restart game to test item" + }, + { + value: false, + text: "Cancel" + } + ] + })) { + nextRunOverrides = tryOn; + restart(); + } + } +} +function distance2(a, b) { + return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2); +} +function distanceBetween(a, b) { + return Math.sqrt(distance2(a, b)); +} +function rainbowColor() { + return `hsl(${Math.round(levelTime / 4) * 2 % 360},100%,70%)`; +} +function repulse(a, b, power, impactsBToo) { + const distance = distanceBetween(a, b); + // Ensure we don't get soft locked + const max = gameZoneWidth / 2; + if (distance > max) return; + // Unit vector + const dx = (a.x - b.x) / distance; + const dy = (a.y - b.y) / distance; + const fact = -power * (max - distance) / (max * 1.2) / 3 * Math.min(500, levelTime) / 500; + if (impactsBToo && typeof b.vx !== 'undefined' && typeof b.vy !== 'undefined') { + b.vx += dx * fact; + b.vy += dy * fact; + } + a.vx -= dx * fact; + a.vy -= dy * fact; + const speed = 10; + const rand = 2; + flashes.push({ + type: "particle", + duration: 100, + time: levelTime, + size: coinSize / 2, + color: rainbowColor(), + ethereal: true, + x: a.x, + y: a.y, + vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand, + vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand + }); + if (impactsBToo && typeof b.vx !== 'undefined' && typeof b.vy !== 'undefined') flashes.push({ + type: "particle", + duration: 100, + time: levelTime, + size: coinSize / 2, + color: rainbowColor(), + ethereal: true, + x: b.x, + y: b.y, + vx: dx * speed + b.vx + (Math.random() - 0.5) * rand, + vy: dy * speed + b.vy + (Math.random() - 0.5) * rand + }); +} +function attract(a, b, power) { + const distance = distanceBetween(a, b); + // Ensure we don't get soft locked + const min = gameZoneWidth * 0.5; + if (distance < min) return; + // Unit vector + const dx = (a.x - b.x) / distance; + const dy = (a.y - b.y) / distance; + const fact = power * (distance - min) / min * Math.min(500, levelTime) / 500; + b.vx += dx * fact; + b.vy += dy * fact; + a.vx -= dx * fact; + a.vy -= dy * fact; + const speed = 10; + const rand = 2; + flashes.push({ + type: "particle", + duration: 100, + time: levelTime, + size: coinSize / 2, + color: rainbowColor(), + ethereal: true, + x: a.x, + y: a.y, + vx: dx * speed + a.vx + (Math.random() - 0.5) * rand, + vy: dy * speed + a.vy + (Math.random() - 0.5) * rand + }); + flashes.push({ + type: "particle", + duration: 100, + time: levelTime, + size: coinSize / 2, + color: rainbowColor(), + ethereal: true, + x: b.x, + y: b.y, + vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand, + vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand + }); +} +let mediaRecorder, captureStream, captureTrack, recordCanvas, recordCanvasCtx; +function recordOneFrame() { + if (!isSettingOn("record")) return; + if (!running) return; + if (!captureStream) return; + drawMainCanvasOnSmallCanvas(); + if (captureTrack?.requestFrame) captureTrack?.requestFrame(); + else if (captureStream?.requestFrame) captureStream.requestFrame(); +} +function drawMainCanvasOnSmallCanvas() { + if (!recordCanvasCtx) return; + recordCanvasCtx.drawImage(gameCanvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height); + // Here we don't use drawText as we don't want to cache a picture for each distinct value of score + recordCanvasCtx.fillStyle = "#FFF"; + recordCanvasCtx.textBaseline = "top"; + recordCanvasCtx.font = "12px monospace"; + recordCanvasCtx.textAlign = "right"; + recordCanvasCtx.fillText(score.toString(), recordCanvas.width - 12, 12); + recordCanvasCtx.textAlign = "left"; + recordCanvasCtx.fillText("Level " + (currentLevel + 1) + "/" + max_levels(), 12, 12); +} +function startRecordingGame() { + if (!isSettingOn("record")) return; + if (!recordCanvas) { + // Smaller canvas with fewer details + recordCanvas = document.createElement("canvas"); + recordCanvasCtx = recordCanvas.getContext("2d", { + antialias: false, + alpha: false + }); + captureStream = recordCanvas.captureStream(0); + captureTrack = captureStream.getVideoTracks()[0]; + if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]); + } + recordCanvas.width = gameZoneWidthRoundedUp; + recordCanvas.height = gameZoneHeight; + // drawMainCanvasOnSmallCanvas() + const recordedChunks = []; + const instance = new MediaRecorder(captureStream, { + videoBitsPerSecond: 3500000 + }); + mediaRecorder = instance; + instance.start(); + mediaRecorder.pause(); + instance.ondataavailable = function(event) { + recordedChunks.push(event.data); + }; + instance.onstop = async function() { + let targetDiv; + let blob = new Blob(recordedChunks, { + type: "video/webm" + }); + if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short + while(!(targetDiv = document.getElementById("level-recording-container")))await new Promise((r)=>setTimeout(r, 200)); + const video = document.createElement("video"); + video.autoplay = true; + video.controls = false; + video.disablePictureInPicture = true; + video.disableRemotePlayback = true; + video.width = recordCanvas.width; + video.height = recordCanvas.height; + video.loop = true; + video.muted = true; + video.playsInline = true; + video.src = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.download = captureFileName("webm"); + a.target = "_blank"; + a.href = video.src; + a.textContent = `Download video (${(blob.size / 1000000).toFixed(2)}MB)`; + targetDiv.appendChild(video); + targetDiv.appendChild(a); + }; +} +function pauseRecording() { + if (!isSettingOn("record")) return; + if (mediaRecorder?.state === "recording") mediaRecorder?.pause(); +} +function resumeRecording() { + if (!isSettingOn("record")) return; + if (mediaRecorder?.state === "paused") mediaRecorder.resume(); +} +function stopRecording() { + if (!isSettingOn("record")) return; + if (!mediaRecorder) return; + mediaRecorder?.stop(); + mediaRecorder = null; +} +function captureFileName(ext = "webm") { + return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + "." + ext; +} +function findLast(arr, predicate) { + let i = arr.length; + while(--i)if (predicate(arr[i], i, arr)) return arr[i]; +} +function toggleFullScreen() { + try { + if (document.fullscreenElement !== null) { + if (document.exitFullscreen) document.exitFullscreen().then(); + else if (document.webkitCancelFullScreen) document.webkitCancelFullScreen(); + } else { + const docel = document.documentElement; + if (docel.requestFullscreen) docel.requestFullscreen().then(); + else if (docel.webkitRequestFullscreen) docel.webkitRequestFullscreen(); + } + } catch (e) { + console.warn(e); + } +} +const pressed = { + ArrowLeft: 0, + ArrowRight: 0, + Shift: 0 +}; +function setKeyPressed(key, on) { + pressed[key] = on; + keyboardPuckSpeed = (pressed.ArrowRight - pressed.ArrowLeft) * (1 + pressed.Shift * 2) * gameZoneWidth / 50; +} +document.addEventListener("keydown", (e)=>{ + if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) toggleFullScreen(); + else if (e.key in pressed) setKeyPressed(e.key, 1); + if (e.key === " " && !alertsOpen) { + if (running) pause(true); + else play(); + } else return; + e.preventDefault(); +}); +document.addEventListener("keyup", (e)=>{ + const focused = document.querySelector("button:focus"); + if (e.key in pressed) setKeyPressed(e.key, 0); + else if (e.key === "ArrowDown" && focused?.nextElementSibling?.tagName === "BUTTON") focused?.nextElementSibling?.focus(); + else if (e.key === "ArrowUp" && focused?.previousElementSibling?.tagName === "BUTTON") focused?.previousElementSibling?.focus(); + else if (e.key === "Escape" && closeModal) closeModal(); + else if (e.key === "Escape" && running) pause(true); + else if (e.key.toLowerCase() === "m" && !alertsOpen) openSettingsPanel().then(); + else if (e.key.toLowerCase() === "s" && !alertsOpen) openScorePanel().then(); + else return; + e.preventDefault(); +}); +function sample(arr) { + return arr[Math.floor(arr.length * Math.random())]; +} +function getMajorityValue(arr) { + const count = {}; + arr.forEach((v)=>count[v] = (count[v] || 0) + 1); + // Object.values inline polyfill + const max = Math.max(...Object.keys(count).map((k)=>count[k])); + return sample(Object.keys(count).filter((k)=>count[k] == max)); +} +fitSize(); +restart(); +tick(); + +},{"./loadGameData":"l1B4x","./options":"d5NoS","@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, "appVersion", ()=>appVersion); +parcelHelpers.export(exports, "icons", ()=>icons); +parcelHelpers.export(exports, "allLevels", ()=>allLevels); +parcelHelpers.export(exports, "upgrades", ()=>upgrades); +var _paletteJson = require("./palette.json"); +var _paletteJsonDefault = parcelHelpers.interopDefault(_paletteJson); +var _levelsJson = require("./levels.json"); +var _levelsJsonDefault = parcelHelpers.interopDefault(_levelsJson); +var _versionJson = require("./version.json"); +var _versionJsonDefault = parcelHelpers.interopDefault(_versionJson); +var _rawUpgrades = require("./rawUpgrades"); +const palette = (0, _paletteJsonDefault.default); +const rawLevelsList = (0, _levelsJsonDefault.default); +const appVersion = (0, _versionJsonDefault.default); +const randomPatterns = [ + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + `` +]; +let attributed = 0; +let levelIconHTMLCanvas = document.createElement("canvas"); +const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", { + antialias: false, + alpha: true +}); +function levelIconHTML(bricks, levelSize, levelName, color) { + const size = 40; + const c = levelIconHTMLCanvas; + const ctx = levelIconHTMLCanvasCtx; + c.width = size; + c.height = size; + if (color) { + ctx.fillStyle = color; + ctx.fillRect(0, 0, size, size); + } else ctx.clearRect(0, 0, size, size); + const pxSize = size / levelSize; + for(let x = 0; x < levelSize; x++)for(let y = 0; y < levelSize; y++){ + const c = bricks[y * levelSize + x]; + if (c) { + ctx.fillStyle = c; + ctx.fillRect(Math.floor(pxSize * x), Math.floor(pxSize * y), Math.ceil(pxSize), Math.ceil(pxSize)); + } + } + // I don't think many blind people will benefit for this but it's nice to have something to put in "alt" + return `${levelName}`; +} +const icons = {}; +const allLevels = rawLevelsList.map((level)=>{ + const bricks = level.bricks.split("").map((c)=>palette[c]); + const icon = levelIconHTML(bricks, level.size, level.name, level.color); + icons[level.name] = icon; + let svg = level.svg; + if (!level.color && !svg) { + svg = randomPatterns[attributed % randomPatterns.length]; + attributed++; + } + return { + ...level, + bricks, + icon, + svg + }; +}).filter((l)=>!l.name.startsWith("icon:")).map((l, li)=>({ + ...l, + threshold: li < 8 ? 0 : Math.round(Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li), + sortKey: (Math.random() + 3) / 3.5 * l.bricks.filter((i)=>i).length + })); +const upgrades = (0, _rawUpgrades.rawUpgrades).map((u)=>({ + ...u, + icon: icons["icon:" + u.id] + })); + +},{"./palette.json":"jhnsJ","./levels.json":"kqnNl","./version.json":"h1X9A","./rawUpgrades":"cvg5m","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"jhnsJ":[function(require,module,exports,__globalThis) { +module.exports = JSON.parse("{\"_\":\"\",\"B\":\"black\",\"W\":\"white\",\"g\":\"#231f20\",\"y\":\"#ffd300\",\"b\":\"#6262EA\",\"t\":\"#5DA3EA\",\"s\":\"#E67070\",\"r\":\"#e32119\",\"R\":\"#ab0c0c\",\"c\":\"#59EEA3\",\"G\":\"#A1F051\",\"v\":\"#A664E8\",\"p\":\"#E869E8\",\"a\":\"#5BECEC\",\"C\":\"#53EE53\",\"S\":\"#F44848\",\"P\":\"#E66BA8\",\"O\":\"#F29E4A\",\"k\":\"#618227\",\"e\":\"#e1c8b4\",\"l\":\"#9b9fa4\"}"); + +},{}],"kqnNl":[function(require,module,exports,__globalThis) { +module.exports = JSON.parse('[{"name":"71 mini","size":5,"bricks":"bbb____bt__btt__b_t___ttt","svg":"","color":""},{"name":"Butterfly","bricks":"_________bb_t_t_bbbbb_t_bbbbbbbtbbbb_bbbtbbb____btb____bbbtbbb__bb_t_bb___________________","size":9,"svg":"","color":""},{"name":"Castle","size":7,"bricks":"s_s_s_ssssssssssBBBssssBBBssttbbbttttbbbtttbtbtbt","svg":""},{"name":"Eyes","size":9,"bricks":"ttttttt__tWWWWWWW_tWrrWttW_tWWWWWWW_ttttttt_____t______ttttt____ttttt_____t_t","svg":"","color":""},{"name":"Creeper","size":10,"bricks":"___________ccGGccGG__cGccGcGc__GBBccBBc__cBBGcBBc__GccBBGGc__ccBBBBcG__GGBBBBcG__cGBccBGc___________","svg":""},{"name":"Stairs","size":8,"bricks":"tt______tt______bbtt____bbtt____vvbbtt__vvbbtt__ppvvbbttppvvbbtt","svg":""},{"name":"Dots","size":9,"bricks":"b_t_a_c_C__________b_t_a_c__________v_b_t_a_c__________v_b_t_a__________p_v_b_t_a","svg":""},{"name":"Lines","size":9,"bricks":"aaaaaaaa___________tttttttt_________aaaaaaaa___________tttttttt_________aaaaaaaa","svg":"","color":""},{"name":"Heart","size":15,"bricks":"__________________RRR___RRR_____RSSSR_RSSSR___RSWWSSRSSSSSR__RSWSSSSSSSSSR__RSSSSSSSSSSSR__RSWSSSSSSSSSR___RSSSSSSSSSR_____RSSSSSSSR_______RSSSSSR_________RSSSR___________RSR_____________R____________________________________","svg":"","color":""},{"name":"Swiss","size":7,"bricks":"________RRRRR__RRWRR__RWWWR__RRWRR__RRRRR","svg":"","color":""},{"name":"Germany","size":6,"bricks":"_______gggg__rrrr__yyyy","svg":"","color":""},{"name":"France","size":8,"bricks":"_________ttWWrr__ttWWrr__ttWWrr__ttWWrr__ttWWrr________","svg":"","color":""},{"name":"Smiley","size":8,"bricks":"_________yy__yy__yy__yy__________________yyyyyy___yyyy__________","svg":"","color":""},{"name":"Labyrinthe","size":11,"bricks":"_______tttS_Stttt_S________t___S__Stt_ttttt____t_____S__ttt_S_S____t___t_tttt_t_S_t____tSt_t_t_Sttt___t_t_____Sttt_tttttS","svg":""},{"name":"Temple","size":11,"bricks":"_______________WWW______WWWWWWW___WWWWWWWWW___t_t_t_t____b_b_b_b____v_v_v_v____p_p_p_p____P_P_P_P____WWWWWWW___WWWWWWWWW_","svg":"","color":""},{"name":"Pacman","size":12,"bricks":"____yyyy______yyyyyyyy___yyyyByyyyy__yyyyyyyyy__yyyyyyyy____yyyyyy______yyyyyy___S_Syyyyyyyy_____yyyyyyyyy___yyyyyyyyyy___yyyyyyyy______yyyy","svg":"","color":""},{"name":"Ship","size":11,"bricks":"____sWW________sWWW_______sWWW_______s___OOOOOOOOOOOOOO_OBOBOBOBOO__OOOOOOOO_bbbbbbbbgbbbbgbbbbggbbbggbbbbbbbb","svg":""},{"name":"We come in peace","size":13,"bricks":"________________a_____a_______a___a_______aaaaaaa_____aaBaaaBaa___aaaaaaaaaaa__aaaaaaaaaaa__a_aaaaaaa_a__a_a_____a_a_____aa_aa_____________________________","svg":"","color":""},{"name":"Space mushroom","size":10,"bricks":"______________WW_______WWWW_____WWWWWW___WWBWWBWW__WWWWWWWW____W__W_____W_WW_W___W_W__W_W","svg":"","color":""},{"name":"Wololo","size":9,"bricks":"____WW_OOW___WW__OWW__W___OWWWbbbW_WWW_WbW_WOW__WWb__OW__bbb__O___W_W__O___W_W__O","svg":"","color":""},{"name":"Small heart","size":15,"bricks":"________________________________RRRR___RRRR___RrWWrR_RWWrrR__RWWrrrRWWrrrR__RrrrrrrrrrrrR__RrrrrrrrrrrrR___RrrrrrrrrrR_____RrrrrrrrR_______RrrrrrR_________RrrrR___________RrR_____________R______________________","svg":"","color":""},{"name":"Eye","size":9,"bricks":"____________ggg_____gWWWg___gWbbbWg_gWWbBbWWg_gWbbbWg___gWWWg_____ggg____________","svg":"","color":"#5da3ea"},{"name":"Enderman","size":10,"bricks":"___________gggggggg__gggggggg__gggggggg__gggggggg__vvvggvvv__gggggggg__gggggggg__gggggggg_____________________","svg":"","color":"#ffffff"},{"name":"Mushroom","size":16,"bricks":"_____________________rrrrWW________WWrrrrWWWW_____WWrrrrrrWWWW____WrrWWWWrrWWW___rrrWWWWWWrrrrr__rrrWWWWWWrrWWr__WrrWWWWWWrWWWW__WWrrWWWWrrWWWW__WWrrrrrrrrrWWr__WrrWWWWWWWWrrr_____WWBWWBWW_______WWWBWWBWWW______WWWWWWWWWW_______WWWWWWWW____________________","svg":"","color":""},{"name":"Tulip","size":11,"bricks":"______________R_R_R______RRRRR______RRRRR______RRRRR_______RRR_________k________k_k_k______k_k_k_______kkk_________k________________","svg":"","color":""},{"name":"Chain","size":7,"bricks":"yyy____yBy____yyyyy____yBy____yyyyy____yBy____yyy","svg":"","color":""},{"name":"Marion","size":9,"bricks":"rr_____rr_rr___rr__rrr_rrr__rrrrrrr__rr_r_rr__rr___rr__rr___rr__rr___rr_rrr___rrr","svg":"","color":""},{"name":"Renan","size":9,"bricks":"yyyyyyy___yyyyyyy__yy___yy__yy___yy__yyyyyy___yy_yy____yy__yy___yy___yy_yyy___yyy","svg":"","color":""},{"name":"Violet Pairs","size":8,"bricks":"b_b_b_b_b_b_b_b__________t_t_t_t_t_t_t_t________b_b_b_b_b_b_b_b","svg":"","color":""},{"name":"Red Cups","size":11,"bricks":"___________rBr_rBr_rBrrrr_rrr_rrr___________r_rBr_rBr_rr_rrr_rrr_r___________rBr_rBr_rBrrrr_rrr_rrr__________","svg":"","color":""},{"name":"Cactus","size":10,"bricks":"____G______rG_Gk______G_Gk______kkkk_r_____kkk_G______GkGk_____rGkk_______Gk________kk________kk_____","svg":"","color":""},{"name":"Sunny Face","size":11,"bricks":"____yyy______yyyyyyy___yyyyyyyyy__yyyyyyyyy_yyyWWyWWyyyyyyyyyyyyyyyyyyyyyyyyy_yyWWWWWyy__yyyWWWyyy___yyyyyyy______yyy","svg":"","color":"#5da3ea"},{"name":"Mountain","size":9,"bricks":"_______________W_______WWW______GGWW__W_GGGGG_kkkGGGGG_kkkkGGGGkkkkkGGGGkkkkkkGGG_________","svg":"","color":""},{"name":"Dollar","size":17,"bricks":"________________________G_G______________G_G____________GGGGGGG_________GGGGGGGGG_______GG__G_G__GG______GG__G_G__GG______GG__G_G___________GGGGGGGG__________GGGGGGGG___________G_G__GG______GG__G_G__GG______GG__G_G__GG_______GGGGGGGGG_________GGGGGGG____________G_G______________G_G________________________","svg":"","color":""},{"name":"Waves","size":8,"bricks":"___bbb____bbb____bbttbbbbbttbbbbttttaatttttaattttaaaaaaa","svg":"","color":""},{"name":"Box","size":8,"bricks":"yyyyyyyyy______yy_bbbb_yy_b__b_yy_b__b_yy_bbbb_yy______yyyyyyyyy","svg":"","color":"","squared":false},{"name":"Rose","size":9,"bricks":"__SS______SSSS_____SSSS_____SSSS______SS_k______k_kk_____kk_k______kk________k","svg":"","color":""},{"name":"Time","size":9,"bricks":"__________WWWWWWW___WWWWW_____yyy_______y________y_______WyW_____WyyyW___yyyyyyy__________","svg":"","color":"","squared":false},{"name":"Watermelon","size":8,"bricks":"_____Sk_____SSBk___SBSSk__SSSSSk_SSBSSk_SBSSSSk_kSSSkk___kkk____","svg":"","color":""},{"name":"Worms","size":13,"bricks":"___sssss_______sssssss______WWsWWsss_____WBsBWsss_____WBsBWsss_____WWsWWsss_____sssssss_______ssssss_____WWWWWWss_______WssWs__s_____ssss__sss___sssssssssss__sssssssss_ss","svg":" ","color":"","squared":false},{"name":"Ocean Sunrise","size":8,"bricks":"SSSSSSSSSSSyySSSSSyyyySSSyyWWyySbttaattbbbttttbbbbbttbbbbbbbbbbb","svg":"","color":""},{"name":"Crosses","size":13,"bricks":"b___b___b___b__v___v___v___vvv_vvv_vvv___v___v___v__p___p___p___ppp_ppp_ppp_ppp___p___p___p__P___P___P___PPP_PPP_PPP___P___P___P__p___p___p___ppp_ppp_ppp_ppp___p___p___p","svg":"","color":""},{"name":"Negative space","size":9,"bricks":"tttttttttt_t_t_t_t_________b_b_b_b_bbbbbbbbbb_b_b_b_b___________t_t_t_t_ttttttttt_________","svg":""},{"name":"UK","size":11,"bricks":"brbbWrWbbrbbbrbWrWbrbbbbbrWrWrbbbWWWWWrWWWWWrrrrrrrrrrrWWWWWrWWWWWbbbrWrWrbbbbbrbWrWbrbbbrbbWrWbbrb__________","svg":"","color":""},{"name":"Greece","size":11,"bricks":"ttWttttttttttWttWWWWWWWWWWWttttttttWttWWWWWWttWttttttttWWWWWWWWWWWtttttttttttWWWWWWWWWWWttttttttttt__________","svg":"","color":""},{"name":"Russia","size":8,"bricks":"________WWWWWWWWWWWWWWWWttttttttttttttttrrrrrrrrrrrrrrrr________________","svg":"","color":""},{"name":"Ukraine","size":8,"bricks":"________ttttttttttttttttttttttttyyyyyyyyyyyyyyyyyyyyyyyy________","svg":"","color":""},{"name":"Poland","size":7,"bricks":"________WWWWW__WWWWW__rrrrr__rrrrr_______________","svg":"","color":""},{"name":"Yellow 71","size":9,"bricks":"_________yyyyy__yyyyyyy_yyy___yy__yy__yyy__yy_yyy___yy_yy____yy_yy____yy__________________","svg":"","color":""},{"name":"71 on white","size":6,"bricks":"WWWWWWrrrWWrWWrWrrWrWWWrWrWWWrWWWWWW______","svg":""},{"name":"Blue 71","size":8,"bricks":"ttttt__bttttt_bb___ttbbb__tt__bb__tt__bb_tt___bb_tt___bb_tt___bb","svg":"","color":""},{"name":"Seventy one","size":21,"bricks":"rr_yy_rrry_yrrry_yrrrr_ry_yr__y_yr_ry_y_r_rr_yy_rr_yy_r_ry_y_r_r_ry_yr__y_yr_ry_y_r_rr_y_yrrry_yrrryyy_r_yyy__________________y______________r_____yyyrrry_yrrryyyrr_y_y__yrr_y_yrr_y_yr__y_yyyyrrr_y_rrry_yrrryyy____________________yrrryyyrrr_________yy_r_ry_yrr_____________rrry_yrrryyyyyyyyyyyy_____________________________________________________________________________________________________________________________","svg":""},{"name":"B71","size":10,"bricks":"__________bbbtttt_b_b__b__tbb_b__b__t_b_bbb__t__b_b__b_t__b_b__bt___b_bbb_t__bbb__________","svg":""},{"name":"Pig","size":9,"bricks":"__________PP___PP__PPP_PPP__WWPPPWW__WBPPPBW__PPsssPP__PsBsBsP__PPsssPP___________","svg":""},{"name":"Big Pig","size":15,"bricks":"________________sss_______sss__ss__sssss__ss____sssssssss_____sWBsssssBWs___ssBBsssssBBss__ssss_____ssss__sss_sssss_sss__sss_sBsBs_sss__sss_sssss_sss___sss_____sss____sssssssssss__GGGsssssssssGGGGGGsGsssssGsGGGGGGssGGGGGssGGG_______________","svg":"","color":""},{"name":"Donkey Kong","size":9,"bricks":"OOr__a___OOr__a___ppppppp_O______a________a____pppppppr_a______b_a___O__ppppppp__","svg":" ","color":""},{"name":"Banana","size":12,"bricks":"_________________e__________eee_________eee_________eee_________eeeyy_____yyeeyyyy___yyyyey_yC___yy_yyy___C_____yyyy_________yyyy_________yyyy","svg":""},{"name":"Fox","size":8,"bricks":"e______eee_OO_eeeeOOOOeeeOBOOBOeOOOOOOOO_WWBBWW___WWWW_____WW___","svg":""},{"name":"Wiki","size":10,"bricks":"_______________________GGGG_____GGkkGG___GkggggkG__GgWWWWgG__GkggggkG___GGkkGG_____GGGG_______________________","svg":""},{"name":"Baby Dog","size":8,"bricks":"_______W__eeeeWWWWeeWeWWWegWegeeeeWWWWee_eWggWe__eWWWWe____WW","svg":""},{"name":"Cute dog","size":9,"bricks":"__________O_____O_OOOWWWOOOOOWWWWWOOOOeWWWWOO_eBeWWBW__eBeWWBW___eWBWW_____WRW____________","svg":""},{"name":"icon:extra_life","size":9,"bricks":"___________rr_rr___rrrrrrr_rrrrrrrrrrrrrrrrrr_rrrrrrr___rrrrr_____rrr_______r_____________","svg":""},{"name":"icon:streak_shots","size":8,"bricks":"_W_W_W__W_W_W_W_tttttt_WttttttW_tttttt_W______W______W_____WWWW","svg":""},{"name":"icon:base_combo","size":8,"bricks":"ttttttttttyyttttttyytyyttttttyyttyyttttttyytyyttttttyytttttttttt________","svg":""},{"name":"icon:slow_down","size":10,"bricks":"_____________kk_______kkkk_____kkkkkkGG__kkkkkkGBG_kkkkkkGGGGkkkkkkGG__GGGGGG____GG__GG_____________","svg":""},{"name":"icon:bigger_puck","size":8,"bricks":"_________tttttt__tttttt______________________W___________WWWWWW_","svg":""},{"name":"icon:viscosity","size":8,"bricks":"________tt______bbtt__ttbbbbttbbbtbbtbbbbbtbbtbbbbbybbybbbbbbbbb","svg":""},{"name":"icon:left_is_lava","size":8,"bricks":"r_______rtttttt_rtttttt_r_______r_______r____W__r_______r_WWW___","svg":""},{"name":"icon:right_is_lava","size":8,"bricks":"_______r_ttttttr_ttttttr_______r_______r_____W_r_______r__WWW__r","svg":""},{"name":"icon:telekinesis","size":8,"bricks":"_____PW_____s______P______s_______P_______s_______P_____WWWWW","svg":""},{"name":"icon:top_is_lava","size":8,"bricks":"rrrrrrrr_tttttt__tttttt____________________W_______________WWW__","svg":""},{"name":"icon:coin_magnet","size":8,"bricks":"__y__y_yy_________y_y_y_y________y_y______________y______WWW____","svg":""},{"name":"icon:skip_last","size":5,"bricks":"_ttt_t_t_ttt_ttt_t_t_ttt_","svg":""},{"name":"icon:multiball","size":8,"bricks":"_________tttttt__tttttt___________W__W____________________WWW___","svg":""},{"name":"icon:smaller_puck","size":8,"bricks":"_________tttttt__tttttt_____________W_____________________WW____","svg":""},{"name":"icon:pierce","size":6,"bricks":"ttttttttttWtttt__ttt__ttt__ttt__tttt","svg":""},{"name":"icon:picky_eater","size":8,"bricks":"rtrtrtrttrtrtrtrrtrtrtrt____________________t_____________WWWW","svg":""},{"name":"icon:metamorphosis","size":8,"bricks":"aaaaaa__aaaa__________W___________ttaatt__tttttt_________WWW","svg":""},{"name":"icon:compound_interest","size":8,"bricks":"_________tttttt__ttt__t______y_____________W__y_________rrWWWrrr","svg":""},{"name":"icon:hot_start","size":7,"bricks":"ttttttttttt_tt_____W_____y_y_____y_____y_y_WWW_y_","svg":""},{"name":"icon:sapper","size":9,"bricks":"_____WW______W__W_tttWttt_yttgggtt__tgggggt__tgggggt__tgggggt__ttgggtt__ttttttt___________","svg":"","color":"#000000"},{"name":"icon:bigger_explosions","size":8,"bricks":"__r_______ry_rr___ryry__ryyyW_rr_rrWyyy___yryrr__yrry_rr_rr","svg":""},{"name":"icon:extra_levels","size":6,"bricks":"__________b__t_bb_ttt_b__t_bbb____________","svg":""},{"name":"icon:pierce_color","size":8,"bricks":"bb___bbbb__b_bbb_____bbb____bbbbb____bbbbb____bbbbb____bbbbb____","svg":""},{"name":"icon:soft_reset","size":8,"bricks":"___rg_____rrgg___rryggg_rryWggggrryWgggg_ryyggg___rrgg_____rg___","svg":""},{"name":"icon:ball_repulse_ball","size":8,"bricks":"WsP__PsWs______sP______P________________P______Ps______sWsP__PsW","svg":""},{"name":"icon:ball_attract_ball","size":8,"bricks":"__P__P____s__s__PsW__WsP________________PsW__WsP__s__s____P__P__","svg":""},{"name":"icon:puck_repulse_ball","size":8,"bricks":"__________________W_______s___W___P__s______P____________WWW__","svg":""},{"name":"A","size":7,"bricks":"___t_____ttt___t___t__t___t_tttttttt_____tt_____t","svg":""},{"name":"B","size":9,"bricks":"_bbbbb_____bb_bb____bb_bb____bb_bb____bbbb_____bb_bb____bb_bb____bb_bb___bbbbb____","svg":""},{"name":"C","size":8,"bricks":"__rrrr___rrrrrr_rrr___rrrr______rr______rrr___rr_rrrrrr___rrrr","svg":""},{"name":"D","size":8,"bricks":"_GGGGG____GG__G___GG__GG__GG__GG__GG__GG__GG__GG__GG__G__GGGGG","svg":""},{"name":"e","size":8,"bricks":"__tttt___tttttt_tt____tttt____tttttttttttt_______tt__tt___tttt_","svg":""},{"name":"icon:wind","size":9,"bricks":"_ss______s___PPPP_s_________sssssss___________sssssss_s________s___PPPP__ss","svg":""},{"name":"icon:sturdy_bricks","size":7,"bricks":"ttbttttbtttbtt____W_____W_W___W___W_______WWW____","svg":""},{"name":"icon:respawn","size":9,"bricks":"tttt___ttttt__t__ttta_ttt_______________________________W_________________WWW","svg":""},{"name":"Elephant","size":18,"bricks":"_________________________llll_________lll_llllll_lll___lsssllllllllsssl__lsssllllllllsssl__lsssllBllBllsssl__lssllWllllWllssl___ll__llllll__ll_________llll_______________ll______________llll______________ll________________________________________________________________________________________________________________________________________","svg":"","color":""},{"name":"Orca","size":20,"bricks":"____________________________________________________________________________________________BBBBB____BBB_BBB___BBBBBBB____BBBBB___BBBBBBBBB____BBB___BBBBWBBWWW_____BBBBBBBBBBBWWWW_____BBBBBBBBBBWWWWW_____BBBBBBBBBWWWWW_______BBBBBBBWWWWW___________WWBBWWW______________BBB_BB______________BB__B______________________________________________________________________________________________________________________________","svg":"","color":"#1c71d8"},{"name":"Shark","size":17,"bricks":"__________________________________________g_______________ggg____________ggggggg_________ggggggggg_______ggggggggggg_____gggggWWWggggg____gBgWWWWWWWgBg___ggWWWWrWrWWWWgg__ggWWWrrrrrWWWgg_ggWWWrrrrrrrWWWggggWWrrrrrrrrrWWgggWWWrWrWrWrWrWWWggWWrrWWWWWWWrrWWggWWWWWWWWWWWWWWWg_________________","svg":"","color":"#3584e4"},{"name":"Bird","size":13,"bricks":"_______RRR____R____RSSSR___RR__RSSWWWR__RSR_RSWWBWR__RSSRRSW_WWyy_RSSSRSWWWR___RSSSSSSRR_____RRSSyyyy______RSyyyyy___RRRRSyyyy____RSSSRyyy_____RRRR________","svg":"","color":""},{"name":"Tux","size":14,"bricks":"_____gggg________gggggggg_____gggggggggg____gggggggggg___gggggggggggg__gggWBggWBggg__gggBBggBBggg__ggggyyyygggg_ggggggyyggggggggggWWWWWWggggg_gWWWWWWWWg_g__WWWWWWWWWW____WWWWWWWWWW____yyy____yyy__","svg":"","color":"#62a0ea"},{"name":"Armenia","size":6,"bricks":"_______rrrr__bbbb__yyyy_____________","svg":"","color":""},{"name":"Austria","size":6,"bricks":"_______rrrr__WWWW__rrrr______","svg":"","color":""},{"name":"Benin","size":8,"bricks":"_________kkyyyy__kkyyyy__kkrrrr__kkrrrr__________________________","svg":"","color":""},{"name":"Botswana","size":10,"bricks":"___________tttttttt__tttttttt__tttttttt__WWWWWWWW__BBBBBBBB__WWWWWWWW__tttttttt__tttttttt__tttttttt___________","svg":"","color":""},{"name":"Bulgaria","size":6,"bricks":"_______WWWW__cccc__rrrr_____________","svg":"","color":""},{"name":"Canada","size":7,"bricks":"________rWWWr__rWrWr__rWWWr______________________","svg":"","color":""},{"name":"Chad","size":8,"bricks":"_________bbyyRR__bbyyRR__bbyyRR","svg":"","color":""},{"name":"China","size":8,"bricks":"_________RRyRRR__RyRyRR__RRyRRR__RRRRRR","svg":"","color":""},{"name":"Colombia","size":7,"bricks":"________yyyyy__yyyyy__bbbbb__RRRRR_______________","svg":"","color":""},{"name":"Republic of the Congo","size":7,"bricks":"________kkkyy__kkyyr__kyyrr__yyrrr_______________","svg":"","color":""},{"name":"C\xf4te d\'Ivoire","size":8,"bricks":"_________OOWWGG__OOWWGG__OOWWGG","svg":"","color":""},{"name":"Denmark","size":8,"bricks":"_________rrWrrr__rrWrrr__WWWWWW__rrWrrr__rrWrrr","svg":"","color":""},{"name":"El Salvador","size":8,"bricks":"_________bbbbbb__bbbbbb__WWWkWW__WWkWWW__bbbbbb__bbbbbb","svg":"","color":""},{"name":"Egypt","size":8,"bricks":"_________RRRRRR__RRRRRR__WWWyWW__WWyWWW__gggggg__gggggg","svg":"","color":"#1c71d8"},{"name":"Estonia","size":8,"bricks":"_________tttttt__tttttt__gggggg__gggggg__WWWWWW__WWWWWW","svg":"","color":"#986a44"},{"name":"Finland","size":6,"bricks":"_______WtWW__tttt__WtWW_____________","svg":"","color":""},{"name":"Gabon","size":5,"bricks":"______CCC__yyy__ttt______","svg":"","color":""},{"name":"Georgia","size":9,"bricks":"__________WrWrWrW__WWWrWWW__rrrrrrr__WWWrWWW__WrWrWrW__________________","svg":"","color":""},{"name":"Guinea","size":8,"bricks":"_________rryycc__rryycc__rryycc","svg":"","color":""},{"name":"Indonesia","size":6,"bricks":"_______rrrr__rrrr__WWWW__WWWW_______","svg":"","color":""},{"name":"icon:one_more_choice","size":7,"bricks":"ttt____tbbb___tbttt__tbtbbb__btbbb___tbbb____bbb_","svg":""},{"name":"icon:instant_upgrade","size":5,"bricks":"ttt__tbbb_tbbb_tbbb__bbb_","svg":""},{"name":"icon:checkmark_checked","size":6,"bricks":"_WWWWGWBBBGGGGBGGWWGGGBWWBGBBW_WWWW_","svg":""},{"name":"icon:checkmark_unchecked","size":6,"bricks":"_WWWW_WBBBBWWBBBBWWBBBBWWBBBBW_WWWW_","svg":""},{"name":"icon:fullscreen","size":6,"bricks":"WW__WWW____W____________W____WWW__WW","svg":""},{"name":"icon:exit_fullscreen","size":6,"bricks":"_W__W_WW__WW____________WW__WW_W__W_","svg":""}]'); + +},{}],"h1X9A":[function(require,module,exports,__globalThis) { +module.exports = JSON.parse("\"29022953\""); + +},{}],"cvg5m":[function(require,module,exports,__globalThis) { +var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); +parcelHelpers.defineInteropFlag(exports); +parcelHelpers.export(exports, "rawUpgrades", ()=>rawUpgrades); +const rawUpgrades = [ + { + requires: "", + threshold: 0, + giftable: false, + id: "extra_life", + name: "+1 life", + max: 7, + help: (lvl)=>`Survive dropping the ball ${lvl} time${lvl > 1 ? "s" : ""}.`, + fullHelp: `Normally, you just have one life, and the run is over as soon as you drop it. + With this perk, you can survive dropping the ball once. A heart in the top right corner will remind you of how many extra lives you have. ` + }, + { + requires: "", + threshold: 0, + id: "streak_shots", + giftable: true, + name: "Single puck hit streak", + max: 1, + help: (lvl)=>`More coins if you break many bricks at once`, + fullHelp: `Every time you break a brick, your combo (number of coins per bricks) increases by one. However, as soon as the ball touches your puck, + the combo is reset to its default value, and you'll just get one coin per brick. So you should try to hit many bricks in one go for more score. + Once your combo rises above the base value, your puck will become red to remind you that it will destroy your combo to touch it with the ball. + This can stack with other combo related perks, the combo will rise faster but reset more easily as any of the conditions is enough to reset it. ` + }, + { + requires: "", + threshold: 0, + id: "base_combo", + giftable: true, + name: "+3 base combo", + max: 7, + help: (lvl)=>`Every brick drops at least ${1 + lvl * 3} coins.`, + fullHelp: `Your combo (number of coins per bricks) normally starts at 1 at the beginning of the level, and resets to one when you bounce around without hitting anything. + With this perk, the combo starts 3 points higher, so you'll always get at least 4 coins per brick. Whenever your combo reset, it will go back to 4 and not 1. + Your ball will glitter a bit to indicate that its combo is higher than one.` + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "slow_down", + name: "Slower ball", + max: 2, + help: (lvl)=>`Ball moves ${lvl > 1 ? "even" : ""} more slowly.`, + fullHelp: `The ball starts relatively slow, but every level of your run it will start a bit faster, and it will also accelerate if you spend a lot of time in one level. This perk makes it + more manageable. You can get it at the start every time by enabling kid mode in the menu.` + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "bigger_puck", + name: "Bigger puck", + max: 2, + help: (lvl)=>`Easily catch ${lvl > 1 ? "even" : ""} more coins.`, + fullHelp: `A bigger puck makes it easier to never miss the ball and to catch more coins, and also to precisely angle the bounces (the ball's angle only depends on where it hits the puck). + However, a large puck is harder to use around the sides of the level, and will make it sometimes unavoidable to miss (not hit anything) which comes with downsides. ` + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "viscosity", + name: "Viscosity", + max: 3, + help: (lvl)=>`${lvl > 1 ? "Even slower" : "Slower"} coins fall.`, + fullHelp: `Coins normally accelerate with gravity and explosions to pretty high speeds. This perk constantly makes them slow down, as if they were in some sort of viscous liquid. + This makes catching them easier, and combines nicely with perks that influence the coin's movement. ` + }, + { + requires: "", + threshold: 0, + id: "left_is_lava", + giftable: true, + name: "Avoid left side", + max: 1, + help: (lvl)=>`More coins if you don't touch the left side.`, + fullHelp: `Whenever you break a brick, your combo will increase by one, so you'll get one more coin from all the next bricks you break. + However, your combo will reset as soon as your ball hits the left side . + As soon as your combo rises, the left side becomes red to remind you that you should avoid hitting them. + The effect stacks with other combo perks, combo rises faster with more upgrades but will also reset if any + of the reset conditions are met.` + }, + { + requires: "", + threshold: 0, + id: "right_is_lava", + giftable: true, + name: "Avoid right side", + max: 1, + help: (lvl)=>`More coins if you don't touch the right side.`, + fullHelp: `Whenever you break a brick, your combo will increase by one, so you'll get one more coin from all the next bricks you break. + However, your combo will reset as soon as your ball hits the right side . + As soon as your combo rises, the right side becomes red to remind you that you should avoid hitting them. + The effect stacks with other combo perks, combo rises faster with more upgrades but will also reset if any + of the reset conditions are met.` + }, + { + requires: "", + threshold: 0, + id: "top_is_lava", + giftable: true, + name: "Sky is the limit", + max: 1, + help: (lvl)=>`More coins if you don't touch the top.`, + fullHelp: `Whenever you break a brick, your combo will increase by one. However, your combo will reset as soon as your ball hit the top of the screen. + When your combo is above the minimum, a red bar will appear at the top to remind you that you should avoid hitting it. + The effect stacks with other combo perks.` + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "skip_last", + name: "Easy Cleanup", + max: 7, + help: (lvl)=>`The last ${lvl > 1 ? lvl + " bricks" : "brick"} left will self-destruct.`, + fullHelp: `You need to break all bricks to go to the next level. However, it can be hard to get the last ones. + Clearing a level early brings extra choices when upgrading. Never missing the bricks is also very beneficial. + So if you find it difficult to break the last bricks, getting this perk a few time can help.` + }, + { + requires: "", + threshold: 500, + id: "telekinesis", + giftable: true, + name: "Puck controls ball", + max: 2, + help: (lvl)=>lvl == 1 ? `Control the ball's trajectory.` : `Stronger effect on the ball`, + fullHelp: `Right after the ball hits your puck, you'll be able to direct it left and right by moving your puck. + The effect stops when the ball hits a brick and resets the next time it touches the puck. It also does nothing when the ball is going downward after bouncing at the top. ` + }, + { + requires: "", + threshold: 1000, + giftable: false, + id: "coin_magnet", + name: "Coins magnet", + max: 3, + help: (lvl)=>lvl == 1 ? `Puck attracts coins.` : `Stronger effect on the coins`, + fullHelp: `Directs the coins to the puck. The effect is stronger if the coin is close to it already. Catching 90% or 100% of coins bring special bonuses in the game. + Another way to catch more coins is to hit bricks from the bottom. The ball's speed and direction impacts the spawned coin's velocity. ` + }, + { + requires: "", + threshold: 1500, + id: "multiball", + giftable: true, + name: "+1 ball", + max: 6, + help: (lvl)=>`Start every levels with ${lvl + 1} balls.`, + fullHelp: `As soon as you drop the ball in Breakout 71, you loose. With this perk, you get two balls, and so you can afford to lose one. + The lost balls come back on the next level or whenever you use one of your extra lives, if you picked that perk. Having more than one balls makes + some further perks available, and of course clears the level faster.` + }, + { + requires: "", + threshold: 2000, + giftable: false, + id: "smaller_puck", + name: "Smaller puck", + max: 2, + help: (lvl)=>lvl == 1 ? `Also gives +5 base combo.` : `Even smaller puck and higher base combo`, + fullHelp: `This makes the puck smaller, which in theory makes some corner shots easier, but really just raises the difficulty. + That's why you also get a nice bonus of +5 coins per brick for all bricks you'll break after picking this. ` + }, + { + requires: "", + threshold: 3000, + id: "pierce", + giftable: true, + name: "Piercing", + max: 3, + help: (lvl)=>`Ball pierces ${3 * lvl} bricks after a puck bounce.`, + fullHelp: `The ball normally bounces as soon as it touches something. With this perk, it will continue its trajectory for up to 3 bricks broken. + After that, it will bounce on the 4th brick, and you'll need to touch the puck to reset the counter. This combines particularly well with Sapper. ` + }, + { + requires: "", + threshold: 4000, + id: "picky_eater", + giftable: true, + name: "Picky eater", + max: 1, + help: (lvl)=>`More coins if you break bricks color by color.`, + fullHelp: `Whenever you break a brick the same color as your ball, your combo increases by one. + If it's a different color, the ball takes that new color, but the combo resets. + The bricks with the right color will get a white border. + Once you get a combo higher than your minimum, the bricks of the wrong color will get a red halo. + If you have more than one ball, they all change color whenever one of them hits a brick. + + ` + }, + { + requires: "", + threshold: 5000, + giftable: false, + id: "metamorphosis", + name: "Stain", + max: 1, + help: (lvl)=>`Coins color the bricks they touch.`, + fullHelp: `With this perk, coins will be of the color of the brick they come from, and will color the first brick they touch in the same color. Coins spawn with the speed + of the ball that broke them, which means you can aim a bit in the direction of the bricks you want to "paint". + ` + }, + { + requires: "", + threshold: 6000, + id: "compound_interest", + giftable: true, + name: "Compound interest", + max: 1, + help: ()=>`+1 combo per brick broken, resets on coin lost`, + fullHelp: `Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins + with your puck, as any lost coin will decrease your combo by one point. One your combo is above the minimum, the bottom of the play area will + have a red line to remind you that coins should not go there. This perk combines with other combo perks, the combo will rise faster but reset more easily. + ` + }, + { + requires: "", + threshold: 7000, + id: "hot_start", + giftable: true, + name: "Hot start", + max: 3, + help: (lvl)=>`Start at combo ${lvl * 15 + 1}, -${lvl} combo per second`, + fullHelp: `At the start of every level, your combo will start at +15 points, but then every second it will be decreased by one. This means the first 15 seconds in a level will spawn + many more coins than the following ones, and you should make sure that you clear the level quickly. The effect stacks with other combo related perks, so you might be able to raise + the combo after the 15s timeout, but it will keep ticking down. Every time you take the perk again, the effect will be more dramatic. + ` + }, + { + requires: "", + threshold: 9000, + id: "sapper", + giftable: true, + name: "Sapper", + max: 7, + help: (lvl)=>lvl === 1 ? "The first brick broken becomes a bomb." : `The first ${lvl} bricks broken become bombs.`, + fullHelp: `Instead of just disappearing, the first brick you break will be replaced by a bomb brick. Bouncing the ball on the puck re-arms the effect. "Piercing" will instantly + detonate the bomb that was just placed. Leveling-up this perk will allow you to place more bombs. Remember that bombs impact the velocity of nearby coins, so too many explosions + could make it hard to catch the fruits of your hard work. + ` + }, + { + requires: "", + threshold: 11000, + id: "bigger_explosions", + name: "Kaboom", + max: 1, + giftable: false, + help: (lvl)=>"Bigger explosions", + fullHelp: `The default explosion clears a 3x3 square, with this it becomes a 5x5 square, and the blowback on the coins is also significantly stronger. ` + }, + { + requires: "", + threshold: 13000, + giftable: false, + id: "extra_levels", + name: "+1 level", + max: 3, + help: (lvl)=>`Play ${lvl + 7} levels instead of 7`, + fullHelp: `The default run can last a max of 7 levels, after which the game is over and whatever score you reached is your run score. + Each level of this perk lets you go one level higher. The last levels are often the ones where you make the most score, so the difference can be dramatic.` + }, + { + requires: "", + threshold: 15000, + giftable: false, + id: "pierce_color", + name: "Color pierce", + max: 1, + help: (lvl)=>`Balls pierce bricks of their color.`, + fullHelp: `Whenever a ball hits a brick of the same color, it will just go through unimpeded. + Once it reaches a brick of a different color, it will break it, take its color and bounce.` + }, + { + requires: "", + threshold: 18000, + giftable: false, + id: "soft_reset", + name: "Soft reset", + max: 2, + help: (lvl)=>`Combo grows ${lvl > 1 ? "even" : ""} slower but resets less.`, + fullHelp: `The combo normally climbs every time you break a brick. This will sometimes cancel that climb, but also limit the impact of a combo reset.` + }, + { + requires: "multiball", + threshold: 21000, + giftable: false, + id: "ball_repulse_ball", + name: "Personal space", + max: 3, + help: (lvl)=>lvl === 1 ? `Balls repulse balls.` : "Stronger repulsion force", + fullHelp: `Balls that are less than half a screen width away will start repulsing each other. The repulsion force is stronger if they are close to each other. + Particles will jet out to symbolize this force being applied. This perk is only offered if you have more than one ball already.` + }, + { + requires: "multiball", + threshold: 25000, + giftable: false, + id: "ball_attract_ball", + name: "Gravity", + max: 3, + help: (lvl)=>lvl === 1 ? `Balls attract balls.` : "Stronger attraction force", + fullHelp: `Balls that are more than half a screen width away will start attracting each other. The attraction force is stronger when they are furthest away from each other. + Rainbow particles will fly to symbolize the attraction force. This perk is only offered if you have more than one ball already.` + }, + { + requires: "", + threshold: 30000, + giftable: false, + id: "puck_repulse_ball", + name: "Soft landing", + max: 2, + help: (lvl)=>lvl === 1 ? `Puck repulses balls.` : "Stronger repulsion force", + fullHelp: `When a ball gets close to the puck, it will start slowing down, and even potentially bouncing without touching the puck.` + }, + { + requires: "", + threshold: 35000, + giftable: false, + id: "wind", + name: "Wind", + max: 3, + help: (lvl)=>lvl === 1 ? `Puck position creates wind.` : "Stronger wind force", + fullHelp: `The wind depends on where your puck is, if it's in the center of the screen nothing happens, if it's on the left it will blow leftwise, if it's on the right of the screen + then it will blow rightwise. The wind affects both the balls and coins.` + }, + { + requires: "", + threshold: 40000, + giftable: false, + id: "sturdy_bricks", + name: "Sturdy bricks", + max: 4, + help: (lvl)=>lvl === 1 ? `Bricks sometimes resist hits but drop more coins.` : "Bricks resist more and drop more coins", + fullHelp: `With level one of this perk, the ball has a 20% chance to bounce harmlessly on bricks, + but generates 10% more coins when it does break one. + This +10% is not shown in the combo number. At level 4 the ball has 80% chance of bouncing and brings 40% more coins.` + }, + { + requires: "", + threshold: 45000, + giftable: false, + id: "respawn", + name: "Respawn", + max: 4, + help: (lvl)=>lvl === 1 ? `The first brick hit of two+ will respawn.` : "More bricks can respawn", + fullHelp: `After breaking two or more bricks, when the ball hits the puck, the first brick will be put back in place, provided that space is free and the brick wasn't a bomb. + Some particle effect will let you know where bricks will appear. Levelling this up lets you respawn up to 4 bricks at a time, but there should always be at least one destroyed. + ` + }, + { + requires: "", + threshold: 50000, + giftable: false, + id: "one_more_choice", + name: "+1 choice until run end", + max: 3, + help: (lvl)=>lvl === 1 ? `Further level ups will offer one more option in the list.` : "Even more options", + fullHelp: `Every upgrade menu will have one more option. + Doesn't increase the number of upgrades you can pick. + ` + }, + { + requires: "", + threshold: 55000, + giftable: false, + id: "instant_upgrade", + name: "+2 upgrades now", + max: 2, + help: (lvl)=>lvl === 1 ? `-1 choice until run end.` : "Even fewer options", + fullHelp: `Immediately pick two upgrades, so that you get one free one and one to repay the one used to get this perk. Every further menu to pick upgrades will have fewer options to choose from.` + } +]; + +},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"gkKU3":[function(require,module,exports,__globalThis) { +exports.interopDefault = function(a) { + return a && a.__esModule ? a : { + default: a + }; +}; +exports.defineInteropFlag = function(a) { + Object.defineProperty(a, '__esModule', { + value: true + }); +}; +exports.exportAll = function(source, dest) { + Object.keys(source).forEach(function(key) { + if (key === 'default' || key === '__esModule' || Object.prototype.hasOwnProperty.call(dest, key)) return; + Object.defineProperty(dest, key, { + enumerable: true, + get: function() { + return source[key]; + } + }); + }); + return dest; +}; +exports.export = function(dest, destName, get) { + Object.defineProperty(dest, destName, { + enumerable: true, + get: get + }); +}; + +},{}],"d5NoS":[function(require,module,exports,__globalThis) { +var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); +parcelHelpers.defineInteropFlag(exports); +parcelHelpers.export(exports, "options", ()=>options); +var _game = require("./game"); +const options = { + sound: { + default: true, + name: `Game sounds`, + help: `Can slow down some phones.`, + afterChange: ()=>{}, + disabled: ()=>false + }, + "mobile-mode": { + default: window.innerHeight > window.innerWidth, + name: `Mobile mode`, + help: `Leaves space for your thumb.`, + afterChange () { + (0, _game.fitSize)(); + }, + disabled: ()=>false + }, + basic: { + default: false, + name: `Basic graphics`, + help: `Better performance on older devices.`, + afterChange: ()=>{}, + disabled: ()=>false + }, + pointerLock: { + default: false, + name: `Mouse pointer lock`, + help: `Locks and hides the mouse cursor.`, + afterChange: ()=>{}, + disabled: ()=>!(0, _game.gameCanvas).requestPointerLock + }, + easy: { + default: false, + name: `Kids mode`, + help: `Start future runs with "slower ball".`, + afterChange: ()=>{}, + disabled: ()=>false + }, + record: { + default: false, + name: `Record gameplay videos`, + help: `Get a video of each level.`, + afterChange: ()=>{}, + disabled () { + return window.location.search.includes("isInWebView=true"); + } + } +}; + +},{"./game":"edeGs","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["hhTAC","3qndx"], "3qndx", "parcelRequire94c2") + + + + diff --git a/package-lock.json b/package-lock.json index f2e0e98..aa72fb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1178,6 +1178,19 @@ "node": ">=16" } }, + "node_modules/@parcel/transformer-html/node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@parcel/transformer-image": { "version": "2.13.3", "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.13.3.tgz", @@ -4479,12 +4492,15 @@ } }, "node_modules/srcset": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", - "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-5.0.1.tgz", + "integrity": "sha512-/P1UYbGfJVlxZag7aABNRrulEXAwCSDo7fklafOQrantuPTDmYgijJMks2zusPCVzgW9+4P69mq7w6pYuZpgxw==", "dev": true, + "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">=12" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/src/game.ts b/src/game.ts index 442e219..2e81c68 100644 --- a/src/game.ts +++ b/src/game.ts @@ -5,7 +5,6 @@ import { Coin, colorString, Flash, - FlashTypes, Level, PerkId, PerksMap, @@ -13,27 +12,18 @@ import { RunStats, Upgrade, } from "./types"; -import { OptionId, options } from "./options"; +import { OptionId, options} from "./options"; const MAX_COINS = 400; const MAX_PARTICLES = 600; export const gameCanvas = document.getElementById("game") as HTMLCanvasElement; -let ctx = gameCanvas.getContext("2d", { alpha: false }); +const ctx = gameCanvas.getContext("2d", { alpha: false }) as CanvasRenderingContext2D; const puckColor = "#FFF"; let ballSize = 20; const coinSize = Math.round(ballSize * 0.8); const puckHeight = ballSize; -allLevels.forEach((l, li) => { - l.threshold = - li < 8 - ? 0 - : Math.round( - Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li, - ); - l.sortKey = ((Math.random() + 3) / 3.5) * l.bricks.filter((i) => i).length; -}); let runLevels: Level[] = []; @@ -49,7 +39,13 @@ bombSVG.src = // Whatever let puckWidth = 200; -const perks: PerksMap = {}; +const makeEmptyPerksMap = ()=>{ + const p = {} as any + upgrades.forEach(u=>p[u.id]=0) + return p as PerksMap +} + +const perks: PerksMap = makeEmptyPerksMap(); let baseSpeed = 12; // applied to x and y let combo = 1; @@ -254,6 +250,7 @@ function spawnExplosion( vy: (Math.random() - 0.5) * 30, color, duration, + ethereal:false, }); } } @@ -281,8 +278,8 @@ function addToScore(coin: Coin) { time: levelTime, size: coinSize / 2, color: coin.color, - x: coin.previousx, - y: coin.previousy, + x: coin.previousX, + y: coin.previousY, vx: (gameCanvas.width - coin.x) / 100, vy: -coin.y / 100, ethereal: true, @@ -309,19 +306,25 @@ function resetBalls() { } for (let i = 0; i < count; i++) { const x = puck - puckWidth / 2 + perBall * (i + 1); + const vx=Math.random() > 0.5 ? baseSpeed : -baseSpeed + balls.push({ x, - previousx: x, + previousX: x, y: gameZoneHeight - 1.5 * ballSize, - previousy: gameZoneHeight - 1.5 * ballSize, - vx: Math.random() > 0.5 ? baseSpeed : -baseSpeed, + previousY: gameZoneHeight - 1.5 * ballSize, + vx , + previousVX:vx, vy: -baseSpeed, + previousVY: -baseSpeed, + sx: 0, sy: 0, sparks: 0, piercedSinceBounce: 0, hitSinceBounce: 0, hitItem: [], + bouncesList: [], sapperUses: 0, }); } @@ -334,11 +337,13 @@ function putBallsAtPuck() { balls.forEach((ball, i) => { const x = puck - puckWidth / 2 + perBall * (i + 1); ball.x = x; - ball.previousx = x; + ball.previousX = x; ball.y = gameZoneHeight - 1.5 * ballSize; - ball.previousy = ball.y; + ball.previousY = ball.y; ball.vx = Math.random() > 0.5 ? baseSpeed : -baseSpeed; + ball.previousVX=ball.vx ball.vy = -baseSpeed; + ball.previousVY=ball.vy ball.sx = 0; ball.sy = 0; ball.hitItem = []; @@ -479,7 +484,7 @@ function getPossibleUpgrades() { .filter((u) => !u?.requires || perks[u?.requires]); } -function shuffleLevels(nameToAvoid = null) { +function shuffleLevels(nameToAvoid :string|null= null) { const target = nextRunOverrides?.level; const firstLevel = nextRunOverrides?.level ? allLevels.filter((l) => l.name === target) @@ -497,7 +502,7 @@ function shuffleLevels(nameToAvoid = null) { } function getUpgraderUnlockPoints() { - let list = []; + let list = [] as {threshold:number,title:string}[]; upgrades.forEach((u) => { if (u.threshold) { @@ -553,7 +558,7 @@ let isCreativeModeRun = false; let pauseUsesDuringRun = 0; -function restart(creativeModePerks: PerksMap | undefined = undefined) { +function restart(creativeModePerks: Partial | undefined = undefined) { // When restarting, we want to avoid restarting with the same level we're on, so we exclude from the next // run's level list totalScoreAtRunStart = getTotalScore(); @@ -613,7 +618,7 @@ gameCanvas.addEventListener("mouseup", (e) => { } else { play(); if (isSettingOn("pointerLock")) { - gameCanvas.requestPointerLock(); + gameCanvas.requestPointerLock().then(); } } }); @@ -689,10 +694,10 @@ function shouldPierceByColor( function ballBrickHitCheck(ball: Ball) { const radius = ballSize / 2; // Make ball/coin bonce, and return bricks that were hit - const { x, y, previousx, previousy } = ball; + const { x, y, previousX, previousY } = ball; - const vhit = hitsSomething(previousx, y, radius); - const hhit = hitsSomething(x, previousy, radius); + const vhit = hitsSomething(previousX, y, radius); + const hhit = hitsSomething(x, previousY, radius); const chit = (typeof vhit == "undefined" && typeof hhit == "undefined" && @@ -714,13 +719,13 @@ function ballBrickHitCheck(ball: Ball) { if (typeof vhit !== "undefined" || typeof chit !== "undefined") { if (!pierce) { - ball.y = ball.previousy; + ball.y = ball.previousY; ball.vy *= -1; } } if (typeof hhit !== "undefined" || typeof chit !== "undefined") { if (!pierce) { - ball.x = ball.previousx; + ball.x = ball.previousX; ball.vx *= -1; } } @@ -731,10 +736,10 @@ function ballBrickHitCheck(ball: Ball) { function coinBrickHitCheck(coin: Coin) { // Make ball/coin bonce, and return bricks that were hit const radius = coinSize / 2; - const { x, y, previousx, previousy } = coin; + const { x, y, previousX, previousY } = coin; - const vhit = hitsSomething(previousx, y, radius); - const hhit = hitsSomething(x, previousy, radius); + const vhit = hitsSomething(previousX, y, radius); + const hhit = hitsSomething(x, previousY, radius); const chit = (typeof vhit == "undefined" && typeof hhit == "undefined" && @@ -742,7 +747,7 @@ function coinBrickHitCheck(coin: Coin) { undefined; if (typeof vhit !== "undefined" || typeof chit !== "undefined") { - coin.y = coin.previousy; + coin.y = coin.previousY; coin.vy *= -1; // Roll on corners @@ -759,7 +764,7 @@ function coinBrickHitCheck(coin: Coin) { } } if (typeof hhit !== "undefined" || typeof chit !== "undefined") { - coin.x = coin.previousx; + coin.x = coin.previousX; coin.vx *= -1; } return vhit ?? hhit ?? chit; @@ -767,14 +772,14 @@ function coinBrickHitCheck(coin: Coin) { function bordersHitCheck(coin: Coin | Ball, radius: number, delta: number) { if (coin.destroyed) return; - coin.previousx = coin.x; - coin.previousy = coin.y; + coin.previousX = coin.x; + coin.previousY = coin.y; coin.x += coin.vx * delta; coin.y += coin.vy * delta; coin.sx ||= 0; coin.sy ||= 0; - coin.sx += coin.previousx - coin.x; - coin.sy += coin.previousy - coin.y; + coin.sx += coin.previousX - coin.x; + coin.sy += coin.previousY - coin.y; coin.sx *= 0.9; coin.sy *= 0.9; @@ -976,7 +981,7 @@ function tick() { const baseParticle = !isSettingOn("basic") && (combo - baseCombo()) * Math.random() > 5 && running && { - type: "particle" as FlashTypes, + type: "particle" as const, duration: 100 * (Math.random() + 1), time: levelTime, size: coinSize / 2, @@ -1057,8 +1062,8 @@ function isTelekinesisActive(ball: Ball) { } function ballTick(ball: Ball, delta: number) { - ball.previousvx = ball.vx; - ball.previousvy = ball.vy; + ball.previousVX = ball.vx; + ball.previousVY = ball.vy; let speedLimitDampener = 1 + @@ -1158,7 +1163,7 @@ function ballTick(ball: Ball, delta: number) { resetCombo(ball.x, ball.y + ballSize); } sounds.wallBeep(ball.x); - ball.bouncesList?.push({ x: ball.previousx, y: ball.previousy }); + ball.bouncesList?.push({ x: ball.previousX, y: ball.previousY }); } // Puck collision @@ -1208,8 +1213,8 @@ function ballTick(ball: Ball, delta: number) { ball.piercedSinceBounce = 0; ball.bouncesList = [ { - x: ball.previousx, - y: ball.previousy, + x: ball.previousX, + y: ball.previousY, }, ]; } @@ -1270,6 +1275,7 @@ function ballTick(ball: Ball, delta: number) { y: ball.y, vx: (Math.random() - 0.5) * baseSpeed, vy: (Math.random() - 0.5) * baseSpeed, + ethereal:false, }); ball.sparks = 0; } @@ -1444,8 +1450,8 @@ function getHistograms() { // One bin per unique value, max 10 const binsCount = Math.min(values.length, 10); if (binsCount < 3) return ""; - const bins = []; - const binsTotal = []; + const bins = [] as number[]; + const binsTotal = [] as number[]; for (let i = 0; i < binsCount; i++) { bins.push(0); binsTotal.push(0); @@ -1623,11 +1629,11 @@ function explodeBrick(index: number, ball: Ball, isExplosion: boolean) { color: perks.metamorphosis ? color : "gold", x: cx, y: cy, - previousx: cx, - previousy: cy, + previousX: cx, + previousY: cy, // Use previous speed because the ball has already bounced - vx: ball.previousvx * (0.5 + Math.random()), - vy: ball.previousvy * (0.5 + Math.random()), + vx: ball.previousVX * (0.5 + Math.random()), + vy: ball.previousVY * (0.5 + Math.random()), sx: 0, sy: 0, a: Math.random() * Math.PI * 2, @@ -1762,8 +1768,11 @@ function render() { ) as CanvasRenderingContext2D; bgctx.fillStyle = level.color || "#000"; bgctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height); - bgctx.fillStyle = ctx.createPattern(background, "repeat"); - bgctx.fillRect(0, 0, width, height); + const pattern=ctx.createPattern(background, "repeat") + if(pattern){ + bgctx.fillStyle = pattern; + bgctx.fillRect(0, 0, width, height); + } } ctx.drawImage(backgroundCanvas, 0, 0); @@ -1809,12 +1818,12 @@ function render() { ); flashes.forEach((flash) => { - const { x, y, time, color, size, type, text, duration } = flash; + const { x, y, time, color, size, type, duration } = flash; const elapsed = levelTime - time; ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); if (type === "text") { ctx.globalCompositeOperation = "source-over"; - drawText(ctx, text, color, size, x, y - elapsed / 10); + drawText(ctx, flash.text, color, size, x, y - elapsed / 10); } else if (type === "particle") { ctx.globalCompositeOperation = "screen"; drawBall(ctx, color, size, x, y); @@ -1952,7 +1961,7 @@ function render() { } let cachedBricksRender = document.createElement("canvas"); -let cachedBricksRenderKey = null; +let cachedBricksRenderKey = ''; function renderAllBricks() { ctx.globalAlpha = 1; @@ -2007,14 +2016,14 @@ function renderAllBricks() { ctx.drawImage(cachedBricksRender, offsetX, 0); } -let cachedGraphics = {}; +let cachedGraphics : {[k:string]:HTMLCanvasElement}= {}; function drawPuck( ctx: CanvasRenderingContext2D, color: colorString, puckWidth: number, puckHeight: number, - yoffset = 0, + yOffset = 0, ) { const key = "puck" + color + "_" + puckWidth + "_" + puckHeight; @@ -2044,7 +2053,7 @@ function drawPuck( ctx.drawImage( cachedGraphics[key], Math.round(puck - puckWidth / 2), - gameZoneHeight - puckHeight * 2 + yoffset, + gameZoneHeight - puckHeight * 2 + yOffset, ); } @@ -2549,10 +2558,18 @@ window.addEventListener("visibilitychange", () => { } }); -const scoreDisplay = document.getElementById("score"); +const scoreDisplay = document.getElementById("score") as HTMLButtonElement; let alertsOpen = 0, - closeModal = null; + closeModal :null |( ()=>void) = null; +type AsyncAlertAction = { + text?: string; + value?: t; + help?: string; + disabled?: boolean; + icon?: string; + className?: string; + } function asyncAlert({ title, text, @@ -2563,14 +2580,7 @@ function asyncAlert({ }: { title?: string; text?: string; - actions?: { - text?: string; - value?: t; - help?: string; - disabled?: boolean; - icon?: string; - className?: string; - }[]; + actions?: AsyncAlertAction[]; textAfterButtons?: string; allowClose?: boolean; actionsAsGrid?: boolean; @@ -2581,7 +2591,7 @@ function asyncAlert({ document.body.appendChild(popupWrap); popupWrap.className = "popup " + (actionsAsGrid ? "actionsAsGrid " : ""); - function closeWithResult(value: t | void) { + function closeWithResult(value: t | undefined) { resolve(value); // Doing this async lets the menu scroll persist if it's shown a second time setTimeout(() => { @@ -2595,10 +2605,10 @@ function asyncAlert({ closeButton.className = "close-modale"; closeButton.addEventListener("click", (e) => { e.preventDefault(); - closeWithResult(null); + closeWithResult(undefined); }); closeModal = () => { - closeWithResult(null); + closeWithResult(undefined); }; popupWrap.appendChild(closeButton); } @@ -2621,7 +2631,7 @@ function asyncAlert({ popup.appendChild(buttons); actions - .filter((i) => i) + ?.filter((i) => i) .forEach(({ text, value, help, disabled, className = "", icon = "" }) => { const button = document.createElement("button"); @@ -2656,10 +2666,10 @@ ${icon} popup.querySelector("button:not([disabled])") as HTMLButtonElement )?.focus(); }).then( - (v: t | null) => { + (v: unknown) => { alertsOpen--; closeModal = null; - return v; + return v as t | undefined; }, () => { closeModal = null; @@ -2669,14 +2679,13 @@ ${icon} } // Settings -let cachedSettings = {}; +let cachedSettings : Partial<{[key in OptionId]:boolean}>= {}; export function isSettingOn(key: OptionId) { if (typeof cachedSettings[key] == "undefined") { try { - cachedSettings[key] = JSON.parse( - localStorage.getItem("breakout-settings-enable-" + key), - ); + const ls=localStorage.getItem("breakout-settings-enable-" + key) + if(ls) cachedSettings[key] = JSON.parse(ls) as boolean; } catch (e) { console.warn(e); } @@ -2694,7 +2703,7 @@ export function toggleSetting(key: OptionId) { } catch (e) { console.warn(e); } - if (options[key].afterChange) options[key].afterChange(); + options[key].afterChange(); } scoreDisplay.addEventListener("click", (e) => { @@ -2732,7 +2741,7 @@ async function openScorePanel() { } } -document.getElementById("menu").addEventListener("click", (e) => { +document.getElementById("menu")?.addEventListener("click", (e) => { e.preventDefault(); openSettingsPanel().then(); }); @@ -2740,10 +2749,22 @@ document.getElementById("menu").addEventListener("click", (e) => { async function openSettingsPanel() { pause(true); - const optionsList = []; - for (const key in options) { + const actions :AsyncAlertAction<()=>void>[]= [{ + text: "Resume", + help: "Return to your run", + value() {}, + }, + { + text: "Starting perk", + help: "Try perks and levels you unlocked", + value() { + openUnlocksList(); + }, + }]; + + for (const key of Object.keys(options) as OptionId[] ) { if (options[key]) - optionsList.push({ + actions.push({ disabled: options[key].disabled(), icon: isSettingOn(key) ? icons["icon:checkmark_checked"] @@ -2758,46 +2779,28 @@ async function openSettingsPanel() { } const creativeModeThreshold = Math.max(...upgrades.map((u) => u.threshold)); - const cb = await asyncAlert<() => void>({ - title: "Breakout 71", - text: ` - `, - allowClose: true, - actions: [ - { - text: "Resume", - help: "Return to your run", - value() {}, - }, - { - text: "Starting perk", - help: "Try perks and levels you unlocked", - value() { - openUnlocksList(); - }, - }, - ...optionsList, - - (document.fullscreenEnabled || document.webkitFullscreenEnabled) && - (document.fullscreenElement !== null - ? { + if(document.fullscreenEnabled || document.webkitFullscreenEnabled){ + if(document.fullscreenElement !== null){ + actions.push( { text: "Exit Fullscreen", icon: icons["icon:exit_fullscreen"], help: "Might not work on some machines", value() { toggleFullScreen(); }, - } - : { + } ) + }else{ + actions.push({ icon: icons["icon:fullscreen"], text: "Fullscreen", help: "Might not work on some machines", value() { toggleFullScreen(); - }, - }), - - { + } + }) + } + } + actions.push({ text: "Creative mode", help: getTotalScore() < creativeModeThreshold @@ -2805,7 +2808,7 @@ async function openSettingsPanel() { : "Test runs with custom perks", disabled: getTotalScore() < creativeModeThreshold, async value() { - let creativeModePerks = {}, + let creativeModePerks :Partial<{ [id in PerkId]:number }>= {}, choice: "start" | Upgrade | void; while ( (choice = await asyncAlert<"start" | Upgrade>({ @@ -2839,9 +2842,8 @@ async function openSettingsPanel() { } } }, - }, - - { + }) + actions.push({ text: "Reset Game", help: "Erase high score and statistics", async value() { @@ -2865,8 +2867,14 @@ async function openSettingsPanel() { window.location.reload(); } }, - }, - ], + }) + + + const cb = await asyncAlert<() => void>({ + title: "Breakout 71", + text: ``, + allowClose: true, + actions , textAfterButtons: `

Made in France by Renan LE CARO. @@ -2982,7 +2990,7 @@ function repulse(a: Ball, b: BallLike, power: number, impactsBToo: boolean) { (((-power * (max - distance)) / (max * 1.2) / 3) * Math.min(500, levelTime)) / 500; - if (impactsBToo) { + if (impactsBToo && typeof b.vx !== 'undefined' && typeof b.vy !== 'undefined') { b.vx += dx * fact; b.vy += dy * fact; } @@ -3003,7 +3011,7 @@ function repulse(a: Ball, b: BallLike, power: number, impactsBToo: boolean) { vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand, vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand, }); - if (impactsBToo) { + if (impactsBToo&& typeof b.vx !== 'undefined' && typeof b.vy !== 'undefined') { flashes.push({ type: "particle", duration: 100, @@ -3019,7 +3027,7 @@ function repulse(a: Ball, b: BallLike, power: number, impactsBToo: boolean) { } } -function attract(a: Ball, b: BallLike, power: number) { +function attract(a: Ball, b: Ball, power: number) { const distance = distanceBetween(a, b); // Ensure we don't get soft locked const min = gameZoneWidth * 0.5; @@ -3063,7 +3071,7 @@ function attract(a: Ball, b: BallLike, power: number) { }); } -let mediaRecorder: MediaRecorder, +let mediaRecorder: MediaRecorder|null, captureStream: MediaStream, captureTrack: CanvasCaptureMediaStreamTrack, recordCanvas: HTMLCanvasElement, @@ -3137,7 +3145,7 @@ function startRecordingGame() { recordCanvas.height = gameZoneHeight; // drawMainCanvasOnSmallCanvas() - const recordedChunks = []; + const recordedChunks :Blob[]= []; const instance = new MediaRecorder(captureStream, { videoBitsPerSecond: 3500000, @@ -3150,7 +3158,7 @@ function startRecordingGame() { }; instance.onstop = async function () { - let targetDiv: HTMLElement; + let targetDiv: HTMLElement|null; let blob = new Blob(recordedChunks, { type: "video/webm" }); if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short @@ -3166,8 +3174,6 @@ function startRecordingGame() { video.disableRemotePlayback = true; video.width = recordCanvas.width; video.height = recordCanvas.height; - // targetDiv.style.width = recordCanvas.width + 'px' - // targetDiv.style.height = recordCanvas.height + 'px' video.loop = true; video.muted = true; video.playsInline = true; @@ -3251,13 +3257,13 @@ function toggleFullScreen() { } } -const pressed = { +const pressed :{[k:string]:number}= { ArrowLeft: 0, ArrowRight: 0, Shift: 0, }; -function setKeyPressed(key: string, on: 0 | 1) { +function setKeyPressed(key: string , on: 0 | 1) { pressed[key] = on; keyboardPuckSpeed = ((pressed.ArrowRight - pressed.ArrowLeft) * @@ -3317,7 +3323,7 @@ function sample(arr: T[]): T { } function getMajorityValue(arr: string[]): string { - const count = {}; + const count :{[k:string]:number}= {}; arr.forEach((v) => (count[v] = (count[v] || 0) + 1)); // Object.values inline polyfill const max = Math.max(...Object.keys(count).map((k) => count[k])); diff --git a/src/loadGameData.ts b/src/loadGameData.ts index 9da904a..1d101a8 100644 --- a/src/loadGameData.ts +++ b/src/loadGameData.ts @@ -69,7 +69,7 @@ function levelIconHTML( return `${levelName}`; } -export const icons = {}; +export const icons = {} as {[k:string]:string}; export const allLevels = rawLevelsList .map((level) => { @@ -88,7 +88,18 @@ export const allLevels = rawLevelsList svg, }; }) - .filter((l) => !l.name.startsWith("icon:")) as Level[]; + .filter((l) => !l.name.startsWith("icon:")) + .map((l,li)=>({ + ...l, + threshold:li < 8 + ? 0 + : Math.round( + Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li, + ), + sortKey:((Math.random() + 3) / 3.5) * l.bricks.filter((i) => i).length + })) as Level[]; + + export const upgrades = rawUpgrades.map((u) => ({ ...u, diff --git a/src/options.ts b/src/options.ts index 9faa608..0db09ee 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,6 +5,7 @@ export const options = { default: true, name: `Game sounds`, help: `Can slow down some phones.`, + afterChange:()=>{}, disabled: () => false, }, "mobile-mode": { @@ -20,35 +21,39 @@ export const options = { default: false, name: `Basic graphics`, help: `Better performance on older devices.`, + afterChange:()=>{}, disabled: () => false, }, pointerLock: { default: false, name: `Mouse pointer lock`, help: `Locks and hides the mouse cursor.`, + afterChange:()=>{}, disabled: () => !gameCanvas.requestPointerLock, }, easy: { default: false, name: `Kids mode`, help: `Start future runs with "slower ball".`, + afterChange:()=>{}, disabled: () => false, }, // Could not get the sharing to work without loading androidx and all the modern android things so for now i'll just disable sharing in the android app record: { default: false, name: `Record gameplay videos`, help: `Get a video of each level.`, + afterChange:()=>{}, disabled() { return window.location.search.includes("isInWebView=true"); }, }, -} as { [k: string]: OptionDef }; +} as const satisfies {[k:string]:OptionDef}; export type OptionDef = { default: boolean; name: string; help: string; disabled: () => boolean; - afterChange?: () => void; + afterChange: () => void; }; -export type OptionId = keyof typeof options; +export type OptionId = keyof typeof options ; diff --git a/src/types.d.ts b/src/types.d.ts index a15e0f9..331fc0a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -15,8 +15,8 @@ export type Level = { bricks: colorString[]; svg: string; color: string; - threshold?: number; - sortKey?: number; + threshold: number; + sortKey: number; }; export type Palette = { [k: string]: string }; @@ -69,8 +69,8 @@ export type Coin = { color: colorString; x: number; y: number; - previousx: number; - previousy: number; + previousX: number; + previousY: number; vx: number; vy: number; sx: number; @@ -83,40 +83,51 @@ export type Coin = { }; export type Ball = { x: number; - previousx: number; + previousX: number; y: number; - previousy: number; + previousY: number; vx: number; vy: number; + previousVX: number; + previousVY: number; sx: number; sy: number; sparks: number; piercedSinceBounce: number; hitSinceBounce: number; hitItem: { index: number; color: string }[]; - bouncesList?: { x: number; y: number }[]; + bouncesList: { x: number; y: number }[]; sapperUses: number; destroyed?: boolean; - previousvx?: number; - previousvy?: number; }; -export type FlashTypes = "text" | "particle" | "ball"; - -export type Flash = { - type: FlashTypes; - text?: string; - time: number; +interface BaseFlash { + time: number; color: colorString; - x: number; - y: number; duration: number; size: number; - vx?: number; - vy?: number; - ethereal?: boolean; destroyed?: boolean; -}; + x: number; + y: number; +} +interface ParticleFlash extends BaseFlash{ + type: 'particle'; + vx: number; + vy: number; + ethereal: boolean; +} + +interface TextFlash extends BaseFlash{ + type:'text'; + text: string; +} + +interface BallFlash extends BaseFlash{ + type:'ball'; +} + +export type Flash = ParticleFlash|TextFlash|BallFlash + export type RunStats = { started: number; @@ -133,9 +144,11 @@ export type RunStats = { max_level: number; }; -export type PerksMap = Partial<{ +export type PerksMap = { [k in PerkId]: number; -}>; +}; + + export type RunHistoryItem = RunStats & { perks?: PerksMap; diff --git a/tsconfig.json b/tsconfig.json index df426ab..44114a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es2015", "rootDir": "src", - "strict": false, + "strict": true, "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true