This commit is contained in:
Renan LE CARO 2025-03-19 21:58:50 +01:00
parent dce41a43ec
commit d2266de792
13 changed files with 2155 additions and 2103 deletions

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29040074 versionCode = 29040298
versionName = "29040074" versionName = "29040298"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

File diff suppressed because one or more lines are too long

6
dist/index.html vendored
View file

@ -1270,7 +1270,7 @@ const upgrades = (0, _upgrades.rawUpgrades).map((u)=>({
})); }));
},{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./upgrades":"1u3Dx"}],"iyP6E":[function(require,module,exports,__globalThis) { },{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./upgrades":"1u3Dx"}],"iyP6E":[function(require,module,exports,__globalThis) {
module.exports = JSON.parse("\"29040074\""); module.exports = JSON.parse("\"29040298\"");
},{}],"6rQoT":[function(require,module,exports,__globalThis) { },{}],"6rQoT":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
@ -2402,7 +2402,7 @@ function spawnExplosion(gameState, count, x, y, color) {
for(let i = 0; i < count; i++)makeParticle(gameState, x + (Math.random() - 0.5) * gameState.brickWidth / 2, y + (Math.random() - 0.5) * gameState.brickWidth / 2, (Math.random() - 0.5) * 30, (Math.random() - 0.5) * 30, color, false); for(let i = 0; i < count; i++)makeParticle(gameState, x + (Math.random() - 0.5) * gameState.brickWidth / 2, y + (Math.random() - 0.5) * gameState.brickWidth / 2, (Math.random() - 0.5) * 30, (Math.random() - 0.5) * 30, color, false);
} }
function explosionAt(gameState, index, x, y, ball) { function explosionAt(gameState, index, x, y, ball) {
if (gameState.bricks[index] == 'black') delete gameState.bricks[index]; if (gameState.bricks[index] == "black") delete gameState.bricks[index];
schedulGameSound(gameState, "explode", ball.x, 1); schedulGameSound(gameState, "explode", ball.x, 1);
const col = index % gameState.gridSize; const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize); const row = Math.floor(index / gameState.gridSize);
@ -2710,7 +2710,7 @@ frames = 1) {
if (!gameState.perks.etherealcoins) { if (!gameState.perks.etherealcoins) {
const flip = gameState.perks.helium > 0 && Math.abs(coin.x - gameState.puckPosition) * 2 > gameState.puckWidth + coin.size; const flip = gameState.perks.helium > 0 && Math.abs(coin.x - gameState.puckPosition) * 2 > gameState.puckWidth + coin.size;
coin.vy += frames * coin.weight * 0.8 * (flip ? -1 : 1); coin.vy += frames * coin.weight * 0.8 * (flip ? -1 : 1);
if (flip && !(0, _options.isOptionOn)('basic') && Math.random() < 0.1) makeParticle(gameState, coin.x, coin.y, 0, gameState.baseSpeed, rainbowColor(), true, 5, 250); if (flip && !(0, _options.isOptionOn)("basic") && Math.random() < 0.1) makeParticle(gameState, coin.x, coin.y, 0, gameState.baseSpeed, rainbowColor(), true, 5, 250);
} }
const speed = Math.abs(coin.sx) + Math.abs(coin.sx); const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames); const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);

View file

@ -1,5 +1,5 @@
// The version of the cache. // The version of the cache.
const VERSION = "29040074"; const VERSION = "29040298";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -1 +1 @@
"29040074" "29040298"

View file

