Creative mode, cleanup loop fix

This commit is contained in:
Renan LE CARO 2025-03-07 20:18:18 +01:00
parent 2d2d4fd963
commit 504fd6649c
8 changed files with 3189 additions and 2901 deletions

View file

@ -38,7 +38,6 @@ quickly destroyed again.
# Game engine features # Game engine features
- the onboarding feels weird, missing a tutorial - the onboarding feels weird, missing a tutorial
- Players can't choose the initial perk
- apk version soft locks at start. - apk version soft locks at start.
- shinier coins by applying glow to them ? - shinier coins by applying glow to them ?
- ask for permanent storage - ask for permanent storage
@ -124,6 +123,28 @@ quickly destroyed again.
- gravity is flipped on the opposite side to the puck (for coins) - gravity is flipped on the opposite side to the puck (for coins)
- balls have gravity - balls have gravity
- coins don't have gravity - coins don't have gravity
- [colin] yoyo - when the ball falls back down, it curbs towards your puck (after hitting a brick or top)
- [colin] single block combo - get +1 combo if the ball only breaks a single block before reaching the puck
- [colin] 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.
- [colin] side pucks - same as above but with two side pucks.
- [colin] ball coins - coins share the same physics as coins and bounce on walls and bricks
- [colin] phantom coins - coins pass through bricks
- [colin] drifting coins - coins slowly drift away from the brick they were generated from, and they need to be collected by the ball
- [colin] bigger ball - self-explanatory
- [colin] smaller ball - yes.
- [colin] sturdy ball - does more damage to bricks, to conter sturdy bricks
- [colin] accumulation - coins aglutinate into bigger coins that hold more value
- [colin] forgiving - you can miss several times without losing your combo. or alternatively, include this ability into the soft reset perk.
- [colin] plot - plot the ball's trajectory as you position your puck
- [colin] golden corners - catch coins at the sides of the puck to double their value
- [colin] varied diet - your combo grows if you keep hitting different coloured bricks each time
- [colin] 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
- [colin] statue - stand still to make the combo grow. move for too long and thi combo will quickly drop
- [colin] piggy bank - bricks absorb coins that fall onto it, and release them back as they are broken, with added value
- [colin] trickle up - if you first hit is the lowest brick of a column, all bricks above get +1 coin inside
- [colin] 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
- [colin] hitman - hit the marked brick for +5 combo. each level increases the combo you get for it.
- [colin] sweet spot - place your puck directly below a moving spot at the top of the level to increase your combo
# Balancing ideas # Balancing ideas
@ -165,31 +186,6 @@ I could unlock the "pro stand" at $999 that just holds the play area higher.
# Colin's feedback (cwpute/obigre) # 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 : 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. * 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.

