From 6850d3b65257832a8920adf10a3f012c65abb6a0 Mon Sep 17 00:00:00 2001 From: Renan LE CARO Date: Fri, 7 Mar 2025 11:34:11 +0100 Subject: [PATCH] Typed existing game.ts --- Readme.md | 68 +++++++- dist/index.html | 336 ++++++++++++++++++++---------------- src/game.ts | 422 ++++++++++++++++++++++----------------------- src/options.ts | 54 ++++++ src/rawUpgrades.ts | 4 +- src/types.d.ts | 192 +++++++++++++-------- 6 files changed, 632 insertions(+), 444 deletions(-) create mode 100644 src/options.ts diff --git a/Readme.md b/Readme.md index d391c31..cba2c97 100644 --- a/Readme.md +++ b/Readme.md @@ -86,8 +86,7 @@ play is intended or if it should even be allowed. - restart run on r - when missing, redo particle trail, but give speed to particle that matches ball direction -# Perks ideas -- Combo balancing : make Compound Interest less OP by defaulting to soft reset for others, or by making it loose more for each missed coin +# New perks ideas - second puck (symmeric to the first one) - keep combo between level, loose half your run score when missing any bricks - offer next level choice after upgrade pick @@ -130,6 +129,21 @@ play is intended or if it should even be allowed. - correction : pick one past upgrade to remove and replace by something else - puck bounce predictions rendered with particles or lines (requires big refactor) - the more balls are close to a brick, the more coins she spawns when breaking +- combo resets when puck moves +- gravity is flipped on the opposite side to the puck (for coins) +- balls have gravity +- coins don't have gravity + +# Balancing ideas + +The dominant strategy now is Compound Interest lvl 3 + coin magnet/viscosity/ +and hot start + explosives and multiball + +- make Compound Interest less OP making it full reset when coins lost +- cap hot start to lvl 2, or make it decrease faster +- make puck smaller as combo increases ? +- coin magnet has no effect when too close, or coins might overshoot, or coins bounce and spread more ? +- add red anti-coins, they destroy your combo and your score, and they behave like heavier coins. # extra levels @@ -156,3 +170,53 @@ and let them spend those coins on upgrades. The upgrades would then be explained "reach high score of 1000" or 'reach high score of 99999 without using "hot start"'. This requires recording a bit more info about each run. I could unlock the "pro stand" at $999 that just holds the play area higher. + + +# Colin's feedback (cwpute/obigre) + +Perks: + +* yoyo - when the ball falls back down, it curbs towards your puck (after hitting a brick or top) +* single block combo - get +1 combo if the ball only breaks a single block before reaching the puck +* mirror puck - a mirrored puck at the top of the screen follows as you move the bottom puck. it helps with keeping combos up and preventing the ball from touching the ceiling. it could appear as a hollow puck so as to not draw too much attention from the main bottom puck. +* side pucks - same as above but with two side pucks. +* ball coins - coins share the same physics as coins and bounce on walls and bricks +* phantom coins - coins pass through bricks +* drifting coins - coins slowly drift away from the brick they were generated from, and they need to be collected by the ball +* bigger ball - self-explanatory +* smaller ball - yes. +* sturdy ball - does more damage to bricks, to conter sturdy bricks +* accumulation - coins aglutinate into bigger coins that hold more value +* forgiving - you can miss several times without losing your combo. or alternatively, include this ability into the soft reset perk. +* plot - plot the ball's trajectory as you position your puck +* golden corners - catch coins at the sides of the puck to double their value +* varied diet - your combo grows if you keep hitting different coloured bricks each time +* earthquake - when the puck hits any side of the screen with velocity, the screen shakes and a brick explodes/falls from the level. alternatively, any brick you catch with the puck gives you the coins at the current combo rate. each level lowers the amount of hits before a brick falls +* statue - stand still to make the combo grow. move for too long and thi combo will quickly drop +* piggy bank - bricks absorb coins that fall onto it, and release them back as they are broken, with added value +* trickle up - if you first hit is the lowest brick of a column, all bricks above get +1 coin inside +* wormhole - the puck sometimes don't bounce the ball back up but teleports it to the top of the screen as if it fell through from bottom to top. higher levels reduce the times it takes to reload that effect +* hitman - hit the marked brick for +5 combo. each level increases the combo you get for it. +* sweet spot - place your puck directly below a moving spot at the top of the level to increase your combo + +IMPROVEMENTS ON EXISTING PERKS : + +* separate the "shoot straight" perk into two : one for left-side, the other for right-side. it will help alleviate the high difficulty of this challenge and provide more interesting ways to play around it. the wind perk could even find a use. +* wind perk is fun but very much unusable. i do not see any situation it can help with. i favor "puck control ball" anytime over it. maybe it blew less hard it could be played with. maybe reuse its mechanic as a level hazard. +* soft-landing is only interesting starting level 2, and only in synergy with "single-puck hit streak" +* instead of "lives", have the perk be like a fourth wall that prevents the ball from falling down but disapears after one strike. it is functionally the same but provides visual feedback to the player so they know they have that perk. +* limit levels to only a handful of coulours, like 5 max, so that the colour-related perks are more viable. + +GENERAL REMARKS ON DIFFERENT ASPECTS : + +* when the player reaches the last level, alow them to loop the run, unlocking a permanent bonus for this run. For example: +5 combo, +1 life per loop… the counterpart would be hazards that slowly populate the levels. +* different visual effects on ball to represent which perks it's imbued with (pierce, sapper…). remove visual while it's not affected (can't pierce/sap anymore until touching the puck). +* always visually put the ball on top of coins so as to clearly see it. sometimes a black outline appears to distinguesh it from coins, this should be used more often imo. +* not brick-shaped bricks, or tilted bricks, that can bounce the ball into fun angles to spice up the game. or even moving blocks ! +* reward the player with more choices/perks for breaking a brick while having reached an increasing combo thresholds. 5 combo, then 10, then 20, then 40 etc… once a threshold is reached you aren't rewarded for that threshold again until you start a rew run +* inspired by Balatro's score system : have some perks add to the multiplicator, and some others to the amount of coins in a brick (or the raw value of coins inside), so that you users want to improve both for maximized profit ! maybe tie one of the to perks that help you, and the other to perks that are bad to you, so that gambling players are forced to make their life harder +* the white outline on bricks asociated with picky eater kinda works but i feel it's more distracting than anything. maybe try something different ? put a cross on matching coloured bricks, or the contrary, grey out other bricks. +* also regarding colour : make it so the ball always start with a colour that matches one currently present in the level. sometimes you don't have white present and it's a waste of a combo :/ +* negative coins, they would spawn from bricks as a hazard and do any of the following: -deactivate a perk for this level -reduce your number of coins -reduce your choice for your next perk -despawn all current coins on screen -lowers your combo. they could either be a negative perk with a bonus, like the small puck, or a hazard that spawns in later levels. +* the way combos look on the puck was better when you didn't see the coin visual on it ! now it easily overflows out of the puck with reduced visibility + diff --git a/dist/index.html b/dist/index.html index 1691ef8..3ce5320 100644 --- a/dist/index.html +++ b/dist/index.html @@ -920,13 +920,21 @@ function hmrAccept(bundle /*: ParcelRequire */ , id /*: string */ ) { var _gameTs = require("./game.ts"); },{"./game.ts":"edeGs"}],"edeGs":[function(require,module,exports,__globalThis) { +var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); +parcelHelpers.defineInteropFlag(exports); +parcelHelpers.export(exports, "gameCanvas", ()=>gameCanvas); +parcelHelpers.export(exports, "fitSize", ()=>fitSize); +parcelHelpers.export(exports, "isSettingOn", ()=>isSettingOn); +parcelHelpers.export(exports, "toggleSetting", ()=>toggleSetting); var _loadGameData = require("./loadGameData"); +var _options = require("./options"); const MAX_COINS = 400; const MAX_PARTICLES = 600; -const canvas = document.getElementById("game"); -let ctx = canvas.getContext("2d", { +const gameCanvas = document.getElementById("game"); +let ctx = gameCanvas.getContext("2d", { alpha: false }); +const puckColor = "#FFF"; let ballSize = 20; const coinSize = Math.round(ballSize * 0.8); const puckHeight = ballSize; @@ -992,8 +1000,9 @@ let running = false, puck = 400, pauseTimeout = null; function play() { if (running) return; running = true; - if (audioContext) audioContext.resume(); + if (audioContext) audioContext.resume().then(); resumeRecording(); + document.body.className = running ? " running " : " paused "; } function pause(playerAskedForPause) { if (!running) return; @@ -1002,10 +1011,11 @@ function pause(playerAskedForPause) { running = false; needsRender = true; if (audioContext) setTimeout(()=>{ - if (!running) audioContext.suspend(); + if (!running) audioContext.suspend().then(); }, 1000); pauseRecording(); pauseTimeout = null; + document.body.className = running ? " running " : " paused "; }, Math.min(Math.max(0, pauseUsesDuringRun - 5) * 50, 500)); if (playerAskedForPause) // Pausing many times in a run will make pause slower pauseUsesDuringRun++; @@ -1018,19 +1028,19 @@ background.addEventListener("load", ()=>{ needsRender = true; }); const fitSize = ()=>{ - const { width, height } = canvas.getBoundingClientRect(); - canvas.width = width; - canvas.height = height; + const { width, height } = gameCanvas.getBoundingClientRect(); + gameCanvas.width = width; + gameCanvas.height = height; ctx.fillStyle = currentLevelInfo()?.color || "black"; ctx.globalAlpha = 1; ctx.fillRect(0, 0, width, height); backgroundCanvas.width = width; backgroundCanvas.height = height; gameZoneHeight = isSettingOn("mobile-mode") ? height * 80 / 100 : height; - const baseWidth = Math.round(Math.min(canvas.width, gameZoneHeight * 0.73)); + const baseWidth = Math.round(Math.min(gameCanvas.width, gameZoneHeight * 0.73)); brickWidth = Math.floor(baseWidth / gridSize / 2) * 2; gameZoneWidth = brickWidth * gridSize; - offsetX = Math.floor((canvas.width - gameZoneWidth) / 2); + offsetX = Math.floor((gameCanvas.width - gameZoneWidth) / 2); offsetXRoundedDown = offsetX; if (offsetX < ballSize) offsetXRoundedDown = 0; gameZoneWidthRoundedUp = width - 2 * offsetXRoundedDown; @@ -1096,7 +1106,7 @@ function addToScore(coin) { color: coin.color, x: coin.previousx, y: coin.previousy, - vx: (canvas.width - coin.x) / 100, + vx: (gameCanvas.width - coin.x) / 100, vy: -coin.y / 100, ethereal: true }); @@ -1254,7 +1264,7 @@ function currentLevelInfo() { function reset_perks() { for (let u of (0, _loadGameData.upgrades))perks[u.id] = 0; const giftable = getPossibleUpgrades().filter((u)=>u.giftable); - const randomGift = nextRunOverrides?.perk || isSettingOn("easy") ? "slow_down" : giftable[Math.floor(Math.random() * giftable.length)].id; + const randomGift = nextRunOverrides?.perk || isSettingOn("easy") && "slow_down" || giftable[Math.floor(Math.random() * giftable.length)].id; perks[randomGift] = 1; delete nextRunOverrides.perk; return randomGift; @@ -1328,34 +1338,34 @@ function setMousePos(x) { if (puck > offsetXRoundedDown + gameZoneWidthRoundedUp - puckWidth / 2) puck = offsetXRoundedDown + gameZoneWidthRoundedUp - puckWidth / 2; if (!running && !levelTime) putBallsAtPuck(); } -canvas.addEventListener("mouseup", (e)=>{ +gameCanvas.addEventListener("mouseup", (e)=>{ if (e.button !== 0) return; if (running) pause(true); else { play(); - if (isSettingOn("pointerLock")) canvas.requestPointerLock(); + if (isSettingOn("pointerLock")) gameCanvas.requestPointerLock(); } }); -canvas.addEventListener("mousemove", (e)=>{ - if (document.pointerLockElement === canvas) setMousePos(puck + e.movementX); +gameCanvas.addEventListener("mousemove", (e)=>{ + if (document.pointerLockElement === gameCanvas) setMousePos(puck + e.movementX); else setMousePos(e.x); }); -canvas.addEventListener("touchstart", (e)=>{ +gameCanvas.addEventListener("touchstart", (e)=>{ e.preventDefault(); if (!e.touches?.length) return; setMousePos(e.touches[0].pageX); play(); }); -canvas.addEventListener("touchend", (e)=>{ +gameCanvas.addEventListener("touchend", (e)=>{ e.preventDefault(); pause(true); }); -canvas.addEventListener("touchcancel", (e)=>{ +gameCanvas.addEventListener("touchcancel", (e)=>{ e.preventDefault(); pause(true); needsRender = true; }); -canvas.addEventListener("touchmove", (e)=>{ +gameCanvas.addEventListener("touchmove", (e)=>{ if (!e.touches?.length) return; setMousePos(e.touches[0].pageX); }); @@ -1376,36 +1386,58 @@ function shouldPierceByColor(vhit, hhit, chit) { if (typeof chit !== "undefined" && bricks[chit] !== ballsColor) return false; return true; } -function brickHitCheck(ballOrCoin, radius, isBall) { +function ballBrickHitCheck(ball) { + const radius = ballSize / 2; // Make ball/coin bonce, and return bricks that were hit - const { x, y, previousx, previousy } = ballOrCoin; + const { x, y, previousx, previousy } = ball; const vhit = hitsSomething(previousx, y, radius); const hhit = hitsSomething(x, previousy, radius); const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined; - let pierce = isBall && ballOrCoin.piercedSinceBounce < perks.pierce * 3; - if (pierce && (typeof vhit !== "undefined" || typeof hhit !== "undefined" || typeof chit !== "undefined")) ballOrCoin.piercedSinceBounce++; - if (isBall && shouldPierceByColor(vhit, hhit, chit)) pierce = true; + let pierce = ball.piercedSinceBounce < perks.pierce * 3; + if (pierce && (typeof vhit !== "undefined" || typeof hhit !== "undefined" || typeof chit !== "undefined")) ball.piercedSinceBounce++; + if (shouldPierceByColor(vhit, hhit, chit)) pierce = true; if (typeof vhit !== "undefined" || typeof chit !== "undefined") { if (!pierce) { - ballOrCoin.y = ballOrCoin.previousy; - ballOrCoin.vy *= -1; - } - if (!isBall) { - // Roll on corners - const leftHit = bricks[brickIndex(x - radius, y + radius)]; - const rightHit = bricks[brickIndex(x + radius, y + radius)]; - if (leftHit && !rightHit) ballOrCoin.vx += 1; - if (!leftHit && rightHit) ballOrCoin.vx -= 1; + ball.y = ball.previousy; + ball.vy *= -1; } } if (typeof hhit !== "undefined" || typeof chit !== "undefined") { if (!pierce) { - ballOrCoin.x = ballOrCoin.previousx; - ballOrCoin.vx *= -1; + ball.x = ball.previousx; + ball.vx *= -1; } } return vhit ?? hhit ?? chit; } +function coinBrickHitCheck(coin) { + // Make ball/coin bonce, and return bricks that were hit + const radius = coinSize / 2; + const { x, y, previousx, previousy } = coin; + const vhit = hitsSomething(previousx, y, radius); + const hhit = hitsSomething(x, previousy, radius); + const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined; + if (typeof vhit !== "undefined" || typeof chit !== "undefined") { + coin.y = coin.previousy; + coin.vy *= -1; + // Roll on corners + const leftHit = bricks[brickIndex(x - radius, y + radius)]; + const rightHit = bricks[brickIndex(x + radius, y + radius)]; + if (leftHit && !rightHit) { + coin.vx += 1; + coin.sa -= 1; + } + if (!leftHit && rightHit) { + coin.vx -= 1; + coin.sa += 1; + } + } + if (typeof hhit !== "undefined" || typeof chit !== "undefined") { + coin.x = coin.previousx; + coin.vx *= -1; + } + return vhit ?? hhit ?? chit; +} function bordersHitCheck(coin, radius, delta) { if (coin.destroyed) return; coin.previousx = coin.x; @@ -1430,8 +1462,8 @@ function bordersHitCheck(coin, radius, delta) { coin.vy *= -1; vhit = 1; } - if (coin.x > canvas.width - offsetXRoundedDown - radius) { - coin.x = canvas.width - offsetXRoundedDown - radius; + if (coin.x > gameCanvas.width - offsetXRoundedDown - radius) { + coin.x = gameCanvas.width - offsetXRoundedDown - radius; coin.vx *= -1; hhit = 1; } @@ -1483,11 +1515,11 @@ function tick() { const hitBorder = bordersHitCheck(coin, coinRadius, delta); if (coin.y > gameZoneHeight - coinRadius - puckHeight && coin.y < gameZoneHeight + puckHeight + coin.vy && Math.abs(coin.x - puck) < coinRadius + puckWidth / 2 + // a bit of margin to be nice puckHeight) addToScore(coin); - else if (coin.y > canvas.height + coinRadius) { + else if (coin.y > gameCanvas.height + coinRadius) { coin.destroyed = true; - if (perks.compound_interest) decreaseCombo(coin.points * perks.compound_interest, coin.x, canvas.height - coinRadius); + if (perks.compound_interest) resetCombo(coin.x, coin.y); } - const hitBrick = brickHitCheck(coin, coinRadius, false); + const hitBrick = coinBrickHitCheck(coin); if (perks.metamorphosis && typeof hitBrick !== "undefined") { if (bricks[hitBrick] && coin.color !== bricks[hitBrick] && bricks[hitBrick] !== "black" && !coin.coloredABrick) { bricks[hitBrick] = coin.color; @@ -1508,7 +1540,7 @@ function tick() { balls.forEach((ball)=>ballTick(ball, delta)); if (perks.wind) { const windD = (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * 2 * perks.wind; - for(var i = 0; i < perks.wind; i++)if (Math.random() * Math.abs(windD) > 0.5) flashes.push({ + for(let i = 0; i < perks.wind; i++)if (Math.random() * Math.abs(windD) > 0.5) flashes.push({ type: "particle", duration: 150, ethereal: true, @@ -1714,7 +1746,7 @@ function ballTick(ball, delta) { } else gameOver("Game Over", "You dropped the ball after catching " + score + " coins. "); } } - const hitBrick = brickHitCheck(ball, ballSize / 2, true); + const hitBrick = ballBrickHitCheck(ball); if (typeof hitBrick !== "undefined") { const initialBrickColor = bricks[hitBrick]; explodeBrick(hitBrick, ball, false); @@ -1832,28 +1864,32 @@ function gameOver(title, intro) {