@ -1,4 +1,4 @@
import {allLevels, appVersion, icons, upgrades} from "./loadGameData"; import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
import { import {
Ball, Ball,
Coin, Coin,
@ -11,12 +11,17 @@ import {
TextFlash, TextFlash,
Upgrade, Upgrade,
} from "./types"; } from "./types";
import {getAudioContext, playPendingSounds} from "./sounds"; import { getAudioContext, playPendingSounds } from "./sounds";
import {currentLevelInfo, getRowColIndex, max_levels, pickedUpgradesHTMl,} from "./game_utils"; import {
currentLevelInfo,
getRowColIndex,
max_levels,
pickedUpgradesHTMl,
} from "./game_utils";
import "./PWA/sw_loader"; import "./PWA/sw_loader";
import {getCurrentLang, t} from "./i18n/i18n"; import { getCurrentLang, t } from "./i18n/i18n";
import {getSettingValue, getTotalScore, setSettingValue} from "./settings"; import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import { import {
forEachLiveOne, forEachLiveOne,
gameStateTick, gameStateTick,
@ -25,12 +30,28 @@ import {
setLevel, setLevel,
setMousePos, setMousePos,
} from "./gameStateMutators"; } from "./gameStateMutators";
import {backgroundCanvas, ctx, gameCanvas, render, scoreDisplay,} from "./render"; import {
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording"; backgroundCanvas,
import {newGameState} from "./newGameState"; ctx,
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal,} from "./asyncAlert"; gameCanvas,
import {isOptionOn, options, toggleOption} from "./options"; render,
import {hashCode} from "./getLevelBackground"; scoreDisplay,
} from "./render";
import {
pauseRecording,
recordOneFrame,
resumeRecording,
startRecordingGame,
} from "./recording";
import { newGameState } from "./newGameState";
import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground";
export function play() { export function play() {
if (gameState.running) return; if (gameState.running) return;
@ -406,8 +427,7 @@ async function openScorePanel() {
async function openSettingsPanel() { async function openSettingsPanel() {
pause(true); pause(true);
const actions: AsyncAlertAction<() => void>[] = [ const actions: AsyncAlertAction<() => void>[] = [];
];
for (const key of Object.keys(options) as OptionId[]) { for (const key of Object.keys(options) as OptionId[]) {
if (options[key]) if (options[key])
@ -449,21 +469,18 @@ async function openSettingsPanel() {
}); });
} }
} }
actions.push( actions.push({
{
text: t("main_menu.resume"), text: t("main_menu.resume"),
help: t("main_menu.resume_help"), help: t("main_menu.resume_help"),
value() {}, value() {},
}) });
actions.push({ actions.push({
text: t("main_menu.unlocks"), text: t("main_menu.unlocks"),
help: t("main_menu.unlocks_help"), help: t("main_menu.unlocks_help"),
value() { value() {
openUnlocksList(); openUnlocksList();
}, },
}) });
actions.push({ actions.push({
text: t("sandbox.title"), text: t("sandbox.title"),

View file

@ -23,18 +23,27 @@ import {
getRowColIndex, getRowColIndex,
isTelekinesisActive, isTelekinesisActive,
isYoyoActive, isYoyoActive,
max_levels, sample, max_levels,
sample,
shouldPierceByColor, shouldPierceByColor,
} from "./game_utils"; } from "./game_utils";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {icons} from "./loadGameData"; import { icons } from "./loadGameData";
import {addToTotalScore} from "./settings"; import { addToTotalScore } from "./settings";
import {background} from "./render"; import { background } from "./render";
import {gameOver} from "./gameOver"; import { gameOver } from "./gameOver";
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openUpgradesPicker, pause,} from "./game"; import {
import {stopRecording} from "./recording"; brickIndex,
import {isOptionOn} from "./options"; fitSize,
gameState,
hasBrick,
hitsSomething,
openUpgradesPicker,
pause,
} from "./game";
import { stopRecording } from "./recording";
import { isOptionOn } from "./options";
export function setMousePos(gameState: GameState, x: number) { export function setMousePos(gameState: GameState, x: number) {
// Sets the puck position, and updates the ball position if they are supposed to follow it // Sets the puck position, and updates the ball position if they are supposed to follow it
@ -227,12 +236,11 @@ export function spawnExplosion(
export function explosionAt( export function explosionAt(
gameState: GameState, gameState: GameState,
index: number, index: number,
x: number, y: number, x: number,
ball: Ball) { y: number,
ball: Ball,
if(gameState.bricks[index]=='black') ) {
delete gameState.bricks[index]; if (gameState.bricks[index] == "black") delete gameState.bricks[index];
schedulGameSound(gameState, "explode", ball.x, 1); schedulGameSound(gameState, "explode", ball.x, 1);
@ -276,8 +284,8 @@ export function explosionAt(
); );
gameState.runStatistics.bricks_broken++; gameState.runStatistics.bricks_broken++;
if(gameState.perks.zen){ if (gameState.perks.zen) {
resetCombo(gameState, x,y) resetCombo(gameState, x, y);
} }
} }
@ -293,10 +301,7 @@ export function explodeBrick(
if (color === "black") { if (color === "black") {
const x = brickCenterX(gameState, index), const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index); y = brickCenterY(gameState, index);
explosionAt(gameState, explosionAt(gameState, index, x, y, ball);
index, x, y,
ball)
} else if (color) { } else if (color) {
// Even if it bounces we don't want to count that as a miss // Even if it bounces we don't want to count that as a miss
@ -328,7 +333,7 @@ export function explodeBrick(
while (coinsToSpawn > 0) { while (coinsToSpawn > 0) {
const points = Math.min(pointsPerCoin, coinsToSpawn); const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) { if (points < 0 || isNaN(points)) {
console.error({points}); console.error({ points });
debugger; debugger;
} }
@ -351,7 +356,8 @@ export function explodeBrick(
); );
} }
gameState.combo += gameState.perks.streak_shots + gameState.combo +=
gameState.perks.streak_shots +
gameState.perks.compound_interest + gameState.perks.compound_interest +
gameState.perks.left_is_lava + gameState.perks.left_is_lava +
gameState.perks.right_is_lava + gameState.perks.right_is_lava +
@ -359,9 +365,7 @@ export function explodeBrick(
gameState.perks.picky_eater + gameState.perks.picky_eater +
gameState.perks.asceticism + gameState.perks.asceticism +
gameState.perks.zen + gameState.perks.zen +
gameState.perks.unbounded gameState.perks.unbounded;
;
if (!isExplosion) { if (!isExplosion) {
// color change // color change
@ -475,7 +479,7 @@ export function addToScore(gameState: GameState, coin: Coin) {
} }
gameState.runStatistics.score += coin.points; gameState.runStatistics.score += coin.points;
if (gameState.perks.asceticism) { if (gameState.perks.asceticism) {
resetCombo(gameState, coin.x, coin.y) resetCombo(gameState, coin.x, coin.y);
} }
} }
@ -498,7 +502,7 @@ export async function setLevel(gameState: GameState, l: number) {
// Reset combo silently // Reset combo silently
if (!gameState.perks.shunt) { if (!gameState.perks.shunt) {
gameState.combo = baseCombo(gameState) gameState.combo = baseCombo(gameState);
} }
gameState.combo += gameState.perks.hot_start * 15; gameState.combo += gameState.perks.hot_start * 15;
@ -630,10 +634,10 @@ export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
} }
export function coinBrickHitCheck(gameState: GameState, coin: Coin) { export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
if(gameState.perks.ghost_coins)return undefined if (gameState.perks.ghost_coins) return undefined;
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const radius = coin.size / 2; const radius = coin.size / 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);
const hhit = hitsSomething(x, previousY, radius); const hhit = hitsSomething(x, previousY, radius);
@ -697,7 +701,10 @@ export function bordersHitCheck(
let vhit = 0, let vhit = 0,
hhit = 0; hhit = 0;
if (coin.x < gameState.offsetXRoundedDown + radius && !gameState.perks.unbounded) { if (
coin.x < gameState.offsetXRoundedDown + radius &&
!gameState.perks.unbounded
) {
coin.x = coin.x =
gameState.offsetXRoundedDown + gameState.offsetXRoundedDown +
radius + radius +
@ -710,7 +717,10 @@ export function bordersHitCheck(
coin.vy *= -1; coin.vy *= -1;
vhit = 1; vhit = 1;
} }
if (coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius && !gameState.perks.unbounded) { if (
coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius &&
!gameState.perks.unbounded
) {
coin.x = coin.x =
gameState.canvasWidth - gameState.canvasWidth -
gameState.offsetXRoundedDown - gameState.offsetXRoundedDown -
@ -765,15 +775,19 @@ export function gameStateTick(
gameState.autoCleanUses++; gameState.autoCleanUses++;
} }
if (!remainingBricks && gameState.noBricksSince == 0) { if (!remainingBricks && gameState.noBricksSince == 0) {
gameState.noBricksSince ||= gameState.levelTime gameState.noBricksSince ||= gameState.levelTime;
} }
if (!remainingBricks && (!liveCount(gameState.coins) || (gameState.levelTime > gameState.noBricksSince + 5000))) { if (
!remainingBricks &&
(!liveCount(gameState.coins) ||
gameState.levelTime > gameState.noBricksSince + 5000)
) {
if (gameState.currentLevel + 1 < max_levels(gameState)) { if (gameState.currentLevel + 1 < max_levels(gameState)) {
setLevel(gameState, gameState.currentLevel + 1); setLevel(gameState, gameState.currentLevel + 1);
} else { } else {
gameOver( gameOver(
t("gameOver.win.title"), t("gameOver.win.title"),
t("gameOver.win.summary", {score: gameState.score}), t("gameOver.win.summary", { score: gameState.score }),
); );
} }
} else if (gameState.running || gameState.levelTime) { } else if (gameState.running || gameState.levelTime) {
@ -781,25 +795,30 @@ export function gameStateTick(
forEachLiveOne(gameState.coins, (coin, coinIndex) => { forEachLiveOne(gameState.coins, (coin, coinIndex) => {
if (gameState.perks.coin_magnet) { if (gameState.perks.coin_magnet) {
const strength = 100 / (100 + const strength =
(100 /
(100 +
Math.pow(coin.y - gameState.gameZoneHeight, 2) + Math.pow(coin.y - gameState.gameZoneHeight, 2) +
Math.pow(coin.x - gameState.puckPosition, 2)) * Math.pow(coin.x - gameState.puckPosition, 2))) *
gameState.perks.coin_magnet; gameState.perks.coin_magnet;
const attractionX = frames * (gameState.puckPosition - coin.x) * strength const attractionX =
frames * (gameState.puckPosition - coin.x) * strength;
coin.vx += attractionX; coin.vx += attractionX;
coin.vy += frames * (gameState.gameZoneHeight - coin.y) * strength / 2 coin.vy +=
(frames * (gameState.gameZoneHeight - coin.y) * strength) / 2;
coin.sa -= attractionX / 10; coin.sa -= attractionX / 10;
} }
if(gameState.perks.ball_attracts_coins){ if (gameState.perks.ball_attracts_coins) {
gameState.balls.forEach(ball=>{ gameState.balls.forEach((ball) => {
const d2= distance2(ball, coin) const d2 = distance2(ball, coin);
coin.vx+=(ball.x-coin.x)/d2*30*gameState.perks.ball_attracts_coins coin.vx +=
coin.vy+=(ball.y-coin.y)/d2*30*gameState.perks.ball_attracts_coins ((ball.x - coin.x) / d2) * 30 * gameState.perks.ball_attracts_coins;
}) coin.vy +=
((ball.y - coin.y) / d2) * 30 * gameState.perks.ball_attracts_coins;
});
} }
const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames; const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames;
@ -816,10 +835,14 @@ export function gameStateTick(
// Gravity // Gravity
if (!gameState.perks.etherealcoins) { if (!gameState.perks.etherealcoins) {
const flip = gameState.perks.helium > 0 && Math.abs(coin.x - gameState.puckPosition) * 2 > gameState.puckWidth + coin.size const flip =
gameState.perks.helium > 0 &&
Math.abs(coin.x - gameState.puckPosition) * 2 >
gameState.puckWidth + coin.size;
coin.vy += frames * coin.weight * 0.8 * (flip ? -1 : 1); coin.vy += frames * coin.weight * 0.8 * (flip ? -1 : 1);
if (flip && !isOptionOn('basic') && Math.random() < 0.1) { if (flip && !isOptionOn("basic") && Math.random() < 0.1) {
makeParticle(gameState, makeParticle(
gameState,
coin.x, coin.x,
coin.y, coin.y,
0, 0,
@ -827,10 +850,9 @@ export function gameStateTick(
rainbowColor(), rainbowColor(),
true, true,
5, 5,
250 250,
) );
} }
} }
const speed = Math.abs(coin.sx) + Math.abs(coin.sx); const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
@ -851,12 +873,14 @@ export function gameStateTick(
if (gameState.perks.compound_interest) { if (gameState.perks.compound_interest) {
resetCombo(gameState, coin.x, coin.y); resetCombo(gameState, coin.x, coin.y);
} }
} else if (gameState.perks.unbounded && (coin.x < -50 || coin.x > gameState.canvasWidth + 50)) { } else if (
gameState.perks.unbounded &&
(coin.x < -50 || coin.x > gameState.canvasWidth + 50)
) {
// Out of bound on sides // Out of bound on sides
destroy(gameState.coins, coinIndex); destroy(gameState.coins, coinIndex);
} }
const hitBrick = coinBrickHitCheck(gameState, coin); const hitBrick = coinBrickHitCheck(gameState, coin);
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
@ -891,29 +915,41 @@ export function gameStateTick(
if (gameState.perks.shocks) { if (gameState.perks.shocks) {
gameState.balls.forEach((a, ai) => gameState.balls.forEach((a, ai) =>
gameState.balls.forEach((b, bi) => { gameState.balls.forEach((b, bi) => {
if (ai < bi && !a.destroyed && !b.destroyed && distance2(a, b) < gameState.ballSize * gameState.ballSize) { if (
let tempVx = a.vx ai < bi &&
let tempVy = a.vy !a.destroyed &&
a.vx = b.vx !b.destroyed &&
a.vy = b.vy distance2(a, b) < gameState.ballSize * gameState.ballSize
b.vx = tempVx ) {
b.vy = tempVy let tempVx = a.vx;
let tempVy = a.vy;
a.vx = b.vx;
a.vy = b.vy;
b.vx = tempVx;
b.vy = tempVy;
let x = (a.x + b.x) / 2 let x = (a.x + b.x) / 2;
let y = (a.y + b.y) / 2 let y = (a.y + b.y) / 2;
const limit = gameState.baseSpeed const limit = gameState.baseSpeed;
a.vx += clamp(a.x - x, -limit, limit)+(Math.random()-0.5) * limit/3 a.vx +=
a.vy += clamp(a.y - y, -limit, limit)+(Math.random()-0.5) * limit/3 clamp(a.x - x, -limit, limit) +
b.vx += clamp(b.x - x, -limit, limit)+(Math.random()-0.5) * limit/3 ((Math.random() - 0.5) * limit) / 3;
b.vy += clamp(b.y - y, -limit, limit)+(Math.random()-0.5) * limit/3 a.vy +=
clamp(a.y - y, -limit, limit) +
((Math.random() - 0.5) * limit) / 3;
b.vx +=
clamp(b.x - x, -limit, limit) +
((Math.random() - 0.5) * limit) / 3;
b.vy +=
clamp(b.y - y, -limit, limit) +
((Math.random() - 0.5) * limit) / 3;
let index = brickIndex(x, y) let index = brickIndex(x, y);
explosionAt(gameState, index, x, y, a) explosionAt(gameState, index, x, y, a);
} }
} }),
) );
)
} }
if (gameState.perks.wind) { if (gameState.perks.wind) {
@ -1081,9 +1117,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
if (isYoyoActive(gameState, ball)) { if (isYoyoActive(gameState, ball)) {
speedLimitDampener += 3; speedLimitDampener += 3;
ball.vx += ball.vx +=
((gameState.puckPosition - ball.x) / 1000) * ((gameState.puckPosition - ball.x) / 1000) * delta * gameState.perks.yoyo;
delta *
gameState.perks.yoyo;
} }
if ( if (
@ -1143,7 +1177,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
i < ball.hitItem?.length - 1 && i < gameState.perks.respawn; i < ball.hitItem?.length - 1 && i < gameState.perks.respawn;
i++ i++
) { ) {
const {index, color} = ball.hitItem[i]; const { index, color } = ball.hitItem[i];
if (gameState.bricks[index] || color === "black") continue; if (gameState.bricks[index] || color === "black") continue;
const vertical = Math.random() > 0.5; const vertical = Math.random() > 0.5;
const dx = Math.random() > 0.5 ? 1 : -1; const dx = Math.random() > 0.5 ? 1 : -1;
@ -1163,7 +1197,12 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
} }
} }
const borderHitCode = bordersHitCheck(gameState, ball, gameState.ballSize / 2, delta); const borderHitCode = bordersHitCheck(
gameState,
ball,
gameState.ballSize / 2,
delta,
);
if (borderHitCode) { if (borderHitCode) {
if ( if (
gameState.perks.left_is_lava && gameState.perks.left_is_lava &&
@ -1216,46 +1255,43 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
} else { } else {
ball.vy *= -1; ball.vy *= -1;
gameState.perks.extra_life -= 1 gameState.perks.extra_life -= 1;
if(gameState.perks.extra_life <0){ if (gameState.perks.extra_life < 0) {
gameState.perks.extra_life=0 gameState.perks.extra_life = 0;
}else if(gameState.perks.sacrifice){ } else if (gameState.perks.sacrifice) {
if(liveCount(gameState.coins)<gameState.MAX_COINS/2){ if (liveCount(gameState.coins) < gameState.MAX_COINS / 2) {
// true duplication // true duplication
let remaining = liveCount(gameState.coins) let remaining = liveCount(gameState.coins);
forEachLiveOne(gameState.coins, (source, index)=>{ forEachLiveOne(gameState.coins, (source, index) => {
if(!remaining) return if (!remaining) return;
append(gameState.coins, copy=>{ append(gameState.coins, (copy) => {
copy.points=source.points copy.points = source.points;
copy.color=source.color copy.color = source.color;
copy.x=source.x copy.x = source.x;
copy.y=source.y copy.y = source.y;
copy.size=source.size copy.size = source.size;
copy.previousX=source.previousX copy.previousX = source.previousX;
copy.previousY=source.previousY copy.previousY = source.previousY;
copy.vx=-source.vx copy.vx = -source.vx;
copy.vy=-source.vy copy.vy = -source.vy;
copy.sx=source.sx copy.sx = source.sx;
copy.sy=source.sy copy.sy = source.sy;
copy.a=source.a copy.a = source.a;
copy.sa=-source.sa copy.sa = -source.sa;
copy.weight=source.weight copy.weight = source.weight;
copy.coloredABrick=source.coloredABrick copy.coloredABrick = source.coloredABrick;
}) });
remaining-- remaining--;
}) });
}else{ } else {
forEachLiveOne(gameState.coins, (source, index)=>{ forEachLiveOne(gameState.coins, (source, index) => {
source.points*=2 source.points *= 2;
}) });
// spawn a few coins for effect, but mostly increment poitns counter // spawn a few coins for effect, but mostly increment poitns counter
} }
} }
schedulGameSound(gameState, "lifeLost", ball.x, 1); schedulGameSound(gameState, "lifeLost", ball.x, 1);
if (!isOptionOn("basic")) { if (!isOptionOn("basic")) {
for (let i = 0; i < 10; i++) for (let i = 0; i < 10; i++)
@ -1276,16 +1312,15 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
resetCombo(gameState, ball.x, ball.y); resetCombo(gameState, ball.x, ball.y);
} }
if (gameState.perks.trampoline) { if (gameState.perks.trampoline) {
gameState.combo+=gameState.perks.trampoline gameState.combo += gameState.perks.trampoline;
} }
if (gameState.perks.nbricks) { if (gameState.perks.nbricks) {
if (ball.hitSinceBounce) { if (ball.hitSinceBounce) {
if (gameState.perks.nbricks === ball.hitSinceBounce) { if (gameState.perks.nbricks === ball.hitSinceBounce) {
gameState.combo += gameState.perks.nbricks gameState.combo += gameState.perks.nbricks;
} else { } else {
resetCombo(gameState, ball.x, ball.y) resetCombo(gameState, ball.x, ball.y);
} }
} }
} }
@ -1293,7 +1328,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
ball.hitItem ball.hitItem
.slice(0, -1) .slice(0, -1)
.slice(0, gameState.perks.respawn) .slice(0, gameState.perks.respawn)
.forEach(({index, color}) => { .forEach(({ index, color }) => {
if (!gameState.bricks[index] && color !== "black") if (!gameState.bricks[index] && color !== "black")
gameState.bricks[index] = color; gameState.bricks[index] = color;
}); });
@ -1302,12 +1337,13 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
if (!ball.hitSinceBounce) { if (!ball.hitSinceBounce) {
gameState.runStatistics.misses++; gameState.runStatistics.misses++;
gameState.levelMisses++; gameState.levelMisses++;
if(gameState.perks.forgiving){ if (gameState.perks.forgiving) {
const indexes = gameState.bricks.map((b,i)=>b ? i:-1) const indexes = gameState.bricks
.filter(i=>i>-1) .map((b, i) => (b ? i : -1))
const pick = sample(indexes) .filter((i) => i > -1);
explodeBrick(gameState,pick, ball, false) const pick = sample(indexes);
}else{ explodeBrick(gameState, pick, ball, false);
} else {
resetCombo(gameState, ball.x, ball.y); resetCombo(gameState, ball.x, ball.y);
} }
makeText( makeText(
@ -1326,23 +1362,25 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
ball.piercedSinceBounce = 0; ball.piercedSinceBounce = 0;
} }
const lostOnSides = gameState.perks.unbounded && ball.x < 50 || ball.x > gameState.canvasWidth + 50 const lostOnSides =
if (gameState.running && (gameState.perks.unbounded && ball.x < 50) ||
ball.x > gameState.canvasWidth + 50;
if (
gameState.running &&
(ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides) (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides)
) { ) {
ball.destroyed = true; ball.destroyed = true;
gameState.runStatistics.balls_lost++; gameState.runStatistics.balls_lost++;
if (!gameState.balls.find((b) => !b.destroyed)) { if (!gameState.balls.find((b) => !b.destroyed)) {
gameOver( gameOver(
t("gameOver.lost.title"), t("gameOver.lost.title"),
t("gameOver.lost.summary", {score: gameState.score}), t("gameOver.lost.summary", { score: gameState.score }),
); );
} }
} }
const radius = gameState.ballSize / 2; const radius = gameState.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;
const vhit = hitsSomething(previousX, y, radius); const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius); const hhit = hitsSomething(x, previousY, radius);
@ -1542,7 +1580,7 @@ export function append<T>(
makeItem(where.list[where.indexMin]); makeItem(where.list[where.indexMin]);
where.indexMin++; where.indexMin++;
} else { } else {
const p = {destroyed: false}; const p = { destroyed: false };
makeItem(p); makeItem(p);
where.list.push(p); where.list.push(p);
} }

View file

@ -1,5 +1,5 @@
import {Ball, GameState, PerkId, PerksMap} from "./types"; import { Ball, GameState, PerkId, PerksMap } from "./types";
import {icons, upgrades} from "./loadGameData"; import { icons, upgrades } from "./loadGameData";
export function getMajorityValue(arr: string[]): string { export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {}; const count: { [k: string]: number } = {};
@ -103,9 +103,8 @@ export function distanceBetween(
return Math.sqrt(distance2(a, b)); return Math.sqrt(distance2(a, b));
} }
export function clamp(value: number, min: number, max: number) {
export function clamp(value, min, max){ return Math.max(min, Math.min(value, max));
return Math.max(min, Math.min(value, max))
} }
export function defaultSounds() { export function defaultSounds() {
return { return {

View file

@ -1,4 +1,4 @@
import {baseCombo, forEachLiveOne, liveCount} from "./gameStateMutators"; import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
import { import {
brickCenterX, brickCenterX,
brickCenterY, brickCenterY,
@ -7,10 +7,10 @@ import {
isYoyoActive, isYoyoActive,
max_levels, max_levels,
} from "./game_utils"; } from "./game_utils";
import {colorString, GameState} from "./types"; import { colorString, GameState } from "./types";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {gameState} from "./game"; import { gameState } from "./game";
import {isOptionOn} from "./options"; import { isOptionOn } from "./options";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement; export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
export const ctx = gameCanvas.getContext("2d", { export const ctx = gameCanvas.getContext("2d", {
@ -29,7 +29,7 @@ export const backgroundCanvas = document.createElement("canvas");
export function render(gameState: GameState) { export function render(gameState: GameState) {
const level = currentLevelInfo(gameState); const level = currentLevelInfo(gameState);
const {width, height} = gameCanvas; const { width, height } = gameCanvas;
if (!width || !height) return; if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) { if (gameState.currentLevel || gameState.levelTime) {
@ -82,13 +82,13 @@ export function render(gameState: GameState) {
}); });
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
forEachLiveOne(gameState.lights, (flash) => { forEachLiveOne(gameState.lights, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawFuzzyBall(ctx, color, size, x, y); drawFuzzyBall(ctx, color, size, x, y);
}); });
forEachLiveOne(gameState.particles, (flash) => { forEachLiveOne(gameState.particles, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawFuzzyBall(ctx, color, size * 3, x, y); drawFuzzyBall(ctx, color, size * 3, x, y);
@ -131,7 +131,7 @@ export function render(gameState: GameState) {
ctx.fillStyle = level.color || "#000"; ctx.fillStyle = level.color || "#000";
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
forEachLiveOne(gameState.particles, (flash) => { forEachLiveOne(gameState.particles, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawBall(ctx, color, size, x, y); drawBall(ctx, color, size, x, y);
@ -194,7 +194,7 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.texts, (flash) => { forEachLiveOne(gameState.texts, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
@ -202,7 +202,7 @@ export function render(gameState: GameState) {
}); });
forEachLiveOne(gameState.particles, (particle) => { forEachLiveOne(gameState.particles, (particle) => {
const {x, y, time, color, size, duration} = particle; const { x, y, time, color, size, duration } = particle;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
@ -218,7 +218,9 @@ export function render(gameState: GameState) {
ctx.fillRect( ctx.fillRect(
gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown, gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown,
gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i,
gameState.perks.unbounded ? gameState.canvasWidth : gameState.gameZoneWidthRoundedUp, gameState.perks.unbounded
? gameState.canvasWidth
: gameState.gameZoneWidthRoundedUp,
1, 1,
); );
} }
@ -240,7 +242,7 @@ export function render(gameState: GameState) {
if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) { if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) {
ctx.strokeStyle = gameState.puckColor; ctx.strokeStyle = gameState.puckColor;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight ); ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
ctx.bezierCurveTo( ctx.bezierCurveTo(
gameState.puckPosition, gameState.puckPosition,
gameState.gameZoneHeight, gameState.gameZoneHeight,
@ -251,9 +253,6 @@ export function render(gameState: GameState) {
); );
ctx.stroke(); ctx.stroke();
} }
}); });
// The puck // The puck
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
@ -318,7 +317,7 @@ export function render(gameState: GameState) {
const hasCombo = gameState.combo > baseCombo(gameState); const hasCombo = gameState.combo > baseCombo(gameState);
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = gameState.perks.unbounded ? 0.1 : 1 ctx.globalAlpha = gameState.perks.unbounded ? 0.1 : 1;
if (gameState.offsetXRoundedDown) { if (gameState.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 = ctx.fillStyle =

2
src/types.d.ts vendored
View file

@ -244,7 +244,7 @@ export type GameState = {
runStatistics: RunStats; runStatistics: RunStats;
lastOffered: Partial<{ [k in PerkId]: number }>; lastOffered: Partial<{ [k in PerkId]: number }>;
levelTime: number; levelTime: number;
noBricksSince: number ; noBricksSince: number;
levelWallBounces: number; levelWallBounces: number;
autoCleanUses: number; autoCleanUses: number;
aboutToPlaySound: { aboutToPlaySound: {

View file

@ -1,11 +1,11 @@
import _rawLevelsList from "./data/levels.json"; import _rawLevelsList from "./data/levels.json";
import {rawUpgrades} from "./upgrades"; import { rawUpgrades } from "./upgrades";
describe("rawUpgrades", ()=>{ describe("rawUpgrades", () => {
it("has an icon for each upgrade", () => {
it('has an icon for each upgrade',()=>{ const missingIcon = rawUpgrades
const missingIcon = rawUpgrades.map(u=>u.id).filter(id=>!_rawLevelsList.find(l=>l.name === 'icon:'+id)) .map((u) => u.id)
expect(missingIcon.join(', ')).toEqual('') .filter((id) => !_rawLevelsList.find((l) => l.name === "icon:" + id));
}) expect(missingIcon.join(", ")).toEqual("");
}) });
});

View file

@ -383,7 +383,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
threshold:70000, threshold: 70000,
giftable: false, giftable: false,
id: "asceticism", id: "asceticism",
max: 1, max: 1,
@ -393,7 +393,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
threshold:75000, threshold: 75000,
giftable: false, giftable: false,
id: "unbounded", id: "unbounded",
max: 1, max: 1,
@ -403,7 +403,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
threshold:80000, threshold: 80000,
giftable: false, giftable: false,
id: "shunt", id: "shunt",
max: 1, max: 1,
@ -413,7 +413,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
threshold:85000, threshold: 85000,
giftable: false, giftable: false,
id: "yoyo", id: "yoyo",
max: 2, max: 2,
@ -423,17 +423,17 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
threshold:90000, threshold: 90000,
giftable: false, giftable: false,
id: "nbricks", id: "nbricks",
max: 3, max: 3,
name: t("upgrades.nbricks.name"), name: t("upgrades.nbricks.name"),
help: (lvl: number) => t("upgrades.nbricks.help",{lvl}), help: (lvl: number) => t("upgrades.nbricks.help", { lvl }),
fullHelp: t("upgrades.nbricks.fullHelp"), fullHelp: t("upgrades.nbricks.fullHelp"),
}, },
{ {
requires: "", requires: "",
threshold:95000, threshold: 95000,
giftable: false, giftable: false,
id: "etherealcoins", id: "etherealcoins",
max: 1, max: 1,
@ -443,7 +443,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "multiball", requires: "multiball",
threshold:100000, threshold: 100000,
giftable: false, giftable: false,
id: "shocks", id: "shocks",
max: 1, max: 1,
@ -453,7 +453,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
threshold:105000, threshold: 105000,
giftable: false, giftable: false,
id: "zen", id: "zen",
max: 1, max: 1,
@ -463,7 +463,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "extra_life", requires: "extra_life",
threshold:110000, threshold: 110000,
giftable: false, giftable: false,
id: "sacrifice", id: "sacrifice",
max: 1, max: 1,
@ -474,27 +474,27 @@ export const rawUpgrades = [
{ {
requires: "", requires: "",
threshold:115000, threshold: 115000,
giftable: false, giftable: false,
id: "trampoline", id: "trampoline",
max: 3, max: 3,
name: t("upgrades.trampoline.name"), name: t("upgrades.trampoline.name"),
help: (lvl: number) => t("upgrades.trampoline.help",{lvl}), help: (lvl: number) => t("upgrades.trampoline.help", { lvl }),
fullHelp: t("upgrades.trampoline.fullHelp"), fullHelp: t("upgrades.trampoline.fullHelp"),
}, },
{ {
requires: "", requires: "",
threshold:120000, threshold: 120000,
giftable: false, giftable: false,
id: "ghost_coins", id: "ghost_coins",
max: 1, max: 1,
name: t("upgrades.ghost_coins.name"), name: t("upgrades.ghost_coins.name"),
help: (lvl: number) => t("upgrades.ghost_coins.help",{lvl}), help: (lvl: number) => t("upgrades.ghost_coins.help", { lvl }),
fullHelp: t("upgrades.ghost_coins.fullHelp"), fullHelp: t("upgrades.ghost_coins.fullHelp"),
}, },
{ {
requires: "", requires: "",
threshold:125000, threshold: 125000,
giftable: false, giftable: false,
id: "forgiving", id: "forgiving",
max: 1, max: 1,
@ -504,7 +504,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
threshold:130000, threshold: 130000,
giftable: false, giftable: false,
id: "ball_attracts_coins", id: "ball_attracts_coins",
max: 3, max: 3,
@ -512,5 +512,4 @@ export const rawUpgrades = [
help: (lvl: number) => t("upgrades.ball_attracts_coins.help"), help: (lvl: number) => t("upgrades.ball_attracts_coins.help"),
fullHelp: t("upgrades.ball_attracts_coins.fullHelp"), fullHelp: t("upgrades.ball_attracts_coins.fullHelp"),
}, },
] as const; ] as const;