234
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,17 @@
import { allLevels, appVersion, icons, upgrades } from "./loadGameData"; import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
import {Ball, BallLike, Coin, colorString, Flash, FlashTypes, Level, PerkId, RunHistoryItem, RunStats} from "./types"; import {
Ball,
BallLike,
Coin,
colorString,
Flash,
FlashTypes,
Level,
PerkId,
RunHistoryItem,
RunStats,
Upgrade,
} from "./types";
import { OptionId, options } from "./options"; import { OptionId, options } from "./options";
const MAX_COINS = 400; const MAX_COINS = 400;
@ -168,7 +180,9 @@ export const fitSize = () => {
backgroundCanvas.height = height; backgroundCanvas.height = height;
gameZoneHeight = isSettingOn("mobile-mode") ? (height * 80) / 100 : height; gameZoneHeight = isSettingOn("mobile-mode") ? (height * 80) / 100 : height;
const baseWidth = Math.round(Math.min(gameCanvas.width, gameZoneHeight * 0.73)); const baseWidth = Math.round(
Math.min(gameCanvas.width, gameZoneHeight * 0.73),
);
brickWidth = Math.floor(baseWidth / gridSize / 2) * 2; brickWidth = Math.floor(baseWidth / gridSize / 2) * 2;
gameZoneWidth = brickWidth * gridSize; gameZoneWidth = brickWidth * gridSize;
offsetX = Math.floor((gameCanvas.width - gameZoneWidth) / 2); offsetX = Math.floor((gameCanvas.width - gameZoneWidth) / 2);
@ -215,7 +229,14 @@ function getRowColIndex(row: number, col: number) {
return row * gridSize + col; return row * gridSize + col;
} }
function spawnExplosion(count: number, x: number, y: number, color: string, duration = 150, size = coinSize) { function spawnExplosion(
count: number,
x: number,
y: number,
color: string,
duration = 150,
size = coinSize,
) {
if (!!isSettingOn("basic")) return; if (!!isSettingOn("basic")) return;
if (flashes.length > MAX_PARTICLES) { if (flashes.length > MAX_PARTICLES) {
// Avoid freezing when lots of explosion happen at once // Avoid freezing when lots of explosion happen at once
@ -246,8 +267,9 @@ let lastPlayedCoinGrab = 0;
function addToScore(coin: Coin) { function addToScore(coin: Coin) {
coin.destroyed = true; coin.destroyed = true;
score += coin.points; score += coin.points;
addToTotalScore(coin.points); addToTotalScore(coin.points);
if (score > highScore) { if (score > highScore && !ignoreThisRunInStats) {
highScore = score; highScore = score;
localStorage.setItem("breakout-3-hs", score.toString()); localStorage.setItem("breakout-3-hs", score.toString());
} }
@ -281,6 +303,9 @@ function resetBalls() {
const perBall = puckWidth / (count + 1); const perBall = puckWidth / (count + 1);
balls = []; balls = [];
ballsColor = "#FFF"; ballsColor = "#FFF";
if(perks.picky_eater || perks.pierce_color){
ballsColor=getMajorityValue(bricks.filter(i=>i)) || '#FFF'
}
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const x = puck - puckWidth / 2 + perBall * (i + 1); const x = puck - puckWidth / 2 + perBall * (i + 1);
balls.push({ balls.push({
@ -414,6 +439,7 @@ function setLevel(l:number) {
currentLevel = l; currentLevel = l;
levelTime = 0; levelTime = 0;
level_skip_last_uses = 0;
lastTickDown = levelTime; lastTickDown = levelTime;
levelStartScore = score; levelStartScore = score;
levelSpawnedCoins = 0; levelSpawnedCoins = 0;
@ -452,7 +478,8 @@ function reset_perks():PerkId {
const giftable = getPossibleUpgrades().filter((u) => u.giftable); const giftable = getPossibleUpgrades().filter((u) => u.giftable);
const randomGift = const randomGift =
nextRunOverrides?.perk || nextRunOverrides?.perk ||
(isSettingOn("easy") && "slow_down" ) || giftable[Math.floor(Math.random() * giftable.length)].id; (isSettingOn("easy") && "slow_down") ||
giftable[Math.floor(Math.random() * giftable.length)].id;
perks[randomGift] = 1; perks[randomGift] = 1;
@ -538,7 +565,7 @@ function pickRandomUpgrades(count: number) {
type RunOverrides = { level?: PerkId; perk?: string }; type RunOverrides = { level?: PerkId; perk?: string };
let nextRunOverrides = {} as RunOverrides; let nextRunOverrides = {} as RunOverrides;
let ignoreThisRunInStats = false;
let pauseUsesDuringRun = 0; let pauseUsesDuringRun = 0;
@ -546,6 +573,7 @@ function restart() {
// When restarting, we want to avoid restarting with the same level we're on, so we exclude from the next // When restarting, we want to avoid restarting with the same level we're on, so we exclude from the next
// run's level list // run's level list
totalScoreAtRunStart = getTotalScore(); totalScoreAtRunStart = getTotalScore();
ignoreThisRunInStats = false;
shuffleLevels(levelTime || score ? currentLevelInfo().name : null); shuffleLevels(levelTime || score ? currentLevelInfo().name : null);
resetRunStatistics(); resetRunStatistics();
score = 0; score = 0;
@ -639,7 +667,11 @@ function hitsSomething(x:number, y:number, radius:number) {
); );
} }
function shouldPierceByColor(vhit:number|undefined, hhit:number|undefined, chit:number|undefined) { function shouldPierceByColor(
vhit: number | undefined,
hhit: number | undefined,
chit: number | undefined,
) {
if (!perks.pierce_color) return false; if (!perks.pierce_color) return false;
if (typeof vhit !== "undefined" && bricks[vhit] !== ballsColor) { if (typeof vhit !== "undefined" && bricks[vhit] !== ballsColor) {
return false; return false;
@ -654,7 +686,7 @@ function shouldPierceByColor(vhit:number|undefined, hhit:number|undefined, chit:
} }
function ballBrickHitCheck(ball: Ball) { function ballBrickHitCheck(ball: Ball) {
const radius=ballSize / 2 const radius = ballSize / 2;
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const { x, y, previousx, previousy } = ball; const { x, y, previousx, previousy } = ball;
@ -684,7 +716,6 @@ function ballBrickHitCheck(ball:Ball) {
ball.y = ball.previousy; ball.y = ball.previousy;
ball.vy *= -1; ball.vy *= -1;
} }
} }
if (typeof hhit !== "undefined" || typeof chit !== "undefined") { if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
if (!pierce) { if (!pierce) {
@ -696,9 +727,8 @@ function ballBrickHitCheck(ball:Ball) {
return vhit ?? hhit ?? chit; return vhit ?? hhit ?? chit;
} }
function coinBrickHitCheck(coin: Coin) { function coinBrickHitCheck(coin: Coin) {
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const radius=coinSize/2 const radius = coinSize / 2;
const { x, y, previousx, previousy } = coin; const { x, y, previousx, previousy } = coin;
const vhit = hitsSomething(previousx, y, radius); const vhit = hitsSomething(previousx, y, radius);
@ -720,7 +750,6 @@ function coinBrickHitCheck(coin:Coin) {
if (leftHit && !rightHit) { if (leftHit && !rightHit) {
coin.vx += 1; coin.vx += 1;
coin.sa -= 1; coin.sa -= 1;
} }
if (!leftHit && rightHit) { if (!leftHit && rightHit) {
coin.vx -= 1; coin.vx -= 1;
@ -808,12 +837,13 @@ function tick() {
decreaseCombo(perks.hot_start, puck, gameZoneHeight - 2 * puckHeight); decreaseCombo(perks.hot_start, puck, gameZoneHeight - 2 * puckHeight);
} }
if (remainingBricks <= perks.skip_last) { if (remainingBricks <= perks.skip_last && !level_skip_last_uses) {
bricks.forEach((type, index) => { bricks.forEach((type, index) => {
if (type) { if (type) {
explodeBrick(index, balls[0], true); explodeBrick(index, balls[0], true);
} }
}); });
level_skip_last_uses++
} }
if (!remainingBricks && !coins.length) { if (!remainingBricks && !coins.length) {
if (currentLevel + 1 < max_levels()) { if (currentLevel + 1 < max_levels()) {
@ -868,9 +898,7 @@ function tick() {
} else if (coin.y > gameCanvas.height + coinRadius) { } else if (coin.y > gameCanvas.height + coinRadius) {
coin.destroyed = true; coin.destroyed = true;
if (perks.compound_interest) { if (perks.compound_interest) {
resetCombo( resetCombo(coin.x, coin.y);
coin.x,coin.y
);
} }
} }
@ -964,22 +992,33 @@ function tick() {
vy: 5, vy: 5,
}); });
} }
if (perks.sides_are_lava) {
const fromLeft = Math.random() > 0.5; if (perks.left_is_lava && baseParticle) {
baseParticle &&
flashes.push({ flashes.push({
...baseParticle, ...baseParticle,
x: offsetXRoundedDown + (fromLeft ? 0 : gameZoneWidthRoundedUp), x: offsetXRoundedDown,
y: Math.random() * gameZoneHeight, y: Math.random() * gameZoneHeight,
vx: fromLeft ? 5 : -5, vx: 5,
vy: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10,
}); });
} }
if (perks.right_is_lava && baseParticle) {
flashes.push({
...baseParticle,
x: offsetXRoundedDown + gameZoneWidthRoundedUp,
y: Math.random() * gameZoneHeight,
vx: -5,
vy: (Math.random() - 0.5) * 10,
});
}
if (perks.compound_interest) { if (perks.compound_interest) {
let x = puck, attemps=0; let x = puck,
attemps = 0;
do { do {
x = offsetXRoundedDown + gameZoneWidthRoundedUp * Math.random(); x = offsetXRoundedDown + gameZoneWidthRoundedUp * Math.random();
attemps++ attemps++;
} while (Math.abs(x - puck) < puckWidth / 2 && attemps < 10); } while (Math.abs(x - puck) < puckWidth / 2 && attemps < 10);
baseParticle && baseParticle &&
flashes.push({ flashes.push({
@ -1097,9 +1136,22 @@ function ballTick(ball:Ball, delta:number) {
const borderHitCode = bordersHitCheck(ball, ballSize / 2, delta); const borderHitCode = bordersHitCheck(ball, ballSize / 2, delta);
if (borderHitCode) { if (borderHitCode) {
if (perks.sides_are_lava && borderHitCode % 2) { if (
perks.left_is_lava &&
borderHitCode % 2 &&
ball.x < offsetX + gameZoneWidth / 2
) {
resetCombo(ball.x, ball.y); resetCombo(ball.x, ball.y);
} }
if (
perks.right_is_lava &&
borderHitCode % 2 &&
ball.x > offsetX + gameZoneWidth / 2
) {
resetCombo(ball.x, ball.y);
}
if (perks.top_is_lava && borderHitCode >= 2) { if (perks.top_is_lava && borderHitCode >= 2) {
resetCombo(ball.x, ball.y + ballSize); resetCombo(ball.x, ball.y + ballSize);
} }
@ -1222,7 +1274,8 @@ function ballTick(ball:Ball, delta:number) {
} }
} }
const defaultRunStats = () => ({ const defaultRunStats = () =>
({
started: Date.now(), started: Date.now(),
levelsPlayed: 0, levelsPlayed: 0,
runTime: 0, runTime: 0,
@ -1251,13 +1304,13 @@ function getTotalScore() {
} }
function addToTotalScore(points: number) { function addToTotalScore(points: number) {
if (ignoreThisRunInStats) return;
try { try {
localStorage.setItem( localStorage.setItem(
"breakout_71_total_score", "breakout_71_total_score",
JSON.stringify(getTotalScore() + points), JSON.stringify(getTotalScore() + points),
); );
} catch (e) { } catch (e) {}
}
} }
function addToTotalPlayTime(ms: number) { function addToTotalPlayTime(ms: number) {
@ -1269,8 +1322,7 @@ function addToTotalPlayTime(ms:number) {
ms, ms,
), ),
); );
} catch (e) { } catch (e) {}
}
} }
function gameOver(title: string, intro: string) { function gameOver(title: string, intro: string) {
@ -1336,19 +1388,21 @@ function gameOver(title:string, intro:string) {
allowClose: true, allowClose: true,
title, title,
text: ` text: `
${ignoreThisRunInStats ? "<p>This test run and its score are not being recorded</p>" : ""}
<p>${intro}</p> <p>${intro}</p>
${unlocksInfo} ${unlocksInfo}
`, `,
actions:[{ actions: [
{
value: null, value: null,
text:'Start a new run', text: "Start a new run",
help:'', help: "",
}], },
],
textAfterButtons: `<div id="level-recording-container"></div> textAfterButtons: `<div id="level-recording-container"></div>
${getHistograms()} ${getHistograms()}
`, `,
}).then(() => restart()); }).then(() => restart());
} }
function getHistograms() { function getHistograms() {
@ -1364,13 +1418,17 @@ function getHistograms() {
runsHistory.push({ ...runStatistics, perks, appVersion }); runsHistory.push({ ...runStatistics, perks, appVersion });
// Generate some histogram // Generate some histogram
if (!ignoreThisRunInStats)
localStorage.setItem( localStorage.setItem(
"breakout_71_runs_history", "breakout_71_runs_history",
JSON.stringify(runsHistory, null, 2), JSON.stringify(runsHistory, null, 2),
); );
const makeHistogram = (title:string, getter: (hi:RunHistoryItem)=>number, unit:string) => { const makeHistogram = (
title: string,
getter: (hi: RunHistoryItem) => number,
unit: string,
) => {
let values = runsHistory.map((h) => getter(h) || 0); let values = runsHistory.map((h) => getter(h) || 0);
let min = Math.min(...values); let min = Math.min(...values);
let max = Math.max(...values); let max = Math.max(...values);
@ -1580,7 +1638,8 @@ function explodeBrick(index:number, ball:Ball, isExplosion:boolean) {
0, 0,
perks.streak_shots + perks.streak_shots +
perks.compound_interest + perks.compound_interest +
perks.sides_are_lava + perks.left_is_lava +
perks.right_is_lava +
perks.top_is_lava + perks.top_is_lava +
perks.picky_eater - perks.picky_eater -
Math.round(Math.random() * perks.soft_reset), Math.round(Math.random() * perks.soft_reset),
@ -1615,7 +1674,7 @@ function explodeBrick(index:number, ball:Ball, isExplosion:boolean) {
spawnExplosion(5 + Math.min(combo, 30), x, y, color, 150, coinSize / 2); spawnExplosion(5 + Math.min(combo, 30), x, y, color, 150, coinSize / 2);
} }
if (!bricks[index] && color!=='black') { if (!bricks[index] && color !== "black") {
ball.hitItem?.push({ ball.hitItem?.push({
index, index,
color, color,
@ -1844,16 +1903,18 @@ function render() {
} }
} }
// Borders // Borders
const redSides = perks.sides_are_lava && combo > baseCombo(); const hasCombo = combo > baseCombo();
ctx.fillStyle = redSides ? "red" : puckColor;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
if (offsetXRoundedDown) { if (offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings // draw outside of gaming area to avoid capturing borders in recordings
ctx.fillStyle = hasCombo && perks.left_is_lava ? "red" : puckColor;
ctx.fillRect(offsetX - 1, 0, 1, height); ctx.fillRect(offsetX - 1, 0, 1, height);
ctx.fillStyle = hasCombo && perks.right_is_lava ? "red" : puckColor;
ctx.fillRect(width - offsetX + 1, 0, 1, height); ctx.fillRect(width - offsetX + 1, 0, 1, height);
} else if (redSides) { } else {
ctx.fillRect(0, 0, 1, height); ctx.fillStyle = "red";
ctx.fillRect(width - 1, 0, 1, height); if (hasCombo && perks.left_is_lava) ctx.fillRect(0, 0, 1, height);
if (hasCombo && perks.right_is_lava) ctx.fillRect(width - 1, 0, 1, height);
} }
if (perks.top_is_lava && combo > baseCombo()) if (perks.top_is_lava && combo > baseCombo())
@ -1905,13 +1966,15 @@ function renderAllBricks() {
"_" + "_" +
redBorderOnBricksWithWrongColor + redBorderOnBricksWithWrongColor +
"_" + "_" +
ballsColor; ballsColor+'_'+perks.pierce_color;
if (newKey !== cachedBricksRenderKey) { if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey; cachedBricksRenderKey = newKey;
cachedBricksRender.width = gameZoneWidth; cachedBricksRender.width = gameZoneWidth;
cachedBricksRender.height = gameZoneWidth + 1; cachedBricksRender.height = gameZoneWidth + 1;
const canctx = cachedBricksRender.getContext("2d") as CanvasRenderingContext2D; const canctx = cachedBricksRender.getContext(
"2d",
) as CanvasRenderingContext2D;
canctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth); canctx.clearRect(0, 0, gameZoneWidth, gameZoneWidth);
canctx.resetTransform(); canctx.resetTransform();
canctx.translate(-offsetX, 0); canctx.translate(-offsetX, 0);
@ -1921,10 +1984,12 @@ function renderAllBricks() {
y = brickCenterY(index); y = brickCenterY(index);
if (!color) return; if (!color) return;
const borderColor =
(ballsColor === color && puckColor) || canctx.globalAlpha = (perks.pierce_color && ballsColor === color && 0.6) || 1;
(color !== "black" && redBorderOnBricksWithWrongColor && "red") ||
const borderColor = (ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor && "red") ||
color; color;
drawBrick(canctx, color, borderColor, x, y); drawBrick(canctx, color, borderColor, x, y);
if (color === "black") { if (color === "black") {
canctx.globalCompositeOperation = "source-over"; canctx.globalCompositeOperation = "source-over";
@ -1938,8 +2003,13 @@ function renderAllBricks() {
let cachedGraphics = {}; let cachedGraphics = {};
function drawPuck(ctx:CanvasRenderingContext2D, color:colorString, function drawPuck(
puckWidth:number, puckHeight:number, yoffset = 0) { ctx: CanvasRenderingContext2D,
color: colorString,
puckWidth: number,
puckHeight: number,
yoffset = 0,
) {
const key = "puck" + color + "_" + puckWidth + "_" + puckHeight; const key = "puck" + color + "_" + puckWidth + "_" + puckHeight;
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
@ -1972,8 +2042,14 @@ function drawPuck(ctx:CanvasRenderingContext2D, color:colorString,
); );
} }
function drawBall(ctx:CanvasRenderingContext2D, function drawBall(
color:colorString, width:number, x:number, y:number, borderColor = "") { ctx: CanvasRenderingContext2D,
color: colorString,
width: number,
x: number,
y: number,
borderColor = "",
) {
const key = "ball" + color + "_" + width + "_" + borderColor; const key = "ball" + color + "_" + width + "_" + borderColor;
const size = Math.round(width); const size = Math.round(width);
@ -2004,8 +2080,15 @@ function drawBall(ctx:CanvasRenderingContext2D,
const angles = 32; const angles = 32;
function drawCoin(ctx:CanvasRenderingContext2D, color:colorString, size:number, function drawCoin(
x:number, y:number, borderColor:colorString, rawAngle:number) { ctx: CanvasRenderingContext2D,
color: colorString,
size: number,
x: number,
y: number,
borderColor: colorString,
rawAngle: number,
) {
const angle = const angle =
((Math.round((rawAngle / Math.PI) * 2 * angles) % angles) + angles) % ((Math.round((rawAngle / Math.PI) * 2 * angles) % angles) + angles) %
angles; angles;
@ -2059,8 +2142,13 @@ function drawCoin(ctx:CanvasRenderingContext2D, color:colorString, size:number,
); );
} }
function drawFuzzyBall(ctx:CanvasRenderingContext2D, color:colorString, width:number, function drawFuzzyBall(
x:number, y:number) { ctx: CanvasRenderingContext2D,
color: colorString,
width: number,
x: number,
y: number,
) {
const key = "fuzzy-circle" + color + "_" + width; const key = "fuzzy-circle" + color + "_" + width;
if (!color) debugger; if (!color) debugger;
const size = Math.round(width * 3); const size = Math.round(width * 3);
@ -2091,8 +2179,13 @@ function drawFuzzyBall(ctx:CanvasRenderingContext2D, color:colorString, width:nu
); );
} }
function drawBrick(ctx:CanvasRenderingContext2D, color:colorString, borderColor:colorString, function drawBrick(
x:number, y:number) { ctx: CanvasRenderingContext2D,
color: colorString,
borderColor: colorString,
x: number,
y: number,
) {
const tlx = Math.ceil(x - brickWidth / 2); const tlx = Math.ceil(x - brickWidth / 2);
const tly = Math.ceil(y - brickWidth / 2); const tly = Math.ceil(y - brickWidth / 2);
const brx = Math.ceil(x + brickWidth / 2) - 1; const brx = Math.ceil(x + brickWidth / 2) - 1;
@ -2131,7 +2224,14 @@ function drawBrick(ctx:CanvasRenderingContext2D, color:colorString, borderColor:
// It's not easy to have a 1px gap between bricks without antialiasing // It's not easy to have a 1px gap between bricks without antialiasing
} }
function roundRect(ctx:CanvasRenderingContext2D, x:number, y:number, width:number, height:number, radius:number) { function roundRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
) {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x + radius, y); ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y); ctx.lineTo(x + width - radius, y);
@ -2145,12 +2245,24 @@ function roundRect(ctx:CanvasRenderingContext2D, x:number, y:number, width:numbe
ctx.closePath(); ctx.closePath();
} }
function drawRedSquare(ctx:CanvasRenderingContext2D, x:number, y:number, width:number, height:number) { function drawRedSquare(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
) {
ctx.fillStyle = "red"; ctx.fillStyle = "red";
ctx.fillRect(x, y, width, height); ctx.fillRect(x, y, width, height);
} }
function drawIMG(ctx:CanvasRenderingContext2D, img:HTMLImageElement, size:number, x:number, y:number) { function drawIMG(
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
size: number,
x: number,
y: number,
) {
const key = "svg" + img + "_" + size + "_" + img.complete; const key = "svg" + img + "_" + size + "_" + img.complete;
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
@ -2174,8 +2286,15 @@ function drawIMG(ctx:CanvasRenderingContext2D, img:HTMLImageElement, size:number
); );
} }
function drawText(ctx:CanvasRenderingContext2D, function drawText(
text:string, color:colorString, fontSize:number, x:number, y:number, left = false) { ctx: CanvasRenderingContext2D,
text: string,
color: colorString,
fontSize: number,
x: number,
y: number,
left = false,
) {
const key = "text" + text + "_" + color + "_" + fontSize + "_" + left; const key = "text" + text + "_" + color + "_" + fontSize + "_" + left;
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
@ -2268,7 +2387,8 @@ const sounds = {
}; };
// How to play the code on the leftconst context = new window.AudioContext(); // How to play the code on the leftconst context = new window.AudioContext();
let audioContext:AudioContext, audioRecordingTrack:MediaStreamAudioDestinationNode; let audioContext: AudioContext,
audioRecordingTrack: MediaStreamAudioDestinationNode;
function getAudioContext() { function getAudioContext() {
if (!audioContext) { if (!audioContext) {
@ -2414,7 +2534,8 @@ function createExplosionSound(pan = 0.5) {
} }
let levelTime = 0; let levelTime = 0;
// Limits skip last to one use per level
let level_skip_last_uses = 0;
window.addEventListener("visibilitychange", () => { window.addEventListener("visibilitychange", () => {
if (document.hidden) { if (document.hidden) {
@ -2432,6 +2553,7 @@ function asyncAlert<t>({
actions, actions,
allowClose = true, allowClose = true,
textAfterButtons = "", textAfterButtons = "",
actionsAsGrid = false,
}: { }: {
title?: string; title?: string;
text?: string; text?: string;
@ -2441,15 +2563,17 @@ function asyncAlert<t>({
help?: string; help?: string;
disabled?: boolean; disabled?: boolean;
icon?: string; icon?: string;
className?: string;
}[]; }[];
textAfterButtons?: string; textAfterButtons?: string;
allowClose?: boolean; allowClose?: boolean;
actionsAsGrid?: boolean;
}): Promise<t | void> { }): Promise<t | void> {
alertsOpen++; alertsOpen++;
return new Promise((resolve) => { return new Promise((resolve) => {
const popupWrap = document.createElement("div"); const popupWrap = document.createElement("div");
document.body.appendChild(popupWrap); document.body.appendChild(popupWrap);
popupWrap.className = "popup"; popupWrap.className = "popup " + (actionsAsGrid ? "actionsAsGrid " : "");
function closeWithResult(value: t | void) { function closeWithResult(value: t | void) {
resolve(value); resolve(value);
@ -2487,9 +2611,12 @@ function asyncAlert<t>({
popup.appendChild(p); popup.appendChild(p);
} }
const buttons = document.createElement("section");
popup.appendChild(buttons);
actions actions
.filter((i) => i) .filter((i) => i)
.forEach(({text, value, help, disabled, icon = ""}) => { .forEach(({ text, value, help, disabled, className = "", icon = "" }) => {
const button = document.createElement("button"); const button = document.createElement("button");
button.innerHTML = ` button.innerHTML = `
@ -2507,7 +2634,8 @@ ${icon}
closeWithResult(value); closeWithResult(value);
}); });
} }
popup.appendChild(button); button.className = className;
buttons.appendChild(button);
}); });
if (textAfterButtons) { if (textAfterButtons) {
@ -2561,7 +2689,6 @@ export function toggleSetting(key:OptionId) {
if (options[key].afterChange) options[key].afterChange(); if (options[key].afterChange) options[key].afterChange();
} }
scoreDisplay.addEventListener("click", (e) => { scoreDisplay.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
openScorePanel().then(); openScorePanel().then();
@ -2572,6 +2699,7 @@ async function openScorePanel() {
const cb = await asyncAlert({ const cb = await asyncAlert({
title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`, title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`,
text: ` text: `
${ignoreThisRunInStats ? "<p>This is a test run, score is not recorded permanently</p>" : ""}
<p>Upgrades picked so far : </p> <p>Upgrades picked so far : </p>
<p>${pickedUpgradesHTMl()}</p> <p>${pickedUpgradesHTMl()}</p>
`, `,
@ -2580,7 +2708,7 @@ async function openScorePanel() {
{ {
text: "Resume", text: "Resume",
help: "Return to your run", help: "Return to your run",
value:()=>{} value: () => {},
}, },
{ {
text: "Restart", text: "Restart",
@ -2601,7 +2729,6 @@ document.getElementById("menu").addEventListener("click", (e) => {
openSettingsPanel().then(); openSettingsPanel().then();
}); });
async function openSettingsPanel() { async function openSettingsPanel() {
pause(true); pause(true);
@ -2621,6 +2748,7 @@ async function openSettingsPanel() {
}, },
}); });
} }
const creativeModeTreshold=Math.max(...upgrades.map((u) => u.threshold))
const cb = await asyncAlert<() => void>({ const cb = await asyncAlert<() => void>({
title: "Breakout 71", title: "Breakout 71",
@ -2631,17 +2759,15 @@ async function openSettingsPanel() {
{ {
text: "Resume", text: "Resume",
help: "Return to your run", help: "Return to your run",
value() { value() {},
},
}, },
{ {
text: "Starting perk", text: "Starting perk",
help: "Try perks and levels you unlocked", help: "Try perks and levels you unlocked",
value() { value() {
openUnlocksList() openUnlocksList();
}, },
}, },
...optionsList, ...optionsList,
(document.fullscreenEnabled || document.webkitFullscreenEnabled) && (document.fullscreenEnabled || document.webkitFullscreenEnabled) &&
@ -2662,6 +2788,50 @@ async function openSettingsPanel() {
toggleFullScreen(); toggleFullScreen();
}, },
}), }),
{
text: "Creative mode",
help:getTotalScore() < creativeModeTreshold ? "Unlocks at total score $"+creativeModeTreshold: "Test runs with custom perks" ,
disabled: getTotalScore() < creativeModeTreshold,
async value() {
let creativeModePerks = {},
choice;
while (
(choice = await asyncAlert<string | Upgrade>({
title: "Select perks",
text: 'Select perks below and press "start run" to try them out in a test run. Scores and stats are not recorded.',
actionsAsGrid: true,
actions: [
...upgrades.map((u) => ({
icon: u.icon,
text: u.name,
help: (creativeModePerks[u.id] || 0) + "/" + u.max,
value: u,
className: creativeModePerks[u.id]
? ""
: "grey-out-unless-hovered",
})),
{
text: "Start run",
value: "start",
},
],
}))
) {
if (choice === "start") {
restart();
ignoreThisRunInStats = true;
Object.assign(perks, creativeModePerks);
break;
} else if (choice) {
creativeModePerks[choice.id] =
((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1);
}
}
},
},
{ {
text: "Reset Game", text: "Reset Game",
help: "Erase high score and statistics", help: "Erase high score and statistics",
@ -2708,7 +2878,6 @@ async function openSettingsPanel() {
} }
async function openUnlocksList() { async function openUnlocksList() {
const ts = getTotalScore(); const ts = getTotalScore();
const actions = [ const actions = [
...upgrades ...upgrades
@ -2716,9 +2885,7 @@ async function openUnlocksList() {
.map(({ name, id, threshold, icon, fullHelp }) => ({ .map(({ name, id, threshold, icon, fullHelp }) => ({
text: name, text: name,
help: help:
ts >= threshold ts >= threshold ? fullHelp : `Unlocks at total score ${threshold}.`,
? fullHelp
: `Unlocks at total score ${threshold}.`,
disabled: ts < threshold, disabled: ts < threshold,
value: { perk: id } as RunOverrides, value: { perk: id } as RunOverrides,
icon, icon,
@ -2776,11 +2943,14 @@ Click an item above to start a run with it.
} }
} }
function distance2(a:{x:number,y:number}, b:{x:number,y:number}) { 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); return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
} }
function distanceBetween(a:{x:number,y:number}, b:{x:number,y:number}) { function distanceBetween(
a: { x: number; y: number },
b: { x: number; y: number },
) {
return Math.sqrt(distance2(a, b)); return Math.sqrt(distance2(a, b));
} }
@ -2898,7 +3068,6 @@ function recordOneFrame() {
captureTrack?.requestFrame(); captureTrack?.requestFrame();
} else if (captureStream?.requestFrame) { } else if (captureStream?.requestFrame) {
captureStream.requestFrame(); captureStream.requestFrame();
} }
} }
@ -2944,7 +3113,8 @@ function startRecordingGame() {
}) as CanvasRenderingContext2D; }) as CanvasRenderingContext2D;
captureStream = recordCanvas.captureStream(0); captureStream = recordCanvas.captureStream(0);
captureTrack = captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack captureTrack =
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) { if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) {
captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]); captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]);
@ -3028,7 +3198,7 @@ function stopRecording() {
mediaRecorder = null; mediaRecorder = null;
} }
function captureFileName(ext='webm') { function captureFileName(ext = "webm") {
return ( return (
"breakout-71-capture-" + "breakout-71-capture-" +
new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") +
@ -3037,7 +3207,10 @@ function captureFileName(ext='webm') {
); );
} }
function findLast<T>(arr:T[], predicate:(item:T,index:number,array:T[])=>boolean) { function findLast<T>(
arr: T[],
predicate: (item: T, index: number, array: T[]) => boolean,
) {
let i = arr.length; let i = arr.length;
while (--i) while (--i)
if (predicate(arr[i], i, arr)) { if (predicate(arr[i], i, arr)) {
@ -3100,20 +3273,19 @@ document.addEventListener("keydown", (e) => {
}); });
document.addEventListener("keyup", (e) => { document.addEventListener("keyup", (e) => {
const focused = document.querySelector("button:focus") const focused = document.querySelector("button:focus");
if (e.key in pressed) { if (e.key in pressed) {
setKeyPressed(e.key, 0); setKeyPressed(e.key, 0);
} else if ( } else if (
e.key === "ArrowDown" && focused?.nextElementSibling?.tagName === "BUTTON" e.key === "ArrowDown" &&
focused?.nextElementSibling?.tagName === "BUTTON"
) { ) {
(focused?.nextElementSibling as HTMLButtonElement)?.focus(); (focused?.nextElementSibling as HTMLButtonElement)?.focus();
} else if ( } else if (
e.key === "ArrowUp" && e.key === "ArrowUp" &&
focused?.previousElementSibling?.tagName === focused?.previousElementSibling?.tagName === "BUTTON"
"BUTTON"
) { ) {
(focused?.previousElementSibling as HTMLButtonElement)?.focus(); (focused?.previousElementSibling as HTMLButtonElement)?.focus();
} else if (e.key === "Escape" && closeModal) { } else if (e.key === "Escape" && closeModal) {
closeModal(); closeModal();
} else if (e.key === "Escape" && running) { } else if (e.key === "Escape" && running) {
@ -3128,6 +3300,19 @@ document.addEventListener("keyup", (e) => {
e.preventDefault(); e.preventDefault();
}); });
function sample<T>(arr:T[]):T{
return arr[Math.floor(arr.length*Math.random())]
}
function getMajorityValue(arr:string[]):string{
const count = {}
arr.forEach(v=>count[v]=(count[v]||0)+1)
const max = Math.max(...Object.values(count))
return sample(Object.keys(count).filter(k=>count[k]==max))
}
fitSize(); fitSize();
restart(); restart();
tick(); tick();

View file

@ -450,9 +450,15 @@
"svg": "" "svg": ""
}, },
{ {
"name": "icon:sides_are_lava", "name": "icon:left_is_lava",
"size": 8, "size": 8,
"bricks": "r______rrttttttrrttttttrr______rr______rr____W_rr______rr_WWW__r", "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": "" "svg": ""
}, },
{ {

View file

@ -42,13 +42,13 @@ export const options = {
return window.location.search.includes("isInWebView=true"); return window.location.search.includes("isInWebView=true");
}, },
}, },
} as {[k:string]:OptionDef} } as { [k: string]: OptionDef };
export type OptionDef = { export type OptionDef = {
default: boolean; default: boolean;
name: string; name: string;
help: string; help: string;
disabled:()=>boolean disabled: () => boolean;
afterChange?:()=>void afterChange?: () => void;
} };
export type OptionId = keyof (typeof options) export type OptionId = keyof typeof options;

View file

@ -75,15 +75,31 @@ export const rawUpgrades = [
{ {
requires: "", requires: "",
threshold: 0, threshold: 0,
id: "sides_are_lava", id: "left_is_lava",
giftable: true, giftable: true,
name: "Shoot straight", name: "Avoid left side",
max: 1, max: 1,
help: (lvl) => `More coins if you don't touch the sides.`, 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 all the next bricks you break. 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 or right side. However, your combo will reset as soon as your ball hits the left side .
As soon as your combo rises, the sides become 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 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.`, of the reset conditions are met.`,
}, },
{ {
@ -94,7 +110,6 @@ export const rawUpgrades = [
name: "Sky is the limit", name: "Sky is the limit",
max: 1, max: 1,
help: (lvl) => `More coins if you don't touch the top.`, 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. 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. 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.`, The effect stacks with other combo perks.`,

View file

@ -90,6 +90,15 @@ body {
max-width: 450px; max-width: 450px;
} }
.popup.actionsAsGrid > div {
max-width: 800px;
section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
.popup > div > * { .popup > div > * {
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -100,7 +109,13 @@ body {
margin-bottom: 20px; margin-bottom: 20px;
} }
.popup > div > button { .popup > div > section {
display: flex;
flex-direction: column;
align-items: stretch;
margin-top: 20px;
button {
font: inherit; font: inherit;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.8);
color: white; color: white;
@ -111,15 +126,42 @@ body {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-top: -1px; margin-top: -1px;
}
.popup > div > button:not([disabled]):hover, &:not([disabled]):hover,
.popup > div > button:not([disabled]):focus { &:not([disabled]):focus {
border-color: #f1d33b; border-color: #f1d33b;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
&[disabled] {
/*border: 1px solid #666;*/
opacity: 0.5;
filter: saturate(0);
pointer-events: none;
}
& > div {
flex-grow: 1;
}
& > div > em {
display: block;
opacity: 0.8;
}
&.grey-out-unless-hovered {
&:not(:hover) {
opacity: 0.6;
img {
filter: saturate(0);
}
}
}
}
}
.popup button.close-modale { .popup button.close-modale {
color: white; color: white;
position: absolute; position: absolute;
@ -131,9 +173,8 @@ body {
border: none; border: none;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
}
.popup button.close-modale:before { &:before {
content: "+"; content: "+";
position: absolute; position: absolute;
transform: translate(-50%, -50%) rotate(45deg); transform: translate(-50%, -50%) rotate(45deg);
@ -143,49 +184,10 @@ body {
left: 26px; left: 26px;
} }
.popup button.close-modale:hover { &:hover {
font-weight: bold; font-weight: bold;
background: black; background: black;
} }
.popup > div > button[disabled] {
/*border: 1px solid #666;*/
opacity: 0.5;
filter: saturate(0);
pointer-events: none;
}
.popup > div > button > div {
flex-grow: 1;
}
.popup > div > button > div > em {
display: block;
opacity: 0.8;
}
.popup > div > button > span.checks {
width: 40px;
height: 40px;
display: inline-flex;
gap: 5px;
flex-grow: 0;
flex-shrink: 0;
}
.popup > div > button > span.checks > span {
flex-basis: 10px;
flex-grow: 1;
flex-shrink: 1;
/*border: 1px solid white;*/
background: white;
opacity: 0.1;
border-radius: 4px;
align-self: stretch;
}
.popup > div > button > span.checks > span.checked {
opacity: 1;
} }
.popup .textAfterButtons { .popup .textAfterButtons {
@ -289,9 +291,11 @@ body {
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
} }
.histogram > span.active > span { .histogram > span.active > span {
background: #4049ca; background: #4049ca;
} }
.histogram > span > span { .histogram > span > span {
/*Visible bar*/ /*Visible bar*/
background: #1c1c2f; background: #1c1c2f;

26
src/types.d.ts vendored
View file

@ -46,16 +46,15 @@ declare global {
} }
interface Element { interface Element {
webkitRequestFullscreen: typeof Element.requestFullscreen webkitRequestFullscreen: typeof Element.requestFullscreen;
} }
interface MediaStream { interface MediaStream {
// https://devdoc.net/web/developer.mozilla.org/en-US/docs/Web/API/CanvasCaptureMediaStream.html // https://devdoc.net/web/developer.mozilla.org/en-US/docs/Web/API/CanvasCaptureMediaStream.html
// On firefox, the capture stream has the requestFrame option // On firefox, the capture stream has the requestFrame option
// instead of the track, go figure // instead of the track, go figure
requestFrame?:()=>void requestFrame?: () => void;
} }
} }
export type BallLike = { export type BallLike = {
@ -63,7 +62,7 @@ export type BallLike = {
y: number; y: number;
vx?: number; vx?: number;
vy?: number; vy?: number;
} };
export type Coin = { export type Coin = {
points: number; points: number;
@ -81,7 +80,7 @@ export type Coin = {
weight: number; weight: number;
destroyed?: boolean; destroyed?: boolean;
coloredABrick?: boolean; coloredABrick?: boolean;
} };
export type Ball = { export type Ball = {
x: number; x: number;
previousx: number; previousx: number;
@ -94,16 +93,15 @@ export type Ball = {
sparks: number; sparks: number;
piercedSinceBounce: number; piercedSinceBounce: number;
hitSinceBounce: number; hitSinceBounce: number;
hitItem: { index: number, color: string }[]; hitItem: { index: number; color: string }[];
bouncesList?: { x: number, y: number }[]; bouncesList?: { x: number; y: number }[];
sapperUses: number; sapperUses: number;
destroyed?: boolean; destroyed?: boolean;
previousvx?: number; previousvx?: number;
previousvy?: number; previousvy?: number;
} };
export type FlashTypes = "text" | "particle" | "ball";
export type FlashTypes = "text" | "particle" | 'ball'
export type Flash = { export type Flash = {
type: FlashTypes; type: FlashTypes;
@ -118,7 +116,7 @@ export type Flash = {
vy?: number; vy?: number;
ethereal?: boolean; ethereal?: boolean;
destroyed?: boolean; destroyed?: boolean;
} };
export type RunStats = { export type RunStats = {
started: number; started: number;
@ -133,11 +131,9 @@ export type RunStats= {
upgrades_picked: number; upgrades_picked: number;
max_combo: number; max_combo: number;
max_level: number; max_level: number;
} };
export type RunHistoryItem = RunStats & { export type RunHistoryItem = RunStats & {
perks?: { [k in PerkId]: number }; perks?: { [k in PerkId]: number };
appVersion?: string; appVersion?: string;
} };