${intro}

${unlocksInfo} `, - textAfterButtons: ` - -
- ${getHistograms(true)} + actions: [ + { + value: null, + text: 'Start a new run', + help: '' + } + ], + textAfterButtons: `
+ ${getHistograms()} ` }).then(()=>restart()); } -function getHistograms(saveStats) { +function getHistograms() { let runStats = ""; try { // Stores only top 100 runs let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]"); runsHistory.sort((a, b)=>a.score - b.score).reverse(); runsHistory = runsHistory.slice(0, 100); - const nonZeroPerks = {}; - for(let k in perks)if (perks[k]) nonZeroPerks[k] = perks[k]; runsHistory.push({ ...runStatistics, - perks: nonZeroPerks + perks, + appVersion: (0, _loadGameData.appVersion) }); // Generate some histogram - if (saveStats) localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory, null, 2)); + localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory, null, 2)); const makeHistogram = (title, getter, unit)=>{ let values = runsHistory.map((h)=>getter(h) || 0); let min = Math.min(...values); @@ -2015,7 +2051,7 @@ function explodeBrick(index, ball, isExplosion) { }); spawnExplosion(5 + Math.min(combo, 30), x, y, color, 150, coinSize / 2); } - if (!bricks[index]) ball.hitItem?.push({ + if (!bricks[index] && color !== 'black') ball.hitItem?.push({ index, color }); @@ -2028,7 +2064,7 @@ function render() { if (!needsRender) return; needsRender = false; const level = currentLevelInfo(); - const { width, height } = canvas; + const { width, height } = gameCanvas; if (!width || !height) return; let scoreInfo = ""; for(let i = 0; i < perks.extra_life; i++)scoreInfo += "\uD83D\uDDA4 "; @@ -2075,11 +2111,11 @@ function render() { if (level.svg && background.width && background.complete) { if (backgroundCanvas.title !== level.name) { backgroundCanvas.title = level.name; - backgroundCanvas.width = canvas.width; - backgroundCanvas.height = canvas.height; + backgroundCanvas.width = gameCanvas.width; + backgroundCanvas.height = gameCanvas.height; const bgctx = backgroundCanvas.getContext("2d"); bgctx.fillStyle = level.color || "#000"; - bgctx.fillRect(0, 0, canvas.width, canvas.height); + bgctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height); bgctx.fillStyle = ctx.createPattern(background, "repeat"); bgctx.fillRect(0, 0, width, height); } @@ -2110,7 +2146,7 @@ function render() { ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude); } ctx.globalCompositeOperation = "source-over"; - renderAllBricks(ctx); + renderAllBricks(); ctx.globalCompositeOperation = "screen"; flashes = flashes.filter((f)=>levelTime - f.time < f.duration && !f.destroyed); flashes.forEach((flash)=>{ @@ -2141,10 +2177,9 @@ function render() { } ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; - const puckColor = "#FFF"; balls.forEach((ball)=>{ + // The white border around is to distinguish colored balls from coins/bg drawBall(ctx, ballsColor, ballSize, ball.x, ball.y, puckColor); - // effect if (isTelekinesisActive(ball)) { ctx.strokeStyle = puckColor; ctx.beginPath(); @@ -2164,7 +2199,7 @@ function render() { const totalWidth = comboTextWidth + coinSize * 2; const left = puck - totalWidth / 2; if (totalWidth < puckWidth) { - drawCoin(ctx, "gold", coinSize, left + coinSize / 2, gameZoneHeight - puckHeight / 2, "#FFF", 0); + drawCoin(ctx, "gold", coinSize, left + coinSize / 2, gameZoneHeight - puckHeight / 2, puckColor, 0); drawText(ctx, comboText, "#000", puckHeight, left + coinSize * 1.5, gameZoneHeight - puckHeight / 2, true); } else drawText(ctx, comboText, "#FFF", puckHeight, puck, gameZoneHeight - puckHeight / 2, false); } @@ -2185,14 +2220,14 @@ function render() { ctx.fillStyle = redBottom ? "red" : puckColor; if (isSettingOn("mobile-mode")) { ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1); - if (!running) drawText(ctx, "Press and hold here to play", puckColor, puckHeight, canvas.width / 2, gameZoneHeight + (canvas.height - gameZoneHeight) / 2); + if (!running) drawText(ctx, "Press and hold here to play", puckColor, puckHeight, gameCanvas.width / 2, gameZoneHeight + (gameCanvas.height - gameZoneHeight) / 2); } else if (redBottom) ctx.fillRect(offsetXRoundedDown, gameZoneHeight - 1, gameZoneWidthRoundedUp, 1); if (shaked) ctx.resetTransform(); recordOneFrame(); } let cachedBricksRender = document.createElement("canvas"); let cachedBricksRenderKey = null; -function renderAllBricks(destinationCtx) { +function renderAllBricks() { ctx.globalAlpha = 1; const redBorderOnBricksWithWrongColor = combo > baseCombo() && perks.picky_eater; const newKey = gameZoneWidth + "_" + bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + ballsColor; @@ -2200,24 +2235,23 @@ function renderAllBricks(destinationCtx) { cachedBricksRenderKey = newKey; cachedBricksRender.width = gameZoneWidth; cachedBricksRender.height = gameZoneWidth + 1; - const ctx = cachedBricksRender.getContext("2d"); - ctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth); - ctx.resetTransform(); - ctx.translate(-offsetX, 0); + const canctx = cachedBricksRender.getContext("2d"); + canctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth); + canctx.resetTransform(); + canctx.translate(-offsetX, 0); // Bricks - const puckColor = "#FFF"; bricks.forEach((color, index)=>{ const x = brickCenterX(index), y = brickCenterY(index); if (!color) return; const borderColor = ballsColor === color && puckColor || color !== "black" && redBorderOnBricksWithWrongColor && "red" || color; - drawBrick(ctx, color, borderColor, x, y); + drawBrick(canctx, color, borderColor, x, y); if (color === "black") { - ctx.globalCompositeOperation = "source-over"; - drawIMG(ctx, bombSVG, brickWidth, x, y); + canctx.globalCompositeOperation = "source-over"; + drawIMG(canctx, bombSVG, brickWidth, x, y); } }); } - destinationCtx.drawImage(cachedBricksRender, offsetX, 0); + ctx.drawImage(cachedBricksRender, offsetX, 0); } let cachedGraphics = {}; function drawPuck(ctx, color, puckWidth, puckHeight, yoffset = 0) { @@ -2260,9 +2294,9 @@ function drawBall(ctx, color, width, x, y, borderColor = "") { ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2)); } const angles = 32; -function drawCoin(ctx, color, size, x, y, bg, rawAngle) { +function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) { const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles; - const key = "coin with halo_" + color + "_" + size + "_" + bg + "_" + (color === "gold" ? angle : "whatever"); + const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "gold" ? angle : "whatever"); if (!cachedGraphics[key]) { const can = document.createElement("canvas"); can.width = size; @@ -2274,7 +2308,7 @@ function drawCoin(ctx, color, size, x, y, bg, rawAngle) { canctx.fillStyle = color; canctx.fill(); if (color === "gold") { - canctx.strokeStyle = bg; + canctx.strokeStyle = borderColor; canctx.stroke(); canctx.beginPath(); canctx.arc(size / 2, size / 2, size / 2 * 0.6, 0, 2 * Math.PI); @@ -2546,9 +2580,6 @@ function createExplosionSound(pan = 0.5) { noiseSource.stop(context.currentTime + 1); } let levelTime = 0; -setInterval(()=>{ - document.body.className = running ? " running " : " paused "; -}, 100); window.addEventListener("visibilitychange", ()=>{ if (document.hidden) pause(true); }); @@ -2631,7 +2662,7 @@ function isSettingOn(key) { } catch (e) { console.warn(e); } - return cachedSettings[key] ?? options[key]?.default ?? false; + return cachedSettings[key] ?? (0, _options.options)[key]?.default ?? false; } function toggleSetting(key) { cachedSettings[key] = !isSettingOn(key); @@ -2641,93 +2672,50 @@ function toggleSetting(key) { } catch (e) { console.warn(e); } - if (options[key].afterChange) options[key].afterChange(); + if ((0, _options.options)[key].afterChange) (0, _options.options)[key].afterChange(); } -scoreDisplay.addEventListener("click", async (e)=>{ +scoreDisplay.addEventListener("click", (e)=>{ e.preventDefault(); - openScorePanel(); + openScorePanel().then(); }); async function openScorePanel() { pause(true); const cb = await asyncAlert({ title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`, text: ` -

Upgrades picked so far :

-

${pickedUpgradesHTMl()}

+

Upgrades picked so far :

+

${pickedUpgradesHTMl()}

`, allowClose: true, actions: [ { text: "Resume", - help: "Return to your run" + help: "Return to your run", + value: ()=>{} }, { text: "Restart", help: "Start a brand new run.", value: ()=>{ restart(); - return true; } } ] }); - if (cb) await cb(); + if (cb) cb(); } document.getElementById("menu").addEventListener("click", (e)=>{ e.preventDefault(); - openSettingsPanel(); + openSettingsPanel().then(); }); -const options = { - sound: { - default: true, - name: `Game sounds`, - help: `Can slow down some phones.`, - disabled: ()=>false - }, - "mobile-mode": { - default: window.innerHeight > window.innerWidth, - name: `Mobile mode`, - help: `Leaves space for your thumb.`, - afterChange () { - fitSize(); - }, - disabled: ()=>false - }, - basic: { - default: false, - name: `Basic graphics`, - help: `Better performance on older devices.`, - disabled: ()=>false - }, - pointerLock: { - default: false, - name: `Mouse pointer lock`, - help: `Locks and hides the mouse cursor.`, - disabled: ()=>!canvas.requestPointerLock - }, - easy: { - default: false, - name: `Kids mode`, - help: `Start future runs with "slower ball".`, - disabled: ()=>false - }, - record: { - default: false, - name: `Record gameplay videos`, - help: `Get a video of each level.`, - disabled () { - return window.location.search.includes("isInWebView=true"); - } - } -}; async function openSettingsPanel() { pause(true); const optionsList = []; - for(const key in options)if (options[key]) optionsList.push({ - disabled: options[key].disabled(), + for(const key in 0, _options.options)if ((0, _options.options)[key]) optionsList.push({ + disabled: (0, _options.options)[key].disabled(), icon: isSettingOn(key) ? (0, _loadGameData.icons)["icon:checkmark_checked"] : (0, _loadGameData.icons)["icon:checkmark_unchecked"], - text: options[key].name, - help: options[key].help, + text: (0, _options.options)[key].name, + help: (0, _options.options)[key].help, value: ()=>{ toggleSetting(key); openSettingsPanel(); @@ -2742,12 +2730,12 @@ async function openSettingsPanel() { { text: "Resume", help: "Return to your run", - async value () {} + value () {} }, { text: "Starting perk", help: "Try perks and levels you unlocked", - async value () { + value () { openUnlocksList(); } }, @@ -2819,7 +2807,7 @@ async function openUnlocksList() { }, icon })), - ...(0, _loadGameData.allLevels).sort((a, b)=>a.threshold - b.threshold).map((l, li)=>{ + ...(0, _loadGameData.allLevels).sort((a, b)=>a.threshold - b.threshold).map((l)=>{ const available = ts >= l.threshold; return { text: l.name, @@ -2956,18 +2944,18 @@ function attract(a, b, power) { vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand }); } -let mediaRecorder, captureStream, recordCanvas, recordCanvasCtx; +let mediaRecorder, captureStream, captureTrack, recordCanvas, recordCanvasCtx; function recordOneFrame() { if (!isSettingOn("record")) return; if (!running) return; if (!captureStream) return; drawMainCanvasOnSmallCanvas(); - if (captureStream.requestFrame) captureStream.requestFrame(); - else captureStream.getVideoTracks()[0].requestFrame(); + if (captureTrack?.requestFrame) captureTrack?.requestFrame(); + else if (captureStream?.requestFrame) captureStream.requestFrame(); } function drawMainCanvasOnSmallCanvas() { if (!recordCanvasCtx) return; - recordCanvasCtx.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height); + 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"; @@ -2980,13 +2968,14 @@ function drawMainCanvasOnSmallCanvas() { function startRecordingGame() { if (!isSettingOn("record")) return; if (!recordCanvas) { - // Smaller canvas with less details + // 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; @@ -3045,7 +3034,7 @@ function stopRecording() { mediaRecorder?.stop(); mediaRecorder = null; } -function captureFileName(ext) { +function captureFileName(ext = 'webm') { return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + "." + ext; } function findLast(arr, predicate) { @@ -3055,11 +3044,11 @@ function findLast(arr, predicate) { function toggleFullScreen() { try { if (document.fullscreenElement !== null) { - if (document.exitFullscreen) document.exitFullscreen(); + if (document.exitFullscreen) document.exitFullscreen().then(); else if (document.webkitCancelFullScreen) document.webkitCancelFullScreen(); } else { const docel = document.documentElement; - if (docel.requestFullscreen) docel.requestFullscreen(); + if (docel.requestFullscreen) docel.requestFullscreen().then(); else if (docel.webkitRequestFullscreen) docel.webkitRequestFullscreen(); } } catch (e) { @@ -3100,7 +3089,7 @@ fitSize(); restart(); tick(); -},{"./loadGameData":"l1B4x"}],"l1B4x":[function(require,module,exports,__globalThis) { +},{"./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); @@ -3389,8 +3378,8 @@ const rawUpgrades = [ id: "compound_interest", giftable: true, name: "Compound interest", - max: 3, - help: (lvl)=>`+${lvl} combo / brick broken, -${lvl} combo per coin lost`, + 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. @@ -3585,7 +3574,56 @@ exports.export = function(dest, destName, get) { }); }; -},{}]},["hhTAC","3qndx"], "3qndx", "parcelRequire94c2") +},{}],"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.`, + 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.`, + disabled: ()=>false + }, + pointerLock: { + default: false, + name: `Mouse pointer lock`, + help: `Locks and hides the mouse cursor.`, + disabled: ()=>!(0, _game.gameCanvas).requestPointerLock + }, + easy: { + default: false, + name: `Kids mode`, + help: `Start future runs with "slower ball".`, + disabled: ()=>false + }, + record: { + default: false, + name: `Record gameplay videos`, + help: `Get a video of each level.`, + 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/src/game.ts b/src/game.ts index 5c0a1d5..5a9ae74 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,11 +1,13 @@ import {allLevels, appVersion, icons, upgrades} from "./loadGameData"; -import {Ball, Coin, colorString, Flash, FlashTypes, Level, PerkId} from "./types"; +import {Ball, BallLike, Coin, colorString, Flash, FlashTypes, Level, PerkId, RunHistoryItem, RunStats} from "./types"; +import {OptionId, options} from "./options"; const MAX_COINS = 400; const MAX_PARTICLES = 600; -const canvas = document.getElementById("game") as HTMLCanvasElement; -let ctx = canvas.getContext("2d", {alpha: false}); +export const gameCanvas = document.getElementById("game") as HTMLCanvasElement; +let ctx = gameCanvas.getContext("2d", {alpha: false}); + const puckColor = "#FFF"; let ballSize = 20; const coinSize = Math.round(ballSize * 0.8); const puckHeight = ballSize; @@ -105,9 +107,10 @@ function play() { if (running) return; running = true; if (audioContext) { - audioContext.resume(); + audioContext.resume().then(); } resumeRecording(); + document.body.className = running ? " running " : " paused "; } function pause(playerAskedForPause: boolean) { @@ -120,11 +123,12 @@ function pause(playerAskedForPause: boolean) { needsRender = true; if (audioContext) { setTimeout(() => { - if (!running) audioContext.suspend(); + if (!running) audioContext.suspend().then(); }, 1000); } pauseRecording(); pauseTimeout = null; + document.body.className = running ? " running " : " paused "; }, Math.min(Math.max(0, pauseUsesDuringRun - 5) * 50, 500), ); @@ -153,10 +157,10 @@ background.addEventListener("load", () => { needsRender = true; }); -const fitSize = () => { - const {width, height} = canvas.getBoundingClientRect(); - canvas.width = width; - canvas.height = height; +export const fitSize = () => { + const {width, height} = gameCanvas.getBoundingClientRect(); + gameCanvas.width = width; + gameCanvas.height = height; ctx.fillStyle = currentLevelInfo()?.color || "black"; ctx.globalAlpha = 1; ctx.fillRect(0, 0, width, height); @@ -164,10 +168,10 @@ const fitSize = () => { backgroundCanvas.height = height; gameZoneHeight = isSettingOn("mobile-mode") ? (height * 80) / 100 : height; - const baseWidth = Math.round(Math.min(canvas.width, gameZoneHeight * 0.73)); + const baseWidth = Math.round(Math.min(gameCanvas.width, gameZoneHeight * 0.73)); brickWidth = Math.floor(baseWidth / gridSize / 2) * 2; gameZoneWidth = brickWidth * gridSize; - offsetX = Math.floor((canvas.width - gameZoneWidth) / 2); + offsetX = Math.floor((gameCanvas.width - gameZoneWidth) / 2); offsetXRoundedDown = offsetX; if (offsetX < ballSize) offsetXRoundedDown = 0; gameZoneWidthRoundedUp = width - 2 * offsetXRoundedDown; @@ -256,7 +260,7 @@ function addToScore(coin: Coin) { color: coin.color, x: coin.previousx, y: coin.previousy, - vx: (canvas.width - coin.x) / 100, + vx: (gameCanvas.width - coin.x) / 100, vy: -coin.y / 100, ethereal: true, }); @@ -402,7 +406,7 @@ async function openUpgradesPicker() { resetBalls(); } -function setLevel(l) { +function setLevel(l:number) { pause(false); if (l > 0) { openUpgradesPicker().then(); @@ -440,20 +444,20 @@ function currentLevelInfo() { return runLevels[currentLevel % runLevels.length]; } -function reset_perks() { +function reset_perks():PerkId { for (let u of upgrades) { perks[u.id] = 0; } const giftable = getPossibleUpgrades().filter((u) => u.giftable); const randomGift = - nextRunOverrides?.perk || isSettingOn("easy") - ? "slow_down" - : giftable[Math.floor(Math.random() * giftable.length)].id; + nextRunOverrides?.perk || + (isSettingOn("easy") && "slow_down" ) || giftable[Math.floor(Math.random() * giftable.length)].id; + perks[randomGift] = 1; delete nextRunOverrides.perk; - return randomGift; + return randomGift as PerkId; } let totalScoreAtRunStart = getTotalScore(); @@ -505,9 +509,9 @@ function getUpgraderUnlockPoints() { .sort((a, b) => a.threshold - b.threshold); } -let lastOffered = {}; +let lastOffered = {} as {[k in PerkId]:number}; -function dontOfferTooSoon(id) { +function dontOfferTooSoon(id:PerkId) { lastOffered[id] = Math.round(Date.now() / 1000); } @@ -555,7 +559,7 @@ function restart() { let keyboardPuckSpeed = 0; -function setMousePos(x) { +function setMousePos(x:number) { needsRender = true; puck = x; @@ -571,60 +575,60 @@ function setMousePos(x) { } } -canvas.addEventListener("mouseup", (e) => { +gameCanvas.addEventListener("mouseup", (e) => { if (e.button !== 0) return; if (running) { pause(true); } else { play(); if (isSettingOn("pointerLock")) { - canvas.requestPointerLock(); + gameCanvas.requestPointerLock(); } } }); -canvas.addEventListener("mousemove", (e) => { - if (document.pointerLockElement === canvas) { +gameCanvas.addEventListener("mousemove", (e) => { + if (document.pointerLockElement === gameCanvas) { setMousePos(puck + e.movementX); } else { setMousePos(e.x); } }); -canvas.addEventListener("touchstart", (e) => { +gameCanvas.addEventListener("touchstart", (e) => { e.preventDefault(); if (!e.touches?.length) return; setMousePos(e.touches[0].pageX); play(); }); -canvas.addEventListener("touchend", (e) => { +gameCanvas.addEventListener("touchend", (e) => { e.preventDefault(); pause(true); }); -canvas.addEventListener("touchcancel", (e) => { +gameCanvas.addEventListener("touchcancel", (e) => { e.preventDefault(); pause(true); needsRender = true; }); -canvas.addEventListener("touchmove", (e) => { +gameCanvas.addEventListener("touchmove", (e) => { if (!e.touches?.length) return; setMousePos(e.touches[0].pageX); }); let lastTick = performance.now(); -function brickIndex(x, y) { +function brickIndex(x:number, y:number) { return getRowColIndex( Math.floor(y / brickWidth), Math.floor((x - offsetX) / brickWidth), ); } -function hasBrick(index) { +function hasBrick(index:number):number|undefined { if (bricks[index]) return index; } -function hitsSomething(x, y, radius) { +function hitsSomething(x:number, y:number, radius:number) { return ( hasBrick(brickIndex(x - radius, y - radius)) ?? hasBrick(brickIndex(x + radius, y - radius)) ?? @@ -633,7 +637,7 @@ function hitsSomething(x, y, radius) { ); } -function shouldPierceByColor(vhit, hhit, chit) { +function shouldPierceByColor(vhit:number|undefined, hhit:number|undefined, chit:number|undefined) { if (!perks.pierce_color) return false; if (typeof vhit !== "undefined" && bricks[vhit] !== ballsColor) { return false; @@ -647,9 +651,10 @@ function shouldPierceByColor(vhit, hhit, chit) { return true; } -function brickHitCheck(ballOrCoin, radius, isBall) { +function ballBrickHitCheck(ball:Ball) { + const radius=ballSize / 2 // Make ball/coin bonce, and return bricks that were hit - const {x, y, previousx, previousy} = ballOrCoin; + const {x, y, previousx, previousy} = ball; const vhit = hitsSomething(previousx, y, radius); const hhit = hitsSomething(x, previousy, radius); @@ -659,49 +664,75 @@ function brickHitCheck(ballOrCoin, radius, isBall) { hitsSomething(x, y, radius)) || undefined; - let pierce = isBall && ballOrCoin.piercedSinceBounce < perks.pierce * 3; + let pierce = ball.piercedSinceBounce < perks.pierce * 3; if ( pierce && (typeof vhit !== "undefined" || typeof hhit !== "undefined" || typeof chit !== "undefined") ) { - ballOrCoin.piercedSinceBounce++; + ball.piercedSinceBounce++; } - if (isBall && shouldPierceByColor(vhit, hhit, chit)) { + if ( shouldPierceByColor(vhit, hhit, chit)) { pierce = true; } if (typeof vhit !== "undefined" || typeof chit !== "undefined") { if (!pierce) { - ballOrCoin.y = ballOrCoin.previousy; - ballOrCoin.vy *= -1; + ball.y = ball.previousy; + ball.vy *= -1; } - if (!isBall) { - // Roll on corners - const leftHit = bricks[brickIndex(x - radius, y + radius)]; - const rightHit = bricks[brickIndex(x + radius, y + radius)]; - - if (leftHit && !rightHit) { - ballOrCoin.vx += 1; - } - if (!leftHit && rightHit) { - ballOrCoin.vx -= 1; - } - } } if (typeof hhit !== "undefined" || typeof chit !== "undefined") { if (!pierce) { - ballOrCoin.x = ballOrCoin.previousx; - ballOrCoin.vx *= -1; + ball.x = ball.previousx; + ball.vx *= -1; } } return vhit ?? hhit ?? chit; } +function coinBrickHitCheck(coin:Coin) { -function bordersHitCheck(coin, radius, delta) { + // Make ball/coin bonce, and return bricks that were hit + const radius=coinSize/2 + const {x, y, previousx, previousy} = coin; + + const vhit = hitsSomething(previousx, y, radius); + const hhit = hitsSomething(x, previousy, radius); + const chit = + (typeof vhit == "undefined" && + typeof hhit == "undefined" && + hitsSomething(x, y, radius)) || + undefined; + + if (typeof vhit !== "undefined" || typeof chit !== "undefined") { + coin.y = coin.previousy; + coin.vy *= -1; + + // Roll on corners + const leftHit = bricks[brickIndex(x - radius, y + radius)]; + const rightHit = bricks[brickIndex(x + radius, y + radius)]; + + if (leftHit && !rightHit) { + coin.vx += 1; + coin.sa -= 1; + + } + if (!leftHit && rightHit) { + coin.vx -= 1; + coin.sa += 1; + } + } + if (typeof hhit !== "undefined" || typeof chit !== "undefined") { + coin.x = coin.previousx; + coin.vx *= -1; + } + return vhit ?? hhit ?? chit; +} + +function bordersHitCheck(coin:Coin|Ball, radius:number, delta:number) { if (coin.destroyed) return; coin.previousx = coin.x; coin.previousy = coin.y; @@ -734,8 +765,8 @@ function bordersHitCheck(coin, radius, delta) { coin.vy *= -1; vhit = 1; } - if (coin.x > canvas.width - offsetXRoundedDown - radius) { - coin.x = canvas.width - offsetXRoundedDown - radius; + if (coin.x > gameCanvas.width - offsetXRoundedDown - radius) { + coin.x = gameCanvas.width - offsetXRoundedDown - radius; coin.vx *= -1; hhit = 1; } @@ -832,18 +863,16 @@ function tick() { puckHeight ) { addToScore(coin); - } else if (coin.y > canvas.height + coinRadius) { + } else if (coin.y > gameCanvas.height + coinRadius) { coin.destroyed = true; if (perks.compound_interest) { - decreaseCombo( - coin.points * perks.compound_interest, - coin.x, - canvas.height - coinRadius, + resetCombo( + coin.x,coin.y ); } } - const hitBrick = brickHitCheck(coin, coinRadius, false); + const hitBrick = coinBrickHitCheck(coin); if (perks.metamorphosis && typeof hitBrick !== "undefined") { if ( @@ -878,7 +907,7 @@ function tick() { ((puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth) * 2 * perks.wind; - for (var i = 0; i < perks.wind; i++) { + for (let i = 0; i < perks.wind; i++) { if (Math.random() * Math.abs(windD) > 0.5) { flashes.push({ type: "particle", @@ -980,11 +1009,11 @@ function tick() { lastTick = currentTick; } -function isTelekinesisActive(ball) { +function isTelekinesisActive(ball:Ball) { return perks.telekinesis && !ball.hitSinceBounce && ball.vy < 0; } -function ballTick(ball, delta) { +function ballTick(ball:Ball, delta:number) { ball.previousvx = ball.vx; ball.previousvy = ball.vy; @@ -1156,7 +1185,7 @@ function ballTick(ball, delta) { } } } - const hitBrick = brickHitCheck(ball, ballSize / 2, true); + const hitBrick = ballBrickHitCheck(ball); if (typeof hitBrick !== "undefined") { const initialBrickColor = bricks[hitBrick]; @@ -1204,7 +1233,7 @@ const defaultRunStats = () => ({ upgrades_picked: 1, max_combo: 1, max_level: 0, -}); +}) as RunStats; let runStatistics = defaultRunStats(); function resetRunStatistics() { @@ -1219,7 +1248,7 @@ function getTotalScore() { } } -function addToTotalScore(points) { +function addToTotalScore(points:number) { try { localStorage.setItem( "breakout_71_total_score", @@ -1229,7 +1258,7 @@ function addToTotalScore(points) { } } -function addToTotalPlayTime(ms) { +function addToTotalPlayTime(ms:number) { try { localStorage.setItem( "breakout_71_total_play_time", @@ -1242,7 +1271,7 @@ function addToTotalPlayTime(ms) { } } -function gameOver(title, intro) { +function gameOver(title:string, intro:string) { if (!running) return; pause(true); stopRecording(); @@ -1300,6 +1329,7 @@ function gameOver(title, intro) { // Avoid the sad sound right as we restart a new games combo = 1; + asyncAlert({ allowClose: true, title, @@ -1307,41 +1337,38 @@ function gameOver(title, intro) {

${intro}

${unlocksInfo} `, - textAfterButtons: ` - -
- ${getHistograms(true)} + actions:[{ + value:null, + text:'Start a new run', + help:'', + }], + textAfterButtons: `
+ ${getHistograms()} `, }).then(() => restart()); + } -function getHistograms(saveStats) { +function getHistograms() { let runStats = ""; try { // Stores only top 100 runs let runsHistory = JSON.parse( localStorage.getItem("breakout_71_runs_history") || "[]", - ); + ) as RunHistoryItem[]; runsHistory.sort((a, b) => a.score - b.score).reverse(); runsHistory = runsHistory.slice(0, 100); - const nonZeroPerks = {}; - for (let k in perks) { - if (perks[k]) { - nonZeroPerks[k] = perks[k]; - } - } - - runsHistory.push({...runStatistics, perks: nonZeroPerks}); + runsHistory.push({...runStatistics, perks,appVersion}); // Generate some histogram - if (saveStats) { - localStorage.setItem( - "breakout_71_runs_history", - JSON.stringify(runsHistory, null, 2), - ); - } - const makeHistogram = (title, getter, unit) => { + + localStorage.setItem( + "breakout_71_runs_history", + JSON.stringify(runsHistory, null, 2), + ); + + const makeHistogram = (title:string, getter: (hi:RunHistoryItem)=>number, unit:string) => { let values = runsHistory.map((h) => getter(h) || 0); let min = Math.min(...values); let max = Math.max(...values); @@ -1362,7 +1389,7 @@ function getHistograms(saveStats) { binsTotal.push(0); } const binSize = (max - min) / bins.length; - const binIndexOf = (v) => + const binIndexOf = (v:number) => Math.min(bins.length - 1, Math.floor((v - min) / binSize)); values.forEach((v) => { if (isNaN(v)) return; @@ -1431,7 +1458,7 @@ function getHistograms(saveStats) { return runStats; } -function explodeBrick(index, ball, isExplosion) { +function explodeBrick(index:number, ball:Ball, isExplosion:boolean) { const color = bricks[index]; if (!color) return; @@ -1586,7 +1613,7 @@ function explodeBrick(index, ball, isExplosion) { spawnExplosion(5 + Math.min(combo, 30), x, y, color, 150, coinSize / 2); } - if (!bricks[index]) { + if (!bricks[index] && color!=='black') { ball.hitItem?.push({ index, color, @@ -1606,7 +1633,7 @@ function render() { needsRender = false; const level = currentLevelInfo(); - const {width, height} = canvas; + const {width, height} = gameCanvas; if (!width || !height) return; let scoreInfo = ""; @@ -1665,13 +1692,13 @@ function render() { if (level.svg && background.width && background.complete) { if (backgroundCanvas.title !== level.name) { backgroundCanvas.title = level.name; - backgroundCanvas.width = canvas.width; - backgroundCanvas.height = canvas.height; + backgroundCanvas.width = gameCanvas.width; + backgroundCanvas.height = gameCanvas.height; const bgctx = backgroundCanvas.getContext( "2d", ) as CanvasRenderingContext2D; bgctx.fillStyle = level.color || "#000"; - bgctx.fillRect(0, 0, canvas.width, canvas.height); + bgctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height); bgctx.fillStyle = ctx.createPattern(background, "repeat"); bgctx.fillRect(0, 0, width, height); } @@ -1711,7 +1738,7 @@ function render() { } ctx.globalCompositeOperation = "source-over"; - renderAllBricks(ctx); + renderAllBricks(); ctx.globalCompositeOperation = "screen"; flashes = flashes.filter( @@ -1758,10 +1785,10 @@ function render() { ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; - const puckColor = "#FFF"; balls.forEach((ball) => { + // The white border around is to distinguish colored balls from coins/bg drawBall(ctx, ballsColor, ballSize, ball.x, ball.y, puckColor); - // effect + if (isTelekinesisActive(ball)) { ctx.strokeStyle = puckColor; ctx.beginPath(); @@ -1790,7 +1817,7 @@ function render() { coinSize, left + coinSize / 2, gameZoneHeight - puckHeight / 2, - "#FFF", + puckColor, 0, ); drawText( @@ -1839,8 +1866,8 @@ function render() { "Press and hold here to play", puckColor, puckHeight, - canvas.width / 2, - gameZoneHeight + (canvas.height - gameZoneHeight) / 2, + gameCanvas.width / 2, + gameZoneHeight + (gameCanvas.height - gameZoneHeight) / 2, ); } } else if (redBottom) { @@ -1862,7 +1889,7 @@ function render() { let cachedBricksRender = document.createElement("canvas"); let cachedBricksRenderKey = null; -function renderAllBricks(destinationCtx) { +function renderAllBricks() { ctx.globalAlpha = 1; const redBorderOnBricksWithWrongColor = @@ -1882,12 +1909,11 @@ function renderAllBricks(destinationCtx) { cachedBricksRender.width = gameZoneWidth; cachedBricksRender.height = gameZoneWidth + 1; - const ctx = cachedBricksRender.getContext("2d") as CanvasRenderingContext2D; - ctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth); - ctx.resetTransform(); - ctx.translate(-offsetX, 0); + const canctx = cachedBricksRender.getContext("2d") as CanvasRenderingContext2D; + canctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth); + canctx.resetTransform(); + canctx.translate(-offsetX, 0); // Bricks - const puckColor = "#FFF"; bricks.forEach((color, index) => { const x = brickCenterX(index), y = brickCenterY(index); @@ -1897,20 +1923,21 @@ function renderAllBricks(destinationCtx) { (ballsColor === color && puckColor) || (color !== "black" && redBorderOnBricksWithWrongColor && "red") || color; - drawBrick(ctx, color, borderColor, x, y); + drawBrick(canctx, color, borderColor, x, y); if (color === "black") { - ctx.globalCompositeOperation = "source-over"; - drawIMG(ctx, bombSVG, brickWidth, x, y); + canctx.globalCompositeOperation = "source-over"; + drawIMG(canctx, bombSVG, brickWidth, x, y); } }); } - destinationCtx.drawImage(cachedBricksRender, offsetX, 0); + ctx.drawImage(cachedBricksRender, offsetX, 0); } let cachedGraphics = {}; -function drawPuck(ctx, color, puckWidth, puckHeight, yoffset = 0) { +function drawPuck(ctx:CanvasRenderingContext2D, color:colorString, + puckWidth:number, puckHeight:number, yoffset = 0) { const key = "puck" + color + "_" + puckWidth + "_" + puckHeight; if (!cachedGraphics[key]) { @@ -1943,7 +1970,8 @@ function drawPuck(ctx, color, puckWidth, puckHeight, yoffset = 0) { ); } -function drawBall(ctx, color, width, x, y, borderColor = "") { +function drawBall(ctx:CanvasRenderingContext2D, + color:colorString, width:number, x:number, y:number, borderColor = "") { const key = "ball" + color + "_" + width + "_" + borderColor; const size = Math.round(width); @@ -1974,7 +2002,8 @@ function drawBall(ctx, color, width, x, y, borderColor = "") { const angles = 32; -function drawCoin(ctx, color, size, x, y, bg, rawAngle) { +function drawCoin(ctx:CanvasRenderingContext2D, color:colorString, size:number, + x:number, y:number, borderColor:colorString, rawAngle:number) { const angle = ((Math.round((rawAngle / Math.PI) * 2 * angles) % angles) + angles) % angles; @@ -1985,7 +2014,7 @@ function drawCoin(ctx, color, size, x, y, bg, rawAngle) { "_" + size + "_" + - bg + + borderColor + "_" + (color === "gold" ? angle : "whatever"); @@ -2003,7 +2032,7 @@ function drawCoin(ctx, color, size, x, y, bg, rawAngle) { canctx.fill(); if (color === "gold") { - canctx.strokeStyle = bg; + canctx.strokeStyle = borderColor; canctx.stroke(); canctx.beginPath(); @@ -2028,7 +2057,8 @@ function drawCoin(ctx, color, size, x, y, bg, rawAngle) { ); } -function drawFuzzyBall(ctx, color, width, x, y) { +function drawFuzzyBall(ctx:CanvasRenderingContext2D, color:colorString, width:number, + x:number, y:number) { const key = "fuzzy-circle" + color + "_" + width; if (!color) debugger; const size = Math.round(width * 3); @@ -2059,7 +2089,8 @@ function drawFuzzyBall(ctx, color, width, x, y) { ); } -function drawBrick(ctx, color, borderColor, x, y) { +function drawBrick(ctx:CanvasRenderingContext2D, color:colorString, borderColor:colorString, + x:number, y:number) { const tlx = Math.ceil(x - brickWidth / 2); const tly = Math.ceil(y - brickWidth / 2); const brx = Math.ceil(x + brickWidth / 2) - 1; @@ -2098,7 +2129,7 @@ function drawBrick(ctx, color, borderColor, x, y) { // It's not easy to have a 1px gap between bricks without antialiasing } -function roundRect(ctx, x, y, width, height, radius) { +function roundRect(ctx:CanvasRenderingContext2D, x:number, y:number, width:number, height:number, radius:number) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); @@ -2112,12 +2143,12 @@ function roundRect(ctx, x, y, width, height, radius) { ctx.closePath(); } -function drawRedSquare(ctx, x, y, width, height) { +function drawRedSquare(ctx:CanvasRenderingContext2D, x:number, y:number, width:number, height:number) { ctx.fillStyle = "red"; ctx.fillRect(x, y, width, height); } -function drawIMG(ctx, img, size, x, y) { +function drawIMG(ctx:CanvasRenderingContext2D, img:HTMLImageElement, size:number, x:number, y:number) { const key = "svg" + img + "_" + size + "_" + img.complete; if (!cachedGraphics[key]) { @@ -2141,7 +2172,8 @@ function drawIMG(ctx, img, size, x, y) { ); } -function drawText(ctx, text, color, fontSize, x, y, left = false) { +function drawText(ctx:CanvasRenderingContext2D, + text:string, color:colorString, fontSize:number, x:number, y:number, left = false) { const key = "text" + text + "_" + color + "_" + fontSize + "_" + left; if (!cachedGraphics[key]) { @@ -2165,14 +2197,14 @@ function drawText(ctx, text, color, fontSize, x, y, left = false) { ); } -function pixelsToPan(pan) { +function pixelsToPan(pan:number) { return (pan - offsetX) / gameZoneWidth; } let lastComboPlayed = NaN, shepard = 6; -function playShepard(delta, pan, volume) { +function playShepard(delta:number, pan:number, volume:number) { const shepardMax = 11, factor = 1.05945594920268, baseNote = 392; @@ -2180,7 +2212,7 @@ function playShepard(delta, pan, volume) { if (shepard > shepardMax) shepard = 0; if (shepard < 0) shepard = shepardMax; - const play = (note) => { + const play = (note:number) => { const freq = baseNote * Math.pow(factor, note); const diff = Math.abs(note - shepardMax * 0.5); const maxDistanceToIdeal = 1.5 * shepardMax; @@ -2195,12 +2227,12 @@ function playShepard(delta, pan, volume) { } const sounds = { - wallBeep: (pan) => { + wallBeep: (pan:number) => { if (!isSettingOn("sound")) return; createSingleBounceSound(800, pixelsToPan(pan)); }, - comboIncreaseMaybe: (x, volume) => { + comboIncreaseMaybe: (x:number, volume:number) => { if (!isSettingOn("sound")) return; let delta = 0; if (!isNaN(lastComboPlayed)) { @@ -2215,11 +2247,11 @@ const sounds = { if (!isSettingOn("sound")) return; playShepard(-1, 0.5, 0.5); }, - coinBounce: (pan, volume) => { + coinBounce: (pan:number, volume:number) => { if (!isSettingOn("sound")) return; createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle"); }, - explode: (pan) => { + explode: (pan:number) => { if (!isSettingOn("sound")) return; createExplosionSound(pixelsToPan(pan)); }, @@ -2227,14 +2259,14 @@ const sounds = { if (!isSettingOn("sound")) return; createRevivalSound(500); }, - coinCatch(pan) { + coinCatch(pan:number) { if (!isSettingOn("sound")) return; createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle"); }, }; // How to play the code on the leftconst context = new window.AudioContext(); -let audioContext, audioRecordingTrack; +let audioContext:AudioContext, audioRecordingTrack:MediaStreamAudioDestinationNode; function getAudioContext() { if (!audioContext) { @@ -2249,7 +2281,7 @@ function createSingleBounceSound( pan = 0.5, volume = 1, duration = 0.1, - type = "sine", + type:OscillatorType = "sine", ) { const context = getAudioContext(); // Frequency for the metal "ping" @@ -2326,7 +2358,7 @@ function createRevivalSound(baseFreq = 440) { oscillators.forEach((osc) => osc.stop(context.currentTime + 2)); } -let noiseBuffer; +let noiseBuffer:AudioBuffer; function createExplosionSound(pan = 0.5) { const context = getAudioContext(); @@ -2381,9 +2413,6 @@ function createExplosionSound(pan = 0.5) { let levelTime = 0; -setInterval(() => { - document.body.className = running ? " running " : " paused "; -}, 100); window.addEventListener("visibilitychange", () => { if (document.hidden) { @@ -2506,7 +2535,7 @@ ${icon} // Settings let cachedSettings = {}; -function isSettingOn(key) { +export function isSettingOn(key: OptionId) { if (typeof cachedSettings[key] == "undefined") { try { cachedSettings[key] = JSON.parse( @@ -2519,7 +2548,7 @@ function isSettingOn(key) { return cachedSettings[key] ?? options[key]?.default ?? false; } -function toggleSetting(key) { +export function toggleSetting(key:OptionId) { cachedSettings[key] = !isSettingOn(key); try { const lskey = "breakout-settings-enable-" + key; @@ -2530,9 +2559,10 @@ function toggleSetting(key) { if (options[key].afterChange) options[key].afterChange(); } -scoreDisplay.addEventListener("click", async (e) => { + +scoreDisplay.addEventListener("click", (e) => { e.preventDefault(); - openScorePanel(); + openScorePanel().then(); }); async function openScorePanel() { @@ -2540,78 +2570,35 @@ async function openScorePanel() { const cb = await asyncAlert({ title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`, text: ` -

Upgrades picked so far :

-

${pickedUpgradesHTMl()}

+

Upgrades picked so far :

+

${pickedUpgradesHTMl()}

`, allowClose: true, actions: [ { text: "Resume", help: "Return to your run", + value:()=>{} }, { text: "Restart", help: "Start a brand new run.", value: () => { restart(); - return true; }, }, ], }); if (cb) { - await cb(); + cb(); } } document.getElementById("menu").addEventListener("click", (e) => { e.preventDefault(); - openSettingsPanel(); + openSettingsPanel().then(); }); -const options = { - sound: { - default: true, - name: `Game sounds`, - help: `Can slow down some phones.`, - disabled: () => false, - }, - "mobile-mode": { - default: window.innerHeight > window.innerWidth, - name: `Mobile mode`, - help: `Leaves space for your thumb.`, - afterChange() { - fitSize(); - }, - disabled: () => false, - }, - basic: { - default: false, - name: `Basic graphics`, - help: `Better performance on older devices.`, - disabled: () => false, - }, - pointerLock: { - default: false, - name: `Mouse pointer lock`, - help: `Locks and hides the mouse cursor.`, - disabled: () => !canvas.requestPointerLock, - }, - easy: { - default: false, - name: `Kids mode`, - help: `Start future runs with "slower ball".`, - 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.`, - disabled() { - return window.location.search.includes("isInWebView=true"); - }, - }, -}; async function openSettingsPanel() { pause(true); @@ -2642,13 +2629,13 @@ async function openSettingsPanel() { { text: "Resume", help: "Return to your run", - async value() { + value() { }, }, { text: "Starting perk", help: "Try perks and levels you unlocked", - async value() { + value() { openUnlocksList() }, }, @@ -2736,7 +2723,7 @@ async function openUnlocksList() { })), ...allLevels .sort((a, b) => a.threshold - b.threshold) - .map((l, li) => { + .map((l) => { const available = ts >= l.threshold; return { text: l.name, @@ -2787,19 +2774,19 @@ Click an item above to start a run with it. } } -function distance2(a, b) { +function distance2(a:{x:number,y:number}, b:{x:number,y:number}) { return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2); } -function distanceBetween(a, b) { +function distanceBetween(a:{x:number,y:number}, b:{x:number,y:number}) { return Math.sqrt(distance2(a, b)); } -function rainbowColor() { +function rainbowColor():colorString { return `hsl(${(Math.round(levelTime / 4) * 2) % 360},100%,70%)`; } -function repulse(a, b, power, impactsBToo) { +function repulse(a:Ball, b:BallLike, power:number, impactsBToo:boolean) { const distance = distanceBetween(a, b); // Ensure we don't get soft locked const max = gameZoneWidth / 2; @@ -2848,7 +2835,7 @@ function repulse(a, b, power, impactsBToo) { } } -function attract(a, b, power) { +function attract(a:Ball, b:BallLike, power:number) { const distance = distanceBetween(a, b); // Ensure we don't get soft locked const min = gameZoneWidth * 0.5; @@ -2892,7 +2879,11 @@ function attract(a, b, power) { }); } -let mediaRecorder, captureStream, recordCanvas, recordCanvasCtx; +let mediaRecorder:MediaRecorder, + captureStream:MediaStream, + captureTrack:CanvasCaptureMediaStreamTrack, + recordCanvas:HTMLCanvasElement, + recordCanvasCtx:CanvasRenderingContext2D; function recordOneFrame() { if (!isSettingOn("record")) { @@ -2901,17 +2892,18 @@ function recordOneFrame() { if (!running) return; if (!captureStream) return; drawMainCanvasOnSmallCanvas(); - if (captureStream.requestFrame) { + if (captureTrack?.requestFrame) { + captureTrack?.requestFrame(); + }else if(captureStream?.requestFrame){ captureStream.requestFrame(); - } else { - captureStream.getVideoTracks()[0].requestFrame(); + } } function drawMainCanvasOnSmallCanvas() { if (!recordCanvasCtx) return; recordCanvasCtx.drawImage( - canvas, + gameCanvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, @@ -2942,18 +2934,18 @@ function startRecordingGame() { return; } if (!recordCanvas) { - // Smaller canvas with less details + // Smaller canvas with fewer details recordCanvas = document.createElement("canvas"); recordCanvasCtx = recordCanvas.getContext("2d", { antialias: false, alpha: false, - }); + }) as CanvasRenderingContext2D; - captureStream = recordCanvas.captureStream(0); + captureStream = recordCanvas.captureStream(0) ; + captureTrack = captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) { captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]); - // captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[1]) } } @@ -2974,7 +2966,7 @@ function startRecordingGame() { }; instance.onstop = async function () { - let targetDiv; + let targetDiv:HTMLElement; let blob = new Blob(recordedChunks, {type: "video/webm"}); if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short @@ -3034,7 +3026,7 @@ function stopRecording() { mediaRecorder = null; } -function captureFileName(ext) { +function captureFileName(ext='webm') { return ( "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + @@ -3043,7 +3035,7 @@ function captureFileName(ext) { ); } -function findLast(arr, predicate) { +function findLast(arr:T[], predicate:(item:T,index:number,array:T[])=>boolean) { let i = arr.length; while (--i) if (predicate(arr[i], i, arr)) { @@ -3055,14 +3047,14 @@ function toggleFullScreen() { try { if (document.fullscreenElement !== null) { if (document.exitFullscreen) { - document.exitFullscreen(); + document.exitFullscreen().then(); } else if (document.webkitCancelFullScreen) { document.webkitCancelFullScreen(); } } else { const docel = document.documentElement; if (docel.requestFullscreen) { - docel.requestFullscreen(); + docel.requestFullscreen().then(); } else if (docel.webkitRequestFullscreen) { docel.webkitRequestFullscreen(); } @@ -3078,7 +3070,7 @@ const pressed = { Shift: 0, }; -function setKeyPressed(key, on) { +function setKeyPressed(key:string, on:0|1) { pressed[key] = on; keyboardPuckSpeed = ((pressed.ArrowRight - pressed.ArrowLeft) * diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..413f038 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,54 @@ +import {fitSize, gameCanvas} from "./game"; + +export const options = { + sound: { + default: true, + name: `Game sounds`, + help: `Can slow down some phones.`, + disabled: () => false, + }, + "mobile-mode": { + default: window.innerHeight > window.innerWidth, + name: `Mobile mode`, + help: `Leaves space for your thumb.`, + afterChange() { + fitSize(); + }, + disabled: () => false, + }, + basic: { + default: false, + name: `Basic graphics`, + help: `Better performance on older devices.`, + disabled: () => false, + }, + pointerLock: { + default: false, + name: `Mouse pointer lock`, + help: `Locks and hides the mouse cursor.`, + disabled: () => !gameCanvas.requestPointerLock, + }, + easy: { + default: false, + name: `Kids mode`, + help: `Start future runs with "slower ball".`, + 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.`, + disabled() { + return window.location.search.includes("isInWebView=true"); + }, + }, +} as {[k:string]:OptionDef} + +export type OptionDef = { + default:boolean; +name:string; +help:string; +disabled:()=>boolean + afterChange?:()=>void +} +export type OptionId = keyof (typeof options) \ No newline at end of file diff --git a/src/rawUpgrades.ts b/src/rawUpgrades.ts index 6108c8a..206e08d 100644 --- a/src/rawUpgrades.ts +++ b/src/rawUpgrades.ts @@ -212,8 +212,8 @@ export const rawUpgrades = [ id: "compound_interest", giftable: true, name: "Compound interest", - max: 3, - help: (lvl) => `+${lvl} combo / brick broken, -${lvl} combo per coin lost`, + 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 diff --git a/src/types.d.ts b/src/types.d.ts index 84c4af7..2476c34 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,103 +1,143 @@ -import { rawUpgrades } from "./rawUpgrades"; +import {rawUpgrades} from "./rawUpgrades"; export type colorString = string; export type RawLevel = { - name: string; - size: number; - bricks: string; - svg: string; - color: string; + name: string; + size: number; + bricks: string; + svg: string; + color: string; }; export type Level = { - name: string; - size: number; - bricks: colorString[]; - svg: string; - color: string; - threshold?: number; - sortKey?: number; + name: string; + size: number; + bricks: colorString[]; + svg: string; + color: string; + threshold?: number; + sortKey?: number; }; export type Palette = { [k: string]: string }; export type Upgrade = { - threshold: number; - giftable: boolean; - id: string; - name: string; - icon: string; - max: number; - help: (lvl: string) => string; - fullHelp: string; - requires: PerkId | ""; + threshold: number; + giftable: boolean; + id: PerkId; + name: string; + icon: string; + max: number; + help: (lvl: number) => string; + fullHelp: string; + requires: PerkId | ""; }; export type PerkId = (typeof rawUpgrades)[number]["id"]; declare global { - interface Window { - webkitAudioContext?: typeof AudioContext; - } - interface Document { - webkitFullscreenEnabled?: boolean; - webkitCancelFullScreen?: ()=>void; - } - interface Element { - webkitRequestFullscreen: typeof Element.requestFullscreen - } + interface Window { + webkitAudioContext?: typeof AudioContext; + } + + interface Document { + webkitFullscreenEnabled?: boolean; + webkitCancelFullScreen?: () => void; + } + + interface Element { + webkitRequestFullscreen: typeof Element.requestFullscreen + } + + interface MediaStream { + // https://devdoc.net/web/developer.mozilla.org/en-US/docs/Web/API/CanvasCaptureMediaStream.html + // On firefox, the capture stream has the requestFrame option + // instead of the track, go figure + requestFrame?:()=>void + } + } -export type Coin={ - points:number; - color: colorString; - x:number; - y:number; - previousx:number; - previousy:number; - vx:number; - vy:number; - sx:number; - sy:number; - a:number; - sa:number; - weight:number; - destroyed?:boolean; - coloredABrick?:boolean; +export type BallLike = { + x: number; + y: number; + vx?: number; + vy?: number; +} + +export type Coin = { + points: number; + color: colorString; + x: number; + y: number; + previousx: number; + previousy: number; + vx: number; + vy: number; + sx: number; + sy: number; + a: number; + sa: number; + weight: number; + destroyed?: boolean; + coloredABrick?: boolean; } export type Ball = { - x:number; - previousx:number; - y:number; - previousy:number; - vx:number; - vy:number; - sx:number; - sy:number; - sparks:number; - piercedSinceBounce:number; - hitSinceBounce:number; - hitItem: {index:number, color:string}[], - sapperUses:number; - destroyed?:boolean; + x: number; + previousx: number; + y: number; + previousy: number; + vx: number; + vy: number; + sx: number; + sy: number; + sparks: number; + piercedSinceBounce: number; + hitSinceBounce: number; + hitItem: { index: number, color: string }[]; + bouncesList?: { x: number, y: number }[]; + sapperUses: number; + destroyed?: boolean; + previousvx?: number; + previousvy?: number; } -export type FlashTypes= "text"|"particle"|'ball' +export type FlashTypes = "text" | "particle" | 'ball' export type Flash = { - type: FlashTypes; - text?:string; - time:number; - color:colorString; - x:number; - y:number; - duration:number; - size:number; - vx?:number; - vy?:number; - ethereal?:boolean; - destroyed?:boolean; - + type: FlashTypes; + text?: string; + time: number; + color: colorString; + x: number; + y: number; + duration: number; + size: number; + vx?: number; + vy?: number; + ethereal?: boolean; + destroyed?: boolean; +} + +export type RunStats= { + started: number; + levelsPlayed: number; + runTime: number; + coins_spawned: number; + score: number; + bricks_broken: number; + misses: number; + balls_lost: number; + puck_bounces: number; + upgrades_picked: number; + max_combo: number; + max_level: number; +} + + +export type RunHistoryItem =RunStats & { + perks?: {[k in PerkId]:number}; + appVersion?:string; }