diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 72f1e72..e05b412 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
- versionCode = 29040074
- versionName = "29040074"
+ versionCode = 29040298
+ versionName = "29040298"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html
index 2185742..e26198d 100644
--- a/app/src/main/assets/index.html
+++ b/app/src/main/assets/index.html
@@ -1 +1 @@
-
Breakout 71
\ No newline at end of file
+Breakout 71
\ No newline at end of file
diff --git a/dist/index.html b/dist/index.html
index 563ee4e..f5833b6 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -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) {
-module.exports = JSON.parse("\"29040074\"");
+module.exports = JSON.parse("\"29040298\"");
},{}],"6rQoT":[function(require,module,exports,__globalThis) {
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);
}
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);
const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize);
@@ -2710,7 +2710,7 @@ frames = 1) {
if (!gameState.perks.etherealcoins) {
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);
- 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 hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
diff --git a/src/PWA/sw-b71.js b/src/PWA/sw-b71.js
index 60a6c38..5f284a4 100644
--- a/src/PWA/sw-b71.js
+++ b/src/PWA/sw-b71.js
@@ -1,5 +1,5 @@
// The version of the cache.
-const VERSION = "29040074";
+const VERSION = "29040298";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;
diff --git a/src/data/levels.json b/src/data/levels.json
index ac4a265..20ebb88 100644
--- a/src/data/levels.json
+++ b/src/data/levels.json
@@ -940,4 +940,4 @@
"svg": null,
"color": ""
}
-]
\ No newline at end of file
+]
diff --git a/src/data/version.json b/src/data/version.json
index 06ae602..e13484e 100644
--- a/src/data/version.json
+++ b/src/data/version.json
@@ -1 +1 @@
-"29040074"
+"29040298"
diff --git a/src/game.ts b/src/game.ts
index 6444d32..cefd415 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1,4 +1,4 @@
-import {allLevels, appVersion, icons, upgrades} from "./loadGameData";
+import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
import {
Ball,
Coin,
@@ -11,12 +11,17 @@ import {
TextFlash,
Upgrade,
} from "./types";
-import {getAudioContext, playPendingSounds} from "./sounds";
-import {currentLevelInfo, getRowColIndex, max_levels, pickedUpgradesHTMl,} from "./game_utils";
+import { getAudioContext, playPendingSounds } from "./sounds";
+import {
+ currentLevelInfo,
+ getRowColIndex,
+ max_levels,
+ pickedUpgradesHTMl,
+} from "./game_utils";
import "./PWA/sw_loader";
-import {getCurrentLang, t} from "./i18n/i18n";
-import {getSettingValue, getTotalScore, setSettingValue} from "./settings";
+import { getCurrentLang, t } from "./i18n/i18n";
+import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import {
forEachLiveOne,
gameStateTick,
@@ -25,12 +30,28 @@ import {
setLevel,
setMousePos,
} from "./gameStateMutators";
-import {backgroundCanvas, ctx, gameCanvas, render, 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";
+import {
+ backgroundCanvas,
+ ctx,
+ gameCanvas,
+ render,
+ 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() {
if (gameState.running) return;
@@ -406,8 +427,7 @@ async function openScorePanel() {
async function openSettingsPanel() {
pause(true);
- const actions: AsyncAlertAction<() => void>[] = [
- ];
+ const actions: AsyncAlertAction<() => void>[] = [];
for (const key of Object.keys(options) as OptionId[]) {
if (options[key])
@@ -449,21 +469,18 @@ async function openSettingsPanel() {
});
}
}
- actions.push(
- {
- text: t("main_menu.resume"),
- help: t("main_menu.resume_help"),
- value() {},
- })
actions.push({
- text: t("main_menu.unlocks"),
- help: t("main_menu.unlocks_help"),
- value() {
- openUnlocksList();
- },
- })
-
-
+ text: t("main_menu.resume"),
+ help: t("main_menu.resume_help"),
+ value() {},
+ });
+ actions.push({
+ text: t("main_menu.unlocks"),
+ help: t("main_menu.unlocks_help"),
+ value() {
+ openUnlocksList();
+ },
+ });
actions.push({
text: t("sandbox.title"),
diff --git a/src/gameStateMutators.ts b/src/gameStateMutators.ts
index 343b869..538036e 100644
--- a/src/gameStateMutators.ts
+++ b/src/gameStateMutators.ts
@@ -1,1578 +1,1616 @@
import {
- Ball,
- BallLike,
- Coin,
- colorString,
- GameState,
- LightFlash,
- ParticleFlash,
- PerkId,
- ReusableArray,
- TextFlash,
+ Ball,
+ BallLike,
+ Coin,
+ colorString,
+ GameState,
+ LightFlash,
+ ParticleFlash,
+ PerkId,
+ ReusableArray,
+ TextFlash,
} from "./types";
import {
- brickCenterX,
- brickCenterY,
- clamp,
- currentLevelInfo,
- distance2,
- distanceBetween,
- getMajorityValue,
- getPossibleUpgrades,
- getRowColIndex,
- isTelekinesisActive,
- isYoyoActive,
- max_levels, sample,
- shouldPierceByColor,
+ brickCenterX,
+ brickCenterY,
+ clamp,
+ currentLevelInfo,
+ distance2,
+ distanceBetween,
+ getMajorityValue,
+ getPossibleUpgrades,
+ getRowColIndex,
+ isTelekinesisActive,
+ isYoyoActive,
+ max_levels,
+ sample,
+ shouldPierceByColor,
} from "./game_utils";
-import {t} from "./i18n/i18n";
-import {icons} from "./loadGameData";
+import { t } from "./i18n/i18n";
+import { icons } from "./loadGameData";
-import {addToTotalScore} from "./settings";
-import {background} from "./render";
-import {gameOver} from "./gameOver";
-import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openUpgradesPicker, pause,} from "./game";
-import {stopRecording} from "./recording";
-import {isOptionOn} from "./options";
+import { addToTotalScore } from "./settings";
+import { background } from "./render";
+import { gameOver } from "./gameOver";
+import {
+ brickIndex,
+ fitSize,
+ gameState,
+ hasBrick,
+ hitsSomething,
+ openUpgradesPicker,
+ pause,
+} from "./game";
+import { stopRecording } from "./recording";
+import { isOptionOn } from "./options";
export function setMousePos(gameState: GameState, x: number) {
- // Sets the puck position, and updates the ball position if they are supposed to follow it
- gameState.puckPosition = x;
- gameState.needsRender = true;
+ // Sets the puck position, and updates the ball position if they are supposed to follow it
+ gameState.puckPosition = x;
+ gameState.needsRender = true;
}
function getBallDefaultVx(gameState: GameState) {
- return (
- (gameState.perks.concave_puck ? 0 : 1) *
- (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed)
- );
+ return (
+ (gameState.perks.concave_puck ? 0 : 1) *
+ (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed)
+ );
}
export function resetBalls(gameState: GameState) {
- const count = 1 + (gameState.perks?.multiball || 0);
- const perBall = gameState.puckWidth / (count + 1);
- gameState.balls = [];
- gameState.ballsColor = "#FFF";
- if (gameState.perks.picky_eater || gameState.perks.pierce_color) {
- gameState.ballsColor =
- getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF";
- }
- for (let i = 0; i < count; i++) {
- const x =
- gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
- const vx = getBallDefaultVx(gameState);
+ const count = 1 + (gameState.perks?.multiball || 0);
+ const perBall = gameState.puckWidth / (count + 1);
+ gameState.balls = [];
+ gameState.ballsColor = "#FFF";
+ if (gameState.perks.picky_eater || gameState.perks.pierce_color) {
+ gameState.ballsColor =
+ getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF";
+ }
+ for (let i = 0; i < count; i++) {
+ const x =
+ gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
+ const vx = getBallDefaultVx(gameState);
- gameState.balls.push({
- x,
- previousX: x,
- y: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
- previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
- vx,
- previousVX: vx,
- vy: -gameState.baseSpeed,
- previousVY: -gameState.baseSpeed,
+ gameState.balls.push({
+ x,
+ previousX: x,
+ y: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
+ previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
+ vx,
+ previousVX: vx,
+ vy: -gameState.baseSpeed,
+ previousVY: -gameState.baseSpeed,
- sx: 0,
- sy: 0,
- piercedSinceBounce: 0,
- hitSinceBounce: 0,
- hitItem: [],
- sapperUses: 0,
- });
- }
- gameState.ballStickToPuck = true;
+ sx: 0,
+ sy: 0,
+ piercedSinceBounce: 0,
+ hitSinceBounce: 0,
+ hitItem: [],
+ sapperUses: 0,
+ });
+ }
+ gameState.ballStickToPuck = true;
}
export function putBallsAtPuck(gameState: GameState) {
- // This reset could be abused to cheat quite easily
- const count = gameState.balls.length;
- const perBall = gameState.puckWidth / (count + 1);
- const vx = getBallDefaultVx(gameState);
- gameState.balls.forEach((ball, i) => {
- const x =
- gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
+ // This reset could be abused to cheat quite easily
+ const count = gameState.balls.length;
+ const perBall = gameState.puckWidth / (count + 1);
+ const vx = getBallDefaultVx(gameState);
+ gameState.balls.forEach((ball, i) => {
+ const x =
+ gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
- ball.x = x;
- ball.previousX = x;
- ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize;
- ball.previousY = ball.y;
- ball.vx = vx;
- ball.previousVX = ball.vx;
- ball.vy = -gameState.baseSpeed;
- ball.previousVY = ball.vy;
- ball.sx = 0;
- ball.sy = 0;
- ball.hitItem = [];
- ball.hitSinceBounce = 0;
- ball.piercedSinceBounce = 0;
- });
+ ball.x = x;
+ ball.previousX = x;
+ ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize;
+ ball.previousY = ball.y;
+ ball.vx = vx;
+ ball.previousVX = ball.vx;
+ ball.vy = -gameState.baseSpeed;
+ ball.previousVY = ball.vy;
+ ball.sx = 0;
+ ball.sy = 0;
+ ball.hitItem = [];
+ ball.hitSinceBounce = 0;
+ ball.piercedSinceBounce = 0;
+ });
}
export function normalizeGameState(gameState: GameState) {
- // This function resets most parameters on the state to correct values, and should be used even when the game is paused
+ // This function resets most parameters on the state to correct values, and should be used even when the game is paused
- gameState.baseSpeed = Math.max(
- 3,
- gameState.gameZoneWidth / 12 / 10 +
- gameState.currentLevel / 3 +
- gameState.levelTime / (30 * 1000) -
- gameState.perks.slow_down * 2,
- );
+ gameState.baseSpeed = Math.max(
+ 3,
+ gameState.gameZoneWidth / 12 / 10 +
+ gameState.currentLevel / 3 +
+ gameState.levelTime / (30 * 1000) -
+ gameState.perks.slow_down * 2,
+ );
- gameState.puckWidth =
- (gameState.gameZoneWidth / 12) *
- (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck);
+ gameState.puckWidth =
+ (gameState.gameZoneWidth / 12) *
+ (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck);
- if (
- gameState.puckPosition <
- gameState.offsetXRoundedDown + gameState.puckWidth / 2
- ) {
- gameState.puckPosition =
- gameState.offsetXRoundedDown + gameState.puckWidth / 2;
- }
- if (
- gameState.puckPosition >
- gameState.offsetXRoundedDown +
- gameState.gameZoneWidthRoundedUp -
- gameState.puckWidth / 2
- ) {
- gameState.puckPosition =
- gameState.offsetXRoundedDown +
- gameState.gameZoneWidthRoundedUp -
- gameState.puckWidth / 2;
- }
- if (gameState.ballStickToPuck) {
- putBallsAtPuck(gameState);
- }
+ if (
+ gameState.puckPosition <
+ gameState.offsetXRoundedDown + gameState.puckWidth / 2
+ ) {
+ gameState.puckPosition =
+ gameState.offsetXRoundedDown + gameState.puckWidth / 2;
+ }
+ if (
+ gameState.puckPosition >
+ gameState.offsetXRoundedDown +
+ gameState.gameZoneWidthRoundedUp -
+ gameState.puckWidth / 2
+ ) {
+ gameState.puckPosition =
+ gameState.offsetXRoundedDown +
+ gameState.gameZoneWidthRoundedUp -
+ gameState.puckWidth / 2;
+ }
+ if (gameState.ballStickToPuck) {
+ putBallsAtPuck(gameState);
+ }
}
export function baseCombo(gameState: GameState) {
- return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
+ return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
}
export function resetCombo(
- gameState: GameState,
- x: number | undefined,
- y: number | undefined,
+ gameState: GameState,
+ x: number | undefined,
+ y: number | undefined,
) {
- const prev = gameState.combo;
- gameState.combo = baseCombo(gameState);
+ const prev = gameState.combo;
+ gameState.combo = baseCombo(gameState);
- if (prev > gameState.combo && gameState.perks.soft_reset) {
- gameState.combo += Math.floor(
- ((prev - gameState.combo) * (gameState.perks.soft_reset * 10)) / 100,
- );
+ if (prev > gameState.combo && gameState.perks.soft_reset) {
+ gameState.combo += Math.floor(
+ ((prev - gameState.combo) * (gameState.perks.soft_reset * 10)) / 100,
+ );
+ }
+ const lost = Math.max(0, prev - gameState.combo);
+ if (lost) {
+ for (let i = 0; i < lost && i < 8; i++) {
+ setTimeout(
+ () => schedulGameSound(gameState, "comboDecrease", x, 1),
+ i * 100,
+ );
}
- const lost = Math.max(0, prev - gameState.combo);
- if (lost) {
- for (let i = 0; i < lost && i < 8; i++) {
- setTimeout(
- () => schedulGameSound(gameState, "comboDecrease", x, 1),
- i * 100,
- );
- }
- if (typeof x !== "undefined" && typeof y !== "undefined") {
- makeText(gameState, x, y, "red", "-" + lost, 20, 150);
- }
+ if (typeof x !== "undefined" && typeof y !== "undefined") {
+ makeText(gameState, x, y, "red", "-" + lost, 20, 150);
}
- return lost;
+ }
+ return lost;
}
export function decreaseCombo(
- gameState: GameState,
- by: number,
- x: number,
- y: number,
+ gameState: GameState,
+ by: number,
+ x: number,
+ y: number,
) {
- const prev = gameState.combo;
- gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by);
- const lost = Math.max(0, prev - gameState.combo);
+ const prev = gameState.combo;
+ gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by);
+ const lost = Math.max(0, prev - gameState.combo);
- if (lost) {
- schedulGameSound(gameState, "comboDecrease", x, 1);
- if (typeof x !== "undefined" && typeof y !== "undefined") {
- makeText(gameState, x, y, "red", "-" + lost, 20, 300);
- }
+ if (lost) {
+ schedulGameSound(gameState, "comboDecrease", x, 1);
+ if (typeof x !== "undefined" && typeof y !== "undefined") {
+ makeText(gameState, x, y, "red", "-" + lost, 20, 300);
}
+ }
}
export function spawnExplosion(
- gameState: GameState,
- count: number,
- x: number,
- y: number,
- color: string,
+ gameState: GameState,
+ count: number,
+ x: number,
+ y: number,
+ color: string,
) {
- if (!!isOptionOn("basic")) return;
+ if (!!isOptionOn("basic")) return;
- if (liveCount(gameState.particles) > gameState.MAX_PARTICLES) {
- // Avoid freezing when lots of explosion happen at once
- count = 1;
- }
- for (let i = 0; i < count; i++) {
- makeParticle(
- gameState,
+ if (liveCount(gameState.particles) > gameState.MAX_PARTICLES) {
+ // Avoid freezing when lots of explosion happen at once
+ count = 1;
+ }
+ 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,
- );
- }
+ 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,
+ );
+ }
}
export function explosionAt(
- gameState: GameState,
- index: number,
- x: number, y: number,
- ball: Ball) {
+ gameState: GameState,
+ index: number,
+ x: number,
+ 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);
-
- const col = index % gameState.gridSize;
- const row = Math.floor(index / gameState.gridSize);
- const size = 1 + gameState.perks.bigger_explosions;
- // Break bricks around
- for (let dx = -size; dx <= size; dx++) {
- for (let dy = -size; dy <= size; dy++) {
- const i = getRowColIndex(gameState, row + dy, col + dx);
- if (gameState.bricks[i] && i !== -1) {
- // Study bricks resist explosions too
- if (
- gameState.bricks[i] !== "black" &&
- gameState.perks.sturdy_bricks > Math.random() * 5
- )
- continue;
- explodeBrick(gameState, i, ball, true);
- }
- }
+ const col = index % gameState.gridSize;
+ const row = Math.floor(index / gameState.gridSize);
+ const size = 1 + gameState.perks.bigger_explosions;
+ // Break bricks around
+ for (let dx = -size; dx <= size; dx++) {
+ for (let dy = -size; dy <= size; dy++) {
+ const i = getRowColIndex(gameState, row + dy, col + dx);
+ if (gameState.bricks[i] && i !== -1) {
+ // Study bricks resist explosions too
+ if (
+ gameState.bricks[i] !== "black" &&
+ gameState.perks.sturdy_bricks > Math.random() * 5
+ )
+ continue;
+ explodeBrick(gameState, i, ball, true);
+ }
}
+ }
- // Blow nearby coins
- forEachLiveOne(gameState.coins, (c) => {
- const dx = c.x - x;
- const dy = c.y - y;
- const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy));
- c.vx += ((dx / d2) * 10 * size) / c.weight;
- c.vy += ((dy / d2) * 10 * size) / c.weight;
- });
- gameState.lastExplosion = Date.now();
+ // Blow nearby coins
+ forEachLiveOne(gameState.coins, (c) => {
+ const dx = c.x - x;
+ const dy = c.y - y;
+ const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy));
+ c.vx += ((dx / d2) * 10 * size) / c.weight;
+ c.vy += ((dy / d2) * 10 * size) / c.weight;
+ });
+ gameState.lastExplosion = Date.now();
- makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150);
+ makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150);
- spawnExplosion(
- gameState,
- 7 * (1 + gameState.perks.bigger_explosions),
- x,
- y,
- "white",
- );
- gameState.runStatistics.bricks_broken++;
+ spawnExplosion(
+ gameState,
+ 7 * (1 + gameState.perks.bigger_explosions),
+ x,
+ y,
+ "white",
+ );
+ gameState.runStatistics.bricks_broken++;
- if(gameState.perks.zen){
- resetCombo(gameState, x,y)
- }
+ if (gameState.perks.zen) {
+ resetCombo(gameState, x, y);
+ }
}
export function explodeBrick(
- gameState: GameState,
- index: number,
- ball: Ball,
- isExplosion: boolean,
+ gameState: GameState,
+ index: number,
+ ball: Ball,
+ isExplosion: boolean,
) {
- const color = gameState.bricks[index];
- if (!color) return;
+ const color = gameState.bricks[index];
+ if (!color) return;
- if (color === "black") {
- const x = brickCenterX(gameState, index),
- y = brickCenterY(gameState, index);
- explosionAt(gameState,
- index, x, y,
- ball)
+ if (color === "black") {
+ const x = brickCenterX(gameState, index),
+ y = brickCenterY(gameState, index);
+ explosionAt(gameState, index, x, y, ball);
+ } else if (color) {
+ // Even if it bounces we don't want to count that as a miss
- } else if (color) {
- // Even if it bounces we don't want to count that as a miss
+ // Flashing is take care of by the tick loop
+ const x = brickCenterX(gameState, index),
+ y = brickCenterY(gameState, index);
- // Flashing is take care of by the tick loop
- const x = brickCenterX(gameState, index),
- y = brickCenterY(gameState, index);
+ gameState.bricks[index] = "";
- gameState.bricks[index] = "";
-
- let coinsToSpawn = gameState.combo;
- if (gameState.perks.sturdy_bricks) {
- // +10% per level
- coinsToSpawn += Math.ceil(
- ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn,
- );
- }
-
- gameState.levelSpawnedCoins += coinsToSpawn;
- gameState.runStatistics.coins_spawned += coinsToSpawn;
- gameState.runStatistics.bricks_broken++;
- const maxCoins = gameState.MAX_COINS * (isOptionOn("basic") ? 0.5 : 1);
- const spawnableCoins =
- liveCount(gameState.coins) > gameState.MAX_COINS
- ? 1
- : Math.floor(maxCoins - liveCount(gameState.coins)) / 3;
-
- const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
-
- while (coinsToSpawn > 0) {
- const points = Math.min(pointsPerCoin, coinsToSpawn);
- if (points < 0 || isNaN(points)) {
- console.error({points});
- debugger;
- }
-
- coinsToSpawn -= points;
-
- const cx =
- x +
- (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize),
- cy =
- y +
- (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize);
- makeCoin(
- gameState,
- cx,
- cy,
- ball.previousVX * (0.5 + Math.random()),
- ball.previousVY * (0.5 + Math.random()),
- gameState.perks.metamorphosis ? color : "gold",
- points,
- );
- }
-
- gameState.combo += gameState.perks.streak_shots +
- gameState.perks.compound_interest +
- gameState.perks.left_is_lava +
- gameState.perks.right_is_lava +
- gameState.perks.top_is_lava +
- gameState.perks.picky_eater +
- gameState.perks.asceticism +
- gameState.perks.zen +
- gameState.perks.unbounded
-
- ;
-
- if (!isExplosion) {
- // color change
- if (
- (gameState.perks.picky_eater || gameState.perks.pierce_color) &&
- color !== gameState.ballsColor &&
- color
- ) {
- if (gameState.perks.picky_eater) {
- resetCombo(gameState, ball.x, ball.y);
- }
- schedulGameSound(gameState, "colorChange", ball.x, 0.8);
- gameState.lastExplosion = gameState.levelTime;
- gameState.ballsColor = color;
- if (!isOptionOn("basic")) {
- gameState.balls.forEach((ball) => {
- spawnExplosion(gameState, 7, ball.previousX, ball.previousY, color);
- });
- }
- } else {
- schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1);
- }
- }
- makeLight(gameState, x, y, color, gameState.brickWidth, 40);
-
- spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color);
+ let coinsToSpawn = gameState.combo;
+ if (gameState.perks.sturdy_bricks) {
+ // +10% per level
+ coinsToSpawn += Math.ceil(
+ ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn,
+ );
}
- if (!gameState.bricks[index] && color !== "black") {
- ball.hitItem?.push({
- index,
- color,
- });
+ gameState.levelSpawnedCoins += coinsToSpawn;
+ gameState.runStatistics.coins_spawned += coinsToSpawn;
+ gameState.runStatistics.bricks_broken++;
+ const maxCoins = gameState.MAX_COINS * (isOptionOn("basic") ? 0.5 : 1);
+ const spawnableCoins =
+ liveCount(gameState.coins) > gameState.MAX_COINS
+ ? 1
+ : Math.floor(maxCoins - liveCount(gameState.coins)) / 3;
+
+ const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
+
+ while (coinsToSpawn > 0) {
+ const points = Math.min(pointsPerCoin, coinsToSpawn);
+ if (points < 0 || isNaN(points)) {
+ console.error({ points });
+ debugger;
+ }
+
+ coinsToSpawn -= points;
+
+ const cx =
+ x +
+ (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize),
+ cy =
+ y +
+ (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize);
+ makeCoin(
+ gameState,
+ cx,
+ cy,
+ ball.previousVX * (0.5 + Math.random()),
+ ball.previousVY * (0.5 + Math.random()),
+ gameState.perks.metamorphosis ? color : "gold",
+ points,
+ );
}
+
+ gameState.combo +=
+ gameState.perks.streak_shots +
+ gameState.perks.compound_interest +
+ gameState.perks.left_is_lava +
+ gameState.perks.right_is_lava +
+ gameState.perks.top_is_lava +
+ gameState.perks.picky_eater +
+ gameState.perks.asceticism +
+ gameState.perks.zen +
+ gameState.perks.unbounded;
+
+ if (!isExplosion) {
+ // color change
+ if (
+ (gameState.perks.picky_eater || gameState.perks.pierce_color) &&
+ color !== gameState.ballsColor &&
+ color
+ ) {
+ if (gameState.perks.picky_eater) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+ schedulGameSound(gameState, "colorChange", ball.x, 0.8);
+ gameState.lastExplosion = gameState.levelTime;
+ gameState.ballsColor = color;
+ if (!isOptionOn("basic")) {
+ gameState.balls.forEach((ball) => {
+ spawnExplosion(gameState, 7, ball.previousX, ball.previousY, color);
+ });
+ }
+ } else {
+ schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1);
+ }
+ }
+ makeLight(gameState, x, y, color, gameState.brickWidth, 40);
+
+ spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color);
+ }
+
+ if (!gameState.bricks[index] && color !== "black") {
+ ball.hitItem?.push({
+ index,
+ color,
+ });
+ }
}
export function dontOfferTooSoon(gameState: GameState, id: PerkId) {
- gameState.lastOffered[id] = Math.round(Date.now() / 1000);
+ gameState.lastOffered[id] = Math.round(Date.now() / 1000);
}
export function pickRandomUpgrades(gameState: GameState, count: number) {
- let list = getPossibleUpgrades(gameState)
- .map((u) => ({
- ...u,
- score: Math.random() + (gameState.lastOffered[u.id] || 0),
- }))
- .sort((a, b) => a.score - b.score)
- .filter((u) => gameState.perks[u.id] < u.max)
- .slice(0, count)
- .sort((a, b) => (a.id > b.id ? 1 : -1));
+ let list = getPossibleUpgrades(gameState)
+ .map((u) => ({
+ ...u,
+ score: Math.random() + (gameState.lastOffered[u.id] || 0),
+ }))
+ .sort((a, b) => a.score - b.score)
+ .filter((u) => gameState.perks[u.id] < u.max)
+ .slice(0, count)
+ .sort((a, b) => (a.id > b.id ? 1 : -1));
- list.forEach((u) => {
- dontOfferTooSoon(gameState, u.id);
- });
+ list.forEach((u) => {
+ dontOfferTooSoon(gameState, u.id);
+ });
- return list.map((u) => ({
- text:
- u.name +
- (gameState.perks[u.id]
- ? t("level_up.upgrade_perk_to_level", {
- level: gameState.perks[u.id] + 1,
- })
- : ""),
- icon: icons["icon:" + u.id],
- value: u.id as PerkId,
- help: u.help(gameState.perks[u.id] + 1),
- }));
+ return list.map((u) => ({
+ text:
+ u.name +
+ (gameState.perks[u.id]
+ ? t("level_up.upgrade_perk_to_level", {
+ level: gameState.perks[u.id] + 1,
+ })
+ : ""),
+ icon: icons["icon:" + u.id],
+ value: u.id as PerkId,
+ help: u.help(gameState.perks[u.id] + 1),
+ }));
}
export function schedulGameSound(
- gameState: GameState,
- sound: keyof GameState["aboutToPlaySound"],
- x: number | void,
- vol: number,
+ gameState: GameState,
+ sound: keyof GameState["aboutToPlaySound"],
+ x: number | void,
+ vol: number,
) {
- if (!vol) return;
- x ??= gameState.offsetX + gameState.gameZoneWidth / 2;
- const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number };
+ if (!vol) return;
+ x ??= gameState.offsetX + gameState.gameZoneWidth / 2;
+ const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number };
- ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol);
- ex.vol += vol;
+ ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol);
+ ex.vol += vol;
}
export function addToScore(gameState: GameState, coin: Coin) {
- gameState.score += coin.points;
- gameState.lastScoreIncrease = gameState.levelTime;
+ gameState.score += coin.points;
+ gameState.lastScoreIncrease = gameState.levelTime;
- addToTotalScore(gameState, coin.points);
- if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) {
- gameState.highScore = gameState.score;
- localStorage.setItem("breakout-3-hs", gameState.score.toString());
- }
- if (!isOptionOn("basic")) {
- makeParticle(
- gameState,
- coin.previousX,
- coin.previousY,
- (gameState.canvasWidth - coin.x) / 100,
- -coin.y / 100,
- coin.color,
- true,
- gameState.coinSize / 2,
- 100 + Math.random() * 50,
- );
- }
+ addToTotalScore(gameState, coin.points);
+ if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) {
+ gameState.highScore = gameState.score;
+ localStorage.setItem("breakout-3-hs", gameState.score.toString());
+ }
+ if (!isOptionOn("basic")) {
+ makeParticle(
+ gameState,
+ coin.previousX,
+ coin.previousY,
+ (gameState.canvasWidth - coin.x) / 100,
+ -coin.y / 100,
+ coin.color,
+ true,
+ gameState.coinSize / 2,
+ 100 + Math.random() * 50,
+ );
+ }
- if (Date.now() - gameState.lastPlayedCoinGrab > 16) {
- gameState.lastPlayedCoinGrab = Date.now();
+ if (Date.now() - gameState.lastPlayedCoinGrab > 16) {
+ gameState.lastPlayedCoinGrab = Date.now();
- schedulGameSound(gameState, "coinCatch", coin.x, 1);
- }
- gameState.runStatistics.score += coin.points;
- if (gameState.perks.asceticism) {
- resetCombo(gameState, coin.x, coin.y)
- }
+ schedulGameSound(gameState, "coinCatch", coin.x, 1);
+ }
+ gameState.runStatistics.score += coin.points;
+ if (gameState.perks.asceticism) {
+ resetCombo(gameState, coin.x, coin.y);
+ }
}
export async function setLevel(gameState: GameState, l: number) {
- stopRecording();
- pause(false);
- if (l > 0) {
- await openUpgradesPicker(gameState);
- }
- gameState.currentLevel = l;
- gameState.levelTime = 0;
- gameState.noBricksSince = 0;
- gameState.levelWallBounces = 0;
- gameState.autoCleanUses = 0;
- gameState.lastTickDown = gameState.levelTime;
- gameState.levelStartScore = gameState.score;
- gameState.levelSpawnedCoins = 0;
- gameState.levelMisses = 0;
- gameState.runStatistics.levelsPlayed++;
+ stopRecording();
+ pause(false);
+ if (l > 0) {
+ await openUpgradesPicker(gameState);
+ }
+ gameState.currentLevel = l;
+ gameState.levelTime = 0;
+ gameState.noBricksSince = 0;
+ gameState.levelWallBounces = 0;
+ gameState.autoCleanUses = 0;
+ gameState.lastTickDown = gameState.levelTime;
+ gameState.levelStartScore = gameState.score;
+ gameState.levelSpawnedCoins = 0;
+ gameState.levelMisses = 0;
+ gameState.runStatistics.levelsPlayed++;
- // Reset combo silently
- if (!gameState.perks.shunt) {
- gameState.combo = baseCombo(gameState)
- }
- gameState.combo += gameState.perks.hot_start * 15;
+ // Reset combo silently
+ if (!gameState.perks.shunt) {
+ gameState.combo = baseCombo(gameState);
+ }
+ gameState.combo += gameState.perks.hot_start * 15;
- resetBalls(gameState);
+ resetBalls(gameState);
- const lvl = currentLevelInfo(gameState);
- if (lvl.size !== gameState.gridSize) {
- gameState.gridSize = lvl.size;
- fitSize();
- }
- empty(gameState.coins);
- empty(gameState.particles);
- empty(gameState.lights);
- empty(gameState.texts);
- gameState.bricks = [...lvl.bricks];
- gameState.needsRender = true;
- // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons
- // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
- background.src = "data:image/svg+xml;UTF8," + lvl.svg;
+ const lvl = currentLevelInfo(gameState);
+ if (lvl.size !== gameState.gridSize) {
+ gameState.gridSize = lvl.size;
+ fitSize();
+ }
+ empty(gameState.coins);
+ empty(gameState.particles);
+ empty(gameState.lights);
+ empty(gameState.texts);
+ gameState.bricks = [...lvl.bricks];
+ gameState.needsRender = true;
+ // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons
+ // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
+ background.src = "data:image/svg+xml;UTF8," + lvl.svg;
}
export function rainbowColor(): colorString {
- return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`;
+ return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`;
}
export function repulse(
- gameState: GameState,
- a: Ball,
- b: BallLike,
- power: number,
- impactsBToo: boolean,
+ gameState: GameState,
+ a: Ball,
+ b: BallLike,
+ power: number,
+ impactsBToo: boolean,
) {
- const distance = distanceBetween(a, b);
- // Ensure we don't get soft locked
- const max = gameState.gameZoneWidth / 4;
- if (distance > max) return;
- // Unit vector
- const dx = (a.x - b.x) / distance;
- const dy = (a.y - b.y) / distance;
- const fact =
- (((-power * (max - distance)) / (max * 1.2) / 3) *
- Math.min(500, gameState.levelTime)) /
- 500;
- if (
- impactsBToo &&
- typeof b.vx !== "undefined" &&
- typeof b.vy !== "undefined"
- ) {
- b.vx += dx * fact;
- b.vy += dy * fact;
- }
- a.vx -= dx * fact;
- a.vy -= dy * fact;
+ const distance = distanceBetween(a, b);
+ // Ensure we don't get soft locked
+ const max = gameState.gameZoneWidth / 4;
+ if (distance > max) return;
+ // Unit vector
+ const dx = (a.x - b.x) / distance;
+ const dy = (a.y - b.y) / distance;
+ const fact =
+ (((-power * (max - distance)) / (max * 1.2) / 3) *
+ Math.min(500, gameState.levelTime)) /
+ 500;
+ if (
+ impactsBToo &&
+ typeof b.vx !== "undefined" &&
+ typeof b.vy !== "undefined"
+ ) {
+ b.vx += dx * fact;
+ b.vy += dy * fact;
+ }
+ a.vx -= dx * fact;
+ a.vy -= dy * fact;
- const speed = 10;
- const rand = 2;
+ const speed = 10;
+ const rand = 2;
+ makeParticle(
+ gameState,
+ a.x,
+ a.y,
+ -dx * speed + a.vx + (Math.random() - 0.5) * rand,
+ -dy * speed + a.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
+ if (
+ impactsBToo &&
+ typeof b.vx !== "undefined" &&
+ typeof b.vy !== "undefined"
+ ) {
makeParticle(
- gameState,
- a.x,
- a.y,
- -dx * speed + a.vx + (Math.random() - 0.5) * rand,
- -dy * speed + a.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
+ gameState,
+ b.x,
+ b.y,
+ dx * speed + b.vx + (Math.random() - 0.5) * rand,
+ dy * speed + b.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
);
- if (
- impactsBToo &&
- typeof b.vx !== "undefined" &&
- typeof b.vy !== "undefined"
- ) {
- makeParticle(
- gameState,
- b.x,
- b.y,
- dx * speed + b.vx + (Math.random() - 0.5) * rand,
- dy * speed + b.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
- );
- }
+ }
}
export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
- const distance = distanceBetween(a, b);
- // Ensure we don't get soft locked
- const min = (gameState.gameZoneWidth * 3) / 4;
- if (distance < min) return;
- // Unit vector
- const dx = (a.x - b.x) / distance;
- const dy = (a.y - b.y) / distance;
+ const distance = distanceBetween(a, b);
+ // Ensure we don't get soft locked
+ const min = (gameState.gameZoneWidth * 3) / 4;
+ if (distance < min) return;
+ // Unit vector
+ const dx = (a.x - b.x) / distance;
+ const dy = (a.y - b.y) / distance;
- const fact =
- (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) /
- 500;
- b.vx += dx * fact;
- b.vy += dy * fact;
- a.vx -= dx * fact;
- a.vy -= dy * fact;
+ const fact =
+ (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) /
+ 500;
+ b.vx += dx * fact;
+ b.vy += dy * fact;
+ a.vx -= dx * fact;
+ a.vy -= dy * fact;
- const speed = 10;
- const rand = 2;
+ const speed = 10;
+ const rand = 2;
- makeParticle(
- gameState,
- a.x,
- a.y,
- dx * speed + a.vx + (Math.random() - 0.5) * rand,
- dy * speed + a.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
- );
- makeParticle(
- gameState,
- b.x,
- b.y,
- -dx * speed + b.vx + (Math.random() - 0.5) * rand,
- -dy * speed + b.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
- );
+ makeParticle(
+ gameState,
+ a.x,
+ a.y,
+ dx * speed + a.vx + (Math.random() - 0.5) * rand,
+ dy * speed + a.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
+ makeParticle(
+ gameState,
+ b.x,
+ b.y,
+ -dx * speed + b.vx + (Math.random() - 0.5) * rand,
+ -dy * speed + b.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
}
export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
- if(gameState.perks.ghost_coins)return undefined
- // Make ball/coin bonce, and return bricks that were hit
- const radius = coin.size / 2;
- const {x, y, previousX, previousY} = coin;
+ if (gameState.perks.ghost_coins) return undefined;
+ // Make ball/coin bonce, and return bricks that were hit
+ const radius = coin.size / 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;
+ 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;
+ if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
+ coin.y = coin.previousY;
+ coin.vy *= -1;
- // Roll on corners
- const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)];
- const rightHit = gameState.bricks[brickIndex(x + radius, y + radius)];
+ // Roll on corners
+ const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)];
+ const rightHit = gameState.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 (leftHit && !rightHit) {
+ coin.vx += 1;
+ coin.sa -= 1;
}
- if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
- coin.x = coin.previousX;
- coin.vx *= -1;
+ if (!leftHit && rightHit) {
+ coin.vx -= 1;
+ coin.sa += 1;
}
- return vhit ?? hhit ?? chit;
+ }
+ if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
+ coin.x = coin.previousX;
+ coin.vx *= -1;
+ }
+ return vhit ?? hhit ?? chit;
}
export function bordersHitCheck(
- gameState: GameState,
- coin: Coin | Ball,
- radius: number,
- delta: number,
+ gameState: GameState,
+ coin: Coin | Ball,
+ radius: number,
+ delta: number,
) {
- if (coin.destroyed) return;
- coin.previousX = coin.x;
- coin.previousY = coin.y;
- coin.x += coin.vx * delta;
- coin.y += coin.vy * delta;
- coin.sx ||= 0;
- coin.sy ||= 0;
- coin.sx += coin.previousX - coin.x;
- coin.sy += coin.previousY - coin.y;
- coin.sx *= 0.9;
- coin.sy *= 0.9;
+ if (coin.destroyed) return;
+ coin.previousX = coin.x;
+ coin.previousY = coin.y;
+ coin.x += coin.vx * delta;
+ coin.y += coin.vy * delta;
+ coin.sx ||= 0;
+ coin.sy ||= 0;
+ coin.sx += coin.previousX - coin.x;
+ coin.sy += coin.previousY - coin.y;
+ coin.sx *= 0.9;
+ coin.sy *= 0.9;
- if (gameState.perks.wind) {
- coin.vx +=
- ((gameState.puckPosition -
- (gameState.offsetX + gameState.gameZoneWidth / 2)) /
- gameState.gameZoneWidth) *
- gameState.perks.wind *
- 0.5;
- }
+ if (gameState.perks.wind) {
+ coin.vx +=
+ ((gameState.puckPosition -
+ (gameState.offsetX + gameState.gameZoneWidth / 2)) /
+ gameState.gameZoneWidth) *
+ gameState.perks.wind *
+ 0.5;
+ }
- let vhit = 0,
- hhit = 0;
+ let vhit = 0,
+ hhit = 0;
- if (coin.x < gameState.offsetXRoundedDown + radius && !gameState.perks.unbounded) {
- coin.x =
- gameState.offsetXRoundedDown +
- radius +
- (gameState.offsetXRoundedDown + radius - coin.x);
- coin.vx *= -1;
- hhit = 1;
- }
- if (coin.y < radius) {
- coin.y = radius + (radius - coin.y);
- coin.vy *= -1;
- vhit = 1;
- }
- if (coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius && !gameState.perks.unbounded) {
- coin.x =
- gameState.canvasWidth -
- gameState.offsetXRoundedDown -
- radius -
- (coin.x -
- (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
- coin.vx *= -1;
- hhit = 1;
- }
+ if (
+ coin.x < gameState.offsetXRoundedDown + radius &&
+ !gameState.perks.unbounded
+ ) {
+ coin.x =
+ gameState.offsetXRoundedDown +
+ radius +
+ (gameState.offsetXRoundedDown + radius - coin.x);
+ coin.vx *= -1;
+ hhit = 1;
+ }
+ if (coin.y < radius) {
+ coin.y = radius + (radius - coin.y);
+ coin.vy *= -1;
+ vhit = 1;
+ }
+ if (
+ coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius &&
+ !gameState.perks.unbounded
+ ) {
+ coin.x =
+ gameState.canvasWidth -
+ gameState.offsetXRoundedDown -
+ radius -
+ (coin.x -
+ (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
+ coin.vx *= -1;
+ hhit = 1;
+ }
- return hhit + vhit * 2;
+ return hhit + vhit * 2;
}
export function gameStateTick(
- gameState: GameState,
- // How many frames to compute at once, can go above 1 to compensate lag
- frames = 1,
+ gameState: GameState,
+ // How many frames to compute at once, can go above 1 to compensate lag
+ frames = 1,
) {
- gameState.runStatistics.max_combo = Math.max(
- gameState.runStatistics.max_combo,
- gameState.combo,
+ gameState.runStatistics.max_combo = Math.max(
+ gameState.runStatistics.max_combo,
+ gameState.combo,
+ );
+
+ gameState.balls = gameState.balls.filter((ball) => !ball.destroyed);
+
+ const remainingBricks = gameState.bricks.filter(
+ (b) => b && b !== "black",
+ ).length;
+
+ if (
+ gameState.levelTime > gameState.lastTickDown + 1000 &&
+ gameState.perks.hot_start
+ ) {
+ gameState.lastTickDown = gameState.levelTime;
+ decreaseCombo(
+ gameState,
+ gameState.perks.hot_start,
+ gameState.puckPosition,
+ gameState.gameZoneHeight - 2 * gameState.puckHeight,
);
+ }
- gameState.balls = gameState.balls.filter((ball) => !ball.destroyed);
+ if (
+ remainingBricks <= gameState.perks.skip_last &&
+ !gameState.autoCleanUses
+ ) {
+ gameState.bricks.forEach((type, index) => {
+ if (type) {
+ explodeBrick(gameState, index, gameState.balls[0], true);
+ }
+ });
+ gameState.autoCleanUses++;
+ }
+ if (!remainingBricks && gameState.noBricksSince == 0) {
+ gameState.noBricksSince ||= gameState.levelTime;
+ }
+ if (
+ !remainingBricks &&
+ (!liveCount(gameState.coins) ||
+ gameState.levelTime > gameState.noBricksSince + 5000)
+ ) {
+ if (gameState.currentLevel + 1 < max_levels(gameState)) {
+ setLevel(gameState, gameState.currentLevel + 1);
+ } else {
+ gameOver(
+ t("gameOver.win.title"),
+ t("gameOver.win.summary", { score: gameState.score }),
+ );
+ }
+ } else if (gameState.running || gameState.levelTime) {
+ const coinRadius = Math.round(gameState.coinSize / 2);
- const remainingBricks = gameState.bricks.filter(
- (b) => b && b !== "black",
- ).length;
+ forEachLiveOne(gameState.coins, (coin, coinIndex) => {
+ if (gameState.perks.coin_magnet) {
+ const strength =
+ (100 /
+ (100 +
+ Math.pow(coin.y - gameState.gameZoneHeight, 2) +
+ Math.pow(coin.x - gameState.puckPosition, 2))) *
+ gameState.perks.coin_magnet;
- if (
- gameState.levelTime > gameState.lastTickDown + 1000 &&
- gameState.perks.hot_start
- ) {
- gameState.lastTickDown = gameState.levelTime;
- decreaseCombo(
+ const attractionX =
+ frames * (gameState.puckPosition - coin.x) * strength;
+
+ coin.vx += attractionX;
+ coin.vy +=
+ (frames * (gameState.gameZoneHeight - coin.y) * strength) / 2;
+ coin.sa -= attractionX / 10;
+ }
+
+ if (gameState.perks.ball_attracts_coins) {
+ gameState.balls.forEach((ball) => {
+ const d2 = distance2(ball, coin);
+ coin.vx +=
+ ((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;
+
+ coin.vy *= ratio;
+ coin.vx *= ratio;
+ if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
+ if (coin.vx < -7 * gameState.baseSpeed)
+ coin.vx = -7 * gameState.baseSpeed;
+ if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed;
+ if (coin.vy < -7 * gameState.baseSpeed)
+ coin.vy = -7 * gameState.baseSpeed;
+ coin.a += coin.sa;
+
+ // Gravity
+ if (!gameState.perks.etherealcoins) {
+ 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);
+ if (flip && !isOptionOn("basic") && Math.random() < 0.1) {
+ makeParticle(
gameState,
- gameState.perks.hot_start,
- gameState.puckPosition,
- gameState.gameZoneHeight - 2 * gameState.puckHeight,
- );
- }
-
- if (
- remainingBricks <= gameState.perks.skip_last &&
- !gameState.autoCleanUses
- ) {
- gameState.bricks.forEach((type, index) => {
- if (type) {
- explodeBrick(gameState, index, gameState.balls[0], true);
- }
- });
- gameState.autoCleanUses++;
- }
- if (!remainingBricks && gameState.noBricksSince == 0) {
- gameState.noBricksSince ||= gameState.levelTime
- }
- if (!remainingBricks && (!liveCount(gameState.coins) || (gameState.levelTime > gameState.noBricksSince + 5000))) {
- if (gameState.currentLevel + 1 < max_levels(gameState)) {
- setLevel(gameState, gameState.currentLevel + 1);
- } else {
- gameOver(
- t("gameOver.win.title"),
- t("gameOver.win.summary", {score: gameState.score}),
- );
+ coin.x,
+ coin.y,
+ 0,
+ gameState.baseSpeed,
+ rainbowColor(),
+ true,
+ 5,
+ 250,
+ );
}
- } else if (gameState.running || gameState.levelTime) {
- const coinRadius = Math.round(gameState.coinSize / 2);
+ }
- forEachLiveOne(gameState.coins, (coin, coinIndex) => {
- if (gameState.perks.coin_magnet) {
- const strength = 100 / (100 +
- Math.pow(coin.y - gameState.gameZoneHeight, 2) +
- Math.pow(coin.x - gameState.puckPosition, 2)) *
- gameState.perks.coin_magnet;
-
- const attractionX = frames * (gameState.puckPosition - coin.x) * strength
-
- coin.vx += attractionX;
- coin.vy += frames * (gameState.gameZoneHeight - coin.y) * strength / 2
- coin.sa -= attractionX / 10;
-
- }
-
- if(gameState.perks.ball_attracts_coins){
- gameState.balls.forEach(ball=>{
- const d2= distance2(ball, coin)
- coin.vx+=(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;
-
- coin.vy *= ratio;
- coin.vx *= ratio;
- if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
- if (coin.vx < -7 * gameState.baseSpeed)
- coin.vx = -7 * gameState.baseSpeed;
- if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed;
- if (coin.vy < -7 * gameState.baseSpeed)
- coin.vy = -7 * gameState.baseSpeed;
- coin.a += coin.sa;
-
- // Gravity
- if (!gameState.perks.etherealcoins) {
- 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);
- if (flip && !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 hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
-
- if (
- coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
- coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy &&
- Math.abs(coin.x - gameState.puckPosition) <
- coinRadius +
- gameState.puckWidth / 2 + // a bit of margin to be nice
- gameState.puckHeight
- ) {
- addToScore(gameState, coin);
- destroy(gameState.coins, coinIndex);
- } else if (coin.y > gameState.canvasHeight + coinRadius) {
- destroy(gameState.coins, coinIndex);
- if (gameState.perks.compound_interest) {
- resetCombo(gameState, coin.x, coin.y);
- }
- } else if (gameState.perks.unbounded && (coin.x < -50 || coin.x > gameState.canvasWidth + 50)) {
- // Out of bound on sides
- destroy(gameState.coins, coinIndex);
- }
-
-
- const hitBrick = coinBrickHitCheck(gameState, coin);
-
- if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
- if (
- gameState.bricks[hitBrick] &&
- coin.color !== gameState.bricks[hitBrick] &&
- gameState.bricks[hitBrick] !== "black" &&
- !coin.coloredABrick
- ) {
- gameState.bricks[hitBrick] = coin.color;
- coin.coloredABrick = true;
-
- schedulGameSound(gameState, "colorChange", coin.x, 0.3);
- }
- }
- if (typeof hitBrick !== "undefined" || hitBorder) {
- coin.vx *= 0.8;
- coin.vy *= 0.8;
- coin.sa *= 0.9;
- if (speed > 20) {
- schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
- }
-
- if (Math.abs(coin.vy) < 3) {
- coin.vy = 0;
- }
- }
- });
-
- gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
-
- if (gameState.perks.shocks) {
- gameState.balls.forEach((a, ai) =>
- gameState.balls.forEach((b, bi) => {
- if (ai < bi && !a.destroyed && !b.destroyed && distance2(a, b) < gameState.ballSize * gameState.ballSize) {
- 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 y = (a.y + b.y) / 2
- const limit = gameState.baseSpeed
- a.vx += clamp(a.x - x, -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)
-
- explosionAt(gameState, index, x, y, a)
- }
- }
- )
- )
- }
-
- if (gameState.perks.wind) {
- const windD =
- ((gameState.puckPosition -
- (gameState.offsetX + gameState.gameZoneWidth / 2)) /
- gameState.gameZoneWidth) *
- 2 *
- gameState.perks.wind;
- for (let i = 0; i < gameState.perks.wind; i++) {
- if (Math.random() * Math.abs(windD) > 0.5) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown +
- Math.random() * gameState.gameZoneWidthRoundedUp,
- Math.random() * gameState.gameZoneHeight,
- windD * 8,
- 0,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 150,
- );
- }
- }
- }
- forEachLiveOne(gameState.particles, (flash, index) => {
- flash.x += flash.vx * frames;
- flash.y += flash.vy * frames;
- if (!flash.ethereal) {
- flash.vy += 0.5;
- if (hasBrick(brickIndex(flash.x, flash.y))) {
- destroy(gameState.particles, index);
- }
- }
- });
- }
-
- if (
- gameState.combo > baseCombo(gameState) &&
- !isOptionOn("basic") &&
- (gameState.combo - baseCombo(gameState)) * Math.random() > 5
- ) {
- // The red should still be visible on a white bg
-
- if (gameState.perks.top_is_lava) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown +
- Math.random() * gameState.gameZoneWidthRoundedUp,
- 0,
- (Math.random() - 0.5) * 10,
- 5,
- "red",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
-
- if (gameState.perks.left_is_lava) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown,
- Math.random() * gameState.gameZoneHeight,
- 5,
- (Math.random() - 0.5) * 10,
- "red",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
-
- if (gameState.perks.right_is_lava) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp,
- Math.random() * gameState.gameZoneHeight,
- -5,
- (Math.random() - 0.5) * 10,
- "red",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
+ const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
+ const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
+ if (
+ coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
+ coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy &&
+ Math.abs(coin.x - gameState.puckPosition) <
+ coinRadius +
+ gameState.puckWidth / 2 + // a bit of margin to be nice
+ gameState.puckHeight
+ ) {
+ addToScore(gameState, coin);
+ destroy(gameState.coins, coinIndex);
+ } else if (coin.y > gameState.canvasHeight + coinRadius) {
+ destroy(gameState.coins, coinIndex);
if (gameState.perks.compound_interest) {
- let x = gameState.puckPosition,
- attemps = 0;
- do {
- x =
- gameState.offsetXRoundedDown +
- gameState.gameZoneWidthRoundedUp * Math.random();
- attemps++;
- } while (
- Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 &&
- attemps < 10
- );
+ resetCombo(gameState, coin.x, coin.y);
+ }
+ } else if (
+ gameState.perks.unbounded &&
+ (coin.x < -50 || coin.x > gameState.canvasWidth + 50)
+ ) {
+ // Out of bound on sides
+ destroy(gameState.coins, coinIndex);
+ }
- makeParticle(
- gameState,
- x,
- gameState.gameZoneHeight,
- (Math.random() - 0.5) * 10,
- -5,
- "red",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
+ const hitBrick = coinBrickHitCheck(gameState, coin);
+
+ if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
+ if (
+ gameState.bricks[hitBrick] &&
+ coin.color !== gameState.bricks[hitBrick] &&
+ gameState.bricks[hitBrick] !== "black" &&
+ !coin.coloredABrick
+ ) {
+ gameState.bricks[hitBrick] = coin.color;
+ coin.coloredABrick = true;
+
+ schedulGameSound(gameState, "colorChange", coin.x, 0.3);
}
- if (gameState.perks.streak_shots) {
- const pos = 0.5 - Math.random();
- makeParticle(
- gameState,
- gameState.puckPosition + gameState.puckWidth * pos,
- gameState.gameZoneHeight - gameState.puckHeight,
- pos * 10,
- -5,
- "red",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
+ }
+ if (typeof hitBrick !== "undefined" || hitBorder) {
+ coin.vx *= 0.8;
+ coin.vy *= 0.8;
+ coin.sa *= 0.9;
+ if (speed > 20) {
+ schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
}
+
+ if (Math.abs(coin.vy) < 3) {
+ coin.vy = 0;
+ }
+ }
+ });
+
+ gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
+
+ if (gameState.perks.shocks) {
+ gameState.balls.forEach((a, ai) =>
+ gameState.balls.forEach((b, bi) => {
+ if (
+ ai < bi &&
+ !a.destroyed &&
+ !b.destroyed &&
+ distance2(a, b) < gameState.ballSize * gameState.ballSize
+ ) {
+ 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 y = (a.y + b.y) / 2;
+ const limit = gameState.baseSpeed;
+ a.vx +=
+ clamp(a.x - x, -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);
+
+ explosionAt(gameState, index, x, y, a);
+ }
+ }),
+ );
}
- forEachLiveOne(gameState.particles, (p, pi) => {
- if (gameState.levelTime > p.time + p.duration) {
- destroy(gameState.particles, pi);
+ if (gameState.perks.wind) {
+ const windD =
+ ((gameState.puckPosition -
+ (gameState.offsetX + gameState.gameZoneWidth / 2)) /
+ gameState.gameZoneWidth) *
+ 2 *
+ gameState.perks.wind;
+ for (let i = 0; i < gameState.perks.wind; i++) {
+ if (Math.random() * Math.abs(windD) > 0.5) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown +
+ Math.random() * gameState.gameZoneWidthRoundedUp,
+ Math.random() * gameState.gameZoneHeight,
+ windD * 8,
+ 0,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 150,
+ );
}
- });
- forEachLiveOne(gameState.texts, (p, pi) => {
- if (gameState.levelTime > p.time + p.duration) {
- destroy(gameState.texts, pi);
- }
- });
- forEachLiveOne(gameState.lights, (p, pi) => {
- if (gameState.levelTime > p.time + p.duration) {
- destroy(gameState.lights, pi);
+ }
+ }
+ forEachLiveOne(gameState.particles, (flash, index) => {
+ flash.x += flash.vx * frames;
+ flash.y += flash.vy * frames;
+ if (!flash.ethereal) {
+ flash.vy += 0.5;
+ if (hasBrick(brickIndex(flash.x, flash.y))) {
+ destroy(gameState.particles, index);
}
+ }
});
+ }
+
+ if (
+ gameState.combo > baseCombo(gameState) &&
+ !isOptionOn("basic") &&
+ (gameState.combo - baseCombo(gameState)) * Math.random() > 5
+ ) {
+ // The red should still be visible on a white bg
+
+ if (gameState.perks.top_is_lava) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown +
+ Math.random() * gameState.gameZoneWidthRoundedUp,
+ 0,
+ (Math.random() - 0.5) * 10,
+ 5,
+ "red",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+
+ if (gameState.perks.left_is_lava) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown,
+ Math.random() * gameState.gameZoneHeight,
+ 5,
+ (Math.random() - 0.5) * 10,
+ "red",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+
+ if (gameState.perks.right_is_lava) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp,
+ Math.random() * gameState.gameZoneHeight,
+ -5,
+ (Math.random() - 0.5) * 10,
+ "red",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+
+ if (gameState.perks.compound_interest) {
+ let x = gameState.puckPosition,
+ attemps = 0;
+ do {
+ x =
+ gameState.offsetXRoundedDown +
+ gameState.gameZoneWidthRoundedUp * Math.random();
+ attemps++;
+ } while (
+ Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 &&
+ attemps < 10
+ );
+
+ makeParticle(
+ gameState,
+ x,
+ gameState.gameZoneHeight,
+ (Math.random() - 0.5) * 10,
+ -5,
+ "red",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+ if (gameState.perks.streak_shots) {
+ const pos = 0.5 - Math.random();
+ makeParticle(
+ gameState,
+ gameState.puckPosition + gameState.puckWidth * pos,
+ gameState.gameZoneHeight - gameState.puckHeight,
+ pos * 10,
+ -5,
+ "red",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+ }
+
+ forEachLiveOne(gameState.particles, (p, pi) => {
+ if (gameState.levelTime > p.time + p.duration) {
+ destroy(gameState.particles, pi);
+ }
+ });
+ forEachLiveOne(gameState.texts, (p, pi) => {
+ if (gameState.levelTime > p.time + p.duration) {
+ destroy(gameState.texts, pi);
+ }
+ });
+ forEachLiveOne(gameState.lights, (p, pi) => {
+ if (gameState.levelTime > p.time + p.duration) {
+ destroy(gameState.lights, pi);
+ }
+ });
}
export function ballTick(gameState: GameState, ball: Ball, delta: number) {
- ball.previousVX = ball.vx;
- ball.previousVY = ball.vy;
+ ball.previousVX = ball.vx;
+ ball.previousVY = ball.vy;
- let speedLimitDampener =
- 1 +
- gameState.perks.telekinesis +
- gameState.perks.ball_repulse_ball +
- gameState.perks.puck_repulse_ball +
- gameState.perks.ball_attract_ball;
+ let speedLimitDampener =
+ 1 +
+ gameState.perks.telekinesis +
+ gameState.perks.ball_repulse_ball +
+ gameState.perks.puck_repulse_ball +
+ gameState.perks.ball_attract_ball;
- if (isTelekinesisActive(gameState, ball)) {
- speedLimitDampener += 3;
- ball.vx +=
- ((gameState.puckPosition - ball.x) / 1000) *
- delta *
- gameState.perks.telekinesis;
- }
- if (isYoyoActive(gameState, ball)) {
- speedLimitDampener += 3;
- ball.vx +=
- ((gameState.puckPosition - ball.x) / 1000) *
- delta *
- gameState.perks.yoyo;
- }
+ if (isTelekinesisActive(gameState, ball)) {
+ speedLimitDampener += 3;
+ ball.vx +=
+ ((gameState.puckPosition - ball.x) / 1000) *
+ delta *
+ gameState.perks.telekinesis;
+ }
+ if (isYoyoActive(gameState, ball)) {
+ speedLimitDampener += 3;
+ ball.vx +=
+ ((gameState.puckPosition - ball.x) / 1000) * delta * gameState.perks.yoyo;
+ }
- if (
- ball.vx * ball.vx + ball.vy * ball.vy <
- gameState.baseSpeed * gameState.baseSpeed * 2
- ) {
- ball.vx *= 1 + 0.02 / speedLimitDampener;
- ball.vy *= 1 + 0.02 / speedLimitDampener;
- } else {
- ball.vx *= 1 - 0.02 / speedLimitDampener;
- ball.vy *= 1 - 0.02 / speedLimitDampener;
- }
- // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract
- if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) {
- ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener;
- }
+ if (
+ ball.vx * ball.vx + ball.vy * ball.vy <
+ gameState.baseSpeed * gameState.baseSpeed * 2
+ ) {
+ ball.vx *= 1 + 0.02 / speedLimitDampener;
+ ball.vy *= 1 + 0.02 / speedLimitDampener;
+ } else {
+ ball.vx *= 1 - 0.02 / speedLimitDampener;
+ ball.vy *= 1 - 0.02 / speedLimitDampener;
+ }
+ // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract
+ if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) {
+ ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener;
+ }
- if (gameState.perks.ball_repulse_ball) {
- for (let b2 of gameState.balls) {
- // avoid computing this twice, and repulsing itself
- if (b2.x >= ball.x) continue;
- repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true);
- }
+ if (gameState.perks.ball_repulse_ball) {
+ for (let b2 of gameState.balls) {
+ // avoid computing this twice, and repulsing itself
+ if (b2.x >= ball.x) continue;
+ repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true);
}
- if (gameState.perks.ball_attract_ball) {
- for (let b2 of gameState.balls) {
- // avoid computing this twice, and repulsing itself
- if (b2.x >= ball.x) continue;
- attract(gameState, ball, b2, gameState.perks.ball_attract_ball);
- }
+ }
+ if (gameState.perks.ball_attract_ball) {
+ for (let b2 of gameState.balls) {
+ // avoid computing this twice, and repulsing itself
+ if (b2.x >= ball.x) continue;
+ attract(gameState, ball, b2, gameState.perks.ball_attract_ball);
}
- if (
- gameState.perks.puck_repulse_ball &&
- Math.abs(ball.x - gameState.puckPosition) <
- gameState.puckWidth / 2 +
+ }
+ if (
+ gameState.perks.puck_repulse_ball &&
+ Math.abs(ball.x - gameState.puckPosition) <
+ gameState.puckWidth / 2 +
(gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10
+ ) {
+ repulse(
+ gameState,
+ ball,
+ {
+ x: gameState.puckPosition,
+ y: gameState.gameZoneHeight,
+ },
+ gameState.perks.puck_repulse_ball + 1,
+ false,
+ );
+ }
+
+ if (
+ gameState.perks.respawn &&
+ ball.hitItem?.length > 1 &&
+ !isOptionOn("basic")
+ ) {
+ for (
+ let i = 0;
+ i < ball.hitItem?.length - 1 && i < gameState.perks.respawn;
+ i++
) {
- repulse(
- gameState,
- ball,
- {
- x: gameState.puckPosition,
- y: gameState.gameZoneHeight,
- },
- gameState.perks.puck_repulse_ball + 1,
- false,
- );
+ const { index, color } = ball.hitItem[i];
+ if (gameState.bricks[index] || color === "black") continue;
+ const vertical = Math.random() > 0.5;
+ const dx = Math.random() > 0.5 ? 1 : -1;
+ const dy = Math.random() > 0.5 ? 1 : -1;
+
+ makeParticle(
+ gameState,
+ brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2,
+ brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2,
+ vertical ? 0 : -dx * gameState.baseSpeed,
+ vertical ? -dy * gameState.baseSpeed : 0,
+ color,
+ true,
+ gameState.coinSize / 2,
+ 250,
+ );
+ }
+ }
+
+ const borderHitCode = bordersHitCheck(
+ gameState,
+ ball,
+ gameState.ballSize / 2,
+ delta,
+ );
+ if (borderHitCode) {
+ if (
+ gameState.perks.left_is_lava &&
+ borderHitCode % 2 &&
+ ball.x < gameState.offsetX + gameState.gameZoneWidth / 2
+ ) {
+ resetCombo(gameState, ball.x, ball.y);
}
if (
- gameState.perks.respawn &&
- ball.hitItem?.length > 1 &&
- !isOptionOn("basic")
+ gameState.perks.right_is_lava &&
+ borderHitCode % 2 &&
+ ball.x > gameState.offsetX + gameState.gameZoneWidth / 2
) {
- for (
- let i = 0;
- i < ball.hitItem?.length - 1 && i < gameState.perks.respawn;
- i++
- ) {
- const {index, color} = ball.hitItem[i];
- if (gameState.bricks[index] || color === "black") continue;
- const vertical = Math.random() > 0.5;
- const dx = Math.random() > 0.5 ? 1 : -1;
- const dy = Math.random() > 0.5 ? 1 : -1;
-
- makeParticle(
- gameState,
- brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2,
- brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2,
- vertical ? 0 : -dx * gameState.baseSpeed,
- vertical ? -dy * gameState.baseSpeed : 0,
- color,
- true,
- gameState.coinSize / 2,
- 250,
- );
- }
+ resetCombo(gameState, ball.x, ball.y);
}
- const borderHitCode = bordersHitCheck(gameState, ball, gameState.ballSize / 2, delta);
- if (borderHitCode) {
- if (
- gameState.perks.left_is_lava &&
- borderHitCode % 2 &&
- ball.x < gameState.offsetX + gameState.gameZoneWidth / 2
- ) {
- resetCombo(gameState, ball.x, ball.y);
- }
-
- if (
- gameState.perks.right_is_lava &&
- borderHitCode % 2 &&
- ball.x > gameState.offsetX + gameState.gameZoneWidth / 2
- ) {
- resetCombo(gameState, ball.x, ball.y);
- }
-
- if (gameState.perks.top_is_lava && borderHitCode >= 2) {
- resetCombo(gameState, ball.x, ball.y + gameState.ballSize);
- }
-
- schedulGameSound(gameState, "wallBeep", ball.x, 1);
- gameState.levelWallBounces++;
- gameState.runStatistics.wall_bounces++;
+ if (gameState.perks.top_is_lava && borderHitCode >= 2) {
+ resetCombo(gameState, ball.x, ball.y + gameState.ballSize);
}
- // Puck collision
- const ylimit =
- gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2;
- const ballIsUnderPuck =
- Math.abs(ball.x - gameState.puckPosition) <
- gameState.ballSize / 2 + gameState.puckWidth / 2;
- if (
- ball.y > ylimit &&
- ball.vy > 0 &&
- (ballIsUnderPuck ||
- (gameState.perks.extra_life &&
- ball.y > ylimit + gameState.puckHeight / 2))
- ) {
- if (ballIsUnderPuck) {
- const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
- const angle = Math.atan2(
- -gameState.puckWidth / 2,
- (ball.x - gameState.puckPosition) *
- (gameState.perks.concave_puck ? -0.5 : 1),
- );
- ball.vx = speed * Math.cos(angle);
- ball.vy = speed * Math.sin(angle);
- schedulGameSound(gameState, "wallBeep", ball.x, 1);
+ schedulGameSound(gameState, "wallBeep", ball.x, 1);
+ gameState.levelWallBounces++;
+ gameState.runStatistics.wall_bounces++;
+ }
+
+ // Puck collision
+ const ylimit =
+ gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2;
+ const ballIsUnderPuck =
+ Math.abs(ball.x - gameState.puckPosition) <
+ gameState.ballSize / 2 + gameState.puckWidth / 2;
+ if (
+ ball.y > ylimit &&
+ ball.vy > 0 &&
+ (ballIsUnderPuck ||
+ (gameState.perks.extra_life &&
+ ball.y > ylimit + gameState.puckHeight / 2))
+ ) {
+ if (ballIsUnderPuck) {
+ const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
+ const angle = Math.atan2(
+ -gameState.puckWidth / 2,
+ (ball.x - gameState.puckPosition) *
+ (gameState.perks.concave_puck ? -0.5 : 1),
+ );
+ ball.vx = speed * Math.cos(angle);
+ ball.vy = speed * Math.sin(angle);
+ schedulGameSound(gameState, "wallBeep", ball.x, 1);
+ } else {
+ ball.vy *= -1;
+
+ gameState.perks.extra_life -= 1;
+ if (gameState.perks.extra_life < 0) {
+ gameState.perks.extra_life = 0;
+ } else if (gameState.perks.sacrifice) {
+ if (liveCount(gameState.coins) < gameState.MAX_COINS / 2) {
+ // true duplication
+ let remaining = liveCount(gameState.coins);
+
+ forEachLiveOne(gameState.coins, (source, index) => {
+ if (!remaining) return;
+ append(gameState.coins, (copy) => {
+ copy.points = source.points;
+ copy.color = source.color;
+ copy.x = source.x;
+ copy.y = source.y;
+ copy.size = source.size;
+ copy.previousX = source.previousX;
+ copy.previousY = source.previousY;
+ copy.vx = -source.vx;
+ copy.vy = -source.vy;
+ copy.sx = source.sx;
+ copy.sy = source.sy;
+ copy.a = source.a;
+ copy.sa = -source.sa;
+ copy.weight = source.weight;
+ copy.coloredABrick = source.coloredABrick;
+ });
+ remaining--;
+ });
} else {
- ball.vy *= -1;
-
- gameState.perks.extra_life -= 1
- if(gameState.perks.extra_life <0){
- gameState.perks.extra_life=0
- }else if(gameState.perks.sacrifice){
- if(liveCount(gameState.coins){
- if(!remaining) return
- append(gameState.coins, copy=>{
- copy.points=source.points
- copy.color=source.color
- copy.x=source.x
- copy.y=source.y
- copy.size=source.size
- copy.previousX=source.previousX
- copy.previousY=source.previousY
- copy.vx=-source.vx
- copy.vy=-source.vy
- copy.sx=source.sx
- copy.sy=source.sy
- copy.a=source.a
- copy.sa=-source.sa
- copy.weight=source.weight
- copy.coloredABrick=source.coloredABrick
- })
- remaining--
- })
- }else{
- forEachLiveOne(gameState.coins, (source, index)=>{
- source.points*=2
- })
- // spawn a few coins for effect, but mostly increment poitns counter
- }
- }
-
-
-
-
- schedulGameSound(gameState, "lifeLost", ball.x, 1);
- if (!isOptionOn("basic")) {
- for (let i = 0; i < 10; i++)
- makeParticle(
- gameState,
- ball.x,
- ball.y,
- Math.random() * gameState.baseSpeed * 3,
- gameState.baseSpeed * 3,
- "red",
- false,
- gameState.coinSize / 2,
- 150,
- );
- }
+ forEachLiveOne(gameState.coins, (source, index) => {
+ source.points *= 2;
+ });
+ // spawn a few coins for effect, but mostly increment poitns counter
}
- if (gameState.perks.streak_shots) {
- resetCombo(gameState, ball.x, ball.y);
- }
- if (gameState.perks.trampoline) {
- gameState.combo+=gameState.perks.trampoline
- }
- if (gameState.perks.nbricks) {
- if (ball.hitSinceBounce) {
- if (gameState.perks.nbricks === ball.hitSinceBounce) {
- gameState.combo += gameState.perks.nbricks
- } else {
- resetCombo(gameState, ball.x, ball.y)
- }
+ }
- }
+ schedulGameSound(gameState, "lifeLost", ball.x, 1);
+ if (!isOptionOn("basic")) {
+ for (let i = 0; i < 10; i++)
+ makeParticle(
+ gameState,
+ ball.x,
+ ball.y,
+ Math.random() * gameState.baseSpeed * 3,
+ gameState.baseSpeed * 3,
+ "red",
+ false,
+ gameState.coinSize / 2,
+ 150,
+ );
+ }
+ }
+ if (gameState.perks.streak_shots) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+ if (gameState.perks.trampoline) {
+ gameState.combo += gameState.perks.trampoline;
+ }
+ if (gameState.perks.nbricks) {
+ if (ball.hitSinceBounce) {
+ if (gameState.perks.nbricks === ball.hitSinceBounce) {
+ gameState.combo += gameState.perks.nbricks;
+ } else {
+ resetCombo(gameState, ball.x, ball.y);
}
-
- if (gameState.perks.respawn) {
- ball.hitItem
- .slice(0, -1)
- .slice(0, gameState.perks.respawn)
- .forEach(({index, color}) => {
- if (!gameState.bricks[index] && color !== "black")
- gameState.bricks[index] = color;
- });
- }
- ball.hitItem = [];
- if (!ball.hitSinceBounce) {
- gameState.runStatistics.misses++;
- gameState.levelMisses++;
- if(gameState.perks.forgiving){
- const indexes = gameState.bricks.map((b,i)=>b ? i:-1)
- .filter(i=>i>-1)
- const pick = sample(indexes)
- explodeBrick(gameState,pick, ball, false)
- }else{
- resetCombo(gameState, ball.x, ball.y);
- }
- makeText(
- gameState,
- gameState.puckPosition,
- gameState.gameZoneHeight - gameState.puckHeight * 2,
- "red",
- t("play.missed_ball"),
- gameState.puckHeight,
- 500,
- );
- }
- gameState.runStatistics.puck_bounces++;
- ball.hitSinceBounce = 0;
- ball.sapperUses = 0;
- ball.piercedSinceBounce = 0;
+ }
}
- const lostOnSides = gameState.perks.unbounded && ball.x < 50 || ball.x > gameState.canvasWidth + 50
- if (gameState.running &&
- (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides)
+ if (gameState.perks.respawn) {
+ ball.hitItem
+ .slice(0, -1)
+ .slice(0, gameState.perks.respawn)
+ .forEach(({ index, color }) => {
+ if (!gameState.bricks[index] && color !== "black")
+ gameState.bricks[index] = color;
+ });
+ }
+ ball.hitItem = [];
+ if (!ball.hitSinceBounce) {
+ gameState.runStatistics.misses++;
+ gameState.levelMisses++;
+ if (gameState.perks.forgiving) {
+ const indexes = gameState.bricks
+ .map((b, i) => (b ? i : -1))
+ .filter((i) => i > -1);
+ const pick = sample(indexes);
+ explodeBrick(gameState, pick, ball, false);
+ } else {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+ makeText(
+ gameState,
+ gameState.puckPosition,
+ gameState.gameZoneHeight - gameState.puckHeight * 2,
+ "red",
+ t("play.missed_ball"),
+ gameState.puckHeight,
+ 500,
+ );
+ }
+ gameState.runStatistics.puck_bounces++;
+ ball.hitSinceBounce = 0;
+ ball.sapperUses = 0;
+ ball.piercedSinceBounce = 0;
+ }
+ const lostOnSides =
+ (gameState.perks.unbounded && ball.x < 50) ||
+ ball.x > gameState.canvasWidth + 50;
+ if (
+ gameState.running &&
+ (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides)
+ ) {
+ ball.destroyed = true;
+ gameState.runStatistics.balls_lost++;
+ if (!gameState.balls.find((b) => !b.destroyed)) {
+ gameOver(
+ t("gameOver.lost.title"),
+ t("gameOver.lost.summary", { score: gameState.score }),
+ );
+ }
+ }
+ const radius = gameState.ballSize / 2;
+ // Make ball/coin bonce, and return bricks that were hit
+ 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;
+
+ const hitBrick = vhit ?? hhit ?? chit;
+ let sturdyBounce =
+ hitBrick &&
+ gameState.bricks[hitBrick] !== "black" &&
+ gameState.perks.sturdy_bricks &&
+ gameState.perks.sturdy_bricks > Math.random() * 5;
+
+ let pierce = false;
+ if (sturdyBounce || typeof hitBrick === "undefined") {
+ // cannot pierce
+ } else if (shouldPierceByColor(gameState, vhit, hhit, chit)) {
+ pierce = true;
+ } else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) {
+ pierce = true;
+ ball.piercedSinceBounce++;
+ }
+
+ if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
+ if (!pierce) {
+ ball.y = ball.previousY;
+ ball.vy *= -1;
+ }
+ }
+ if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
+ if (!pierce) {
+ ball.x = ball.previousX;
+ ball.vx *= -1;
+ }
+ }
+
+ if (sturdyBounce) {
+ schedulGameSound(gameState, "wallBeep", x, 1);
+ return;
+ }
+ if (typeof hitBrick !== "undefined") {
+ const initialBrickColor = gameState.bricks[hitBrick];
+
+ ball.hitSinceBounce++;
+ explodeBrick(gameState, hitBrick, ball, false);
+
+ if (
+ ball.sapperUses < gameState.perks.sapper &&
+ initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
+ !gameState.bricks[hitBrick]
) {
- ball.destroyed = true;
- gameState.runStatistics.balls_lost++;
- if (!gameState.balls.find((b) => !b.destroyed)) {
- gameOver(
- t("gameOver.lost.title"),
- t("gameOver.lost.summary", {score: gameState.score}),
- );
- }
+ gameState.bricks[hitBrick] = "black";
+ ball.sapperUses++;
}
- const radius = gameState.ballSize / 2;
- // Make ball/coin bonce, and return bricks that were hit
- 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;
+ if (!isOptionOn("basic")) {
+ const remainingPierce =
+ gameState.perks.pierce * 3 - ball.piercedSinceBounce;
+ const remainingSapper = ball.sapperUses < gameState.perks.sapper;
+ const extraCombo = gameState.combo - 1;
+ if (
+ (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) ||
+ (remainingSapper && Math.random() > 0.1 / (1 + remainingSapper)) ||
+ (extraCombo && Math.random() > 0.1 / (1 + extraCombo))
+ ) {
+ const color = remainingSapper
+ ? Math.random() > 0.5
+ ? "orange"
+ : "red"
+ : gameState.ballsColor;
- const hitBrick = vhit ?? hhit ?? chit;
- let sturdyBounce =
- hitBrick &&
- gameState.bricks[hitBrick] !== "black" &&
- gameState.perks.sturdy_bricks &&
- gameState.perks.sturdy_bricks > Math.random() * 5;
-
- let pierce = false;
- if (sturdyBounce || typeof hitBrick === "undefined") {
- // cannot pierce
- } else if (shouldPierceByColor(gameState, vhit, hhit, chit)) {
- pierce = true;
- } else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) {
- pierce = true;
- ball.piercedSinceBounce++;
- }
-
- if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
- if (!pierce) {
- ball.y = ball.previousY;
- ball.vy *= -1;
- }
- }
- if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
- if (!pierce) {
- ball.x = ball.previousX;
- ball.vx *= -1;
- }
- }
-
- if (sturdyBounce) {
- schedulGameSound(gameState, "wallBeep", x, 1);
- return;
- }
- if (typeof hitBrick !== "undefined") {
- const initialBrickColor = gameState.bricks[hitBrick];
-
- ball.hitSinceBounce++;
- explodeBrick(gameState, hitBrick, ball, false);
-
- if (
- ball.sapperUses < gameState.perks.sapper &&
- initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
- !gameState.bricks[hitBrick]
- ) {
- gameState.bricks[hitBrick] = "black";
- ball.sapperUses++;
- }
- }
-
- if (!isOptionOn("basic")) {
- const remainingPierce =
- gameState.perks.pierce * 3 - ball.piercedSinceBounce;
- const remainingSapper = ball.sapperUses < gameState.perks.sapper;
- const extraCombo = gameState.combo - 1;
- if (
- (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) ||
- (remainingSapper && Math.random() > 0.1 / (1 + remainingSapper)) ||
- (extraCombo && Math.random() > 0.1 / (1 + extraCombo))
- ) {
- const color = remainingSapper
- ? Math.random() > 0.5
- ? "orange"
- : "red"
- : gameState.ballsColor;
-
- makeParticle(
- gameState,
- ball.x,
- ball.y,
- gameState.perks.pierce_color || remainingPierce
- ? -ball.vx + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
- : (Math.random() - 0.5) * gameState.baseSpeed,
- gameState.perks.pierce_color || remainingPierce
- ? -ball.vy + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
- : (Math.random() - 0.5) * gameState.baseSpeed,
- color,
- true,
- gameState.coinSize / 2,
- 100,
- );
- }
+ makeParticle(
+ gameState,
+ ball.x,
+ ball.y,
+ gameState.perks.pierce_color || remainingPierce
+ ? -ball.vx + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
+ : (Math.random() - 0.5) * gameState.baseSpeed,
+ gameState.perks.pierce_color || remainingPierce
+ ? -ball.vy + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
+ : (Math.random() - 0.5) * gameState.baseSpeed,
+ color,
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
}
+ }
}
function makeCoin(
- gameState: GameState,
- x: number,
- y: number,
- vx: number,
- vy: number,
- color = "gold",
- points = 1,
+ gameState: GameState,
+ x: number,
+ y: number,
+ vx: number,
+ vy: number,
+ color = "gold",
+ points = 1,
) {
- append(gameState.coins, (p: Partial) => {
- p.x = x;
- p.y = y;
- p.size = gameState.coinSize;
- p.previousX = x;
- p.previousY = y;
- p.vx = vx;
- p.vy = vy;
- p.sx = 0;
- p.sy = 0;
- p.color = color;
- p.a = Math.random() * Math.PI * 2;
- p.sa = Math.random() - 0.5;
- p.weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
- p.points = points;
- });
+ append(gameState.coins, (p: Partial) => {
+ p.x = x;
+ p.y = y;
+ p.size = gameState.coinSize;
+ p.previousX = x;
+ p.previousY = y;
+ p.vx = vx;
+ p.vy = vy;
+ p.sx = 0;
+ p.sy = 0;
+ p.color = color;
+ p.a = Math.random() * Math.PI * 2;
+ p.sa = Math.random() - 0.5;
+ p.weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
+ p.points = points;
+ });
}
function makeParticle(
- gameState: GameState,
- x: number,
- y: number,
- vx: number,
- vy: number,
- color: colorString,
- ethereal = false,
- size = 8,
- duration = 150,
+ gameState: GameState,
+ x: number,
+ y: number,
+ vx: number,
+ vy: number,
+ color: colorString,
+ ethereal = false,
+ size = 8,
+ duration = 150,
) {
- append(gameState.particles, (p: Partial) => {
- p.time = gameState.levelTime;
- p.x = x;
- p.y = y;
- p.vx = vx;
- p.vy = vy;
- p.color = color;
- p.size = size;
- p.duration = duration;
- p.ethereal = ethereal;
- });
+ append(gameState.particles, (p: Partial) => {
+ p.time = gameState.levelTime;
+ p.x = x;
+ p.y = y;
+ p.vx = vx;
+ p.vy = vy;
+ p.color = color;
+ p.size = size;
+ p.duration = duration;
+ p.ethereal = ethereal;
+ });
}
function makeText(
- gameState: GameState,
- x: number,
- y: number,
- color: colorString,
- text: string,
- size = 20,
- duration = 150,
+ gameState: GameState,
+ x: number,
+ y: number,
+ color: colorString,
+ text: string,
+ size = 20,
+ duration = 150,
) {
- append(gameState.texts, (p: Partial) => {
- p.time = gameState.levelTime;
- p.x = x;
- p.y = y;
- p.color = color;
- p.size = size;
- p.duration = duration;
- p.text = text;
- });
+ append(gameState.texts, (p: Partial) => {
+ p.time = gameState.levelTime;
+ p.x = x;
+ p.y = y;
+ p.color = color;
+ p.size = size;
+ p.duration = duration;
+ p.text = text;
+ });
}
function makeLight(
- gameState: GameState,
- x: number,
- y: number,
- color: colorString,
- size = 8,
- duration = 150,
+ gameState: GameState,
+ x: number,
+ y: number,
+ color: colorString,
+ size = 8,
+ duration = 150,
) {
- append(gameState.lights, (p: Partial) => {
- p.time = gameState.levelTime;
- p.x = x;
- p.y = y;
- p.color = color;
- p.size = size;
- p.duration = duration;
- });
+ append(gameState.lights, (p: Partial) => {
+ p.time = gameState.levelTime;
+ p.x = x;
+ p.y = y;
+ p.color = color;
+ p.size = size;
+ p.duration = duration;
+ });
}
export function append(
- where: ReusableArray,
- makeItem: (match: Partial) => void,
+ where: ReusableArray,
+ makeItem: (match: Partial) => void,
) {
- while (
- where.list[where.indexMin] &&
- !where.list[where.indexMin].destroyed &&
- where.indexMin < where.list.length
- ) {
- where.indexMin++;
- }
- if (where.indexMin < where.list.length) {
- where.list[where.indexMin].destroyed = false;
- makeItem(where.list[where.indexMin]);
- where.indexMin++;
- } else {
- const p = {destroyed: false};
- makeItem(p);
- where.list.push(p);
- }
- where.total++;
+ while (
+ where.list[where.indexMin] &&
+ !where.list[where.indexMin].destroyed &&
+ where.indexMin < where.list.length
+ ) {
+ where.indexMin++;
+ }
+ if (where.indexMin < where.list.length) {
+ where.list[where.indexMin].destroyed = false;
+ makeItem(where.list[where.indexMin]);
+ where.indexMin++;
+ } else {
+ const p = { destroyed: false };
+ makeItem(p);
+ where.list.push(p);
+ }
+ where.total++;
}
export function destroy(where: ReusableArray, index: number) {
- if (where.list[index].destroyed) return;
- where.list[index].destroyed = true;
- where.indexMin = Math.min(where.indexMin, index);
- where.total--;
+ if (where.list[index].destroyed) return;
+ where.list[index].destroyed = true;
+ where.indexMin = Math.min(where.indexMin, index);
+ where.total--;
}
export function liveCount(where: ReusableArray) {
- return where.total;
+ return where.total;
}
export function empty(where: ReusableArray) {
- where.total = 0;
- where.indexMin = 0;
- where.list.forEach((i) => (i.destroyed = true));
+ where.total = 0;
+ where.indexMin = 0;
+ where.list.forEach((i) => (i.destroyed = true));
}
export function forEachLiveOne(
- where: ReusableArray,
- cb: (t: T, index: number) => void,
+ where: ReusableArray,
+ cb: (t: T, index: number) => void,
) {
- where.list.forEach((item: T, index: number) => {
- if (item && !item.destroyed) {
- cb(item, index);
- }
- });
+ where.list.forEach((item: T, index: number) => {
+ if (item && !item.destroyed) {
+ cb(item, index);
+ }
+ });
}
diff --git a/src/game_utils.ts b/src/game_utils.ts
index c87d256..ec07e26 100644
--- a/src/game_utils.ts
+++ b/src/game_utils.ts
@@ -1,5 +1,5 @@
-import {Ball, GameState, PerkId, PerksMap} from "./types";
-import {icons, upgrades} from "./loadGameData";
+import { Ball, GameState, PerkId, PerksMap } from "./types";
+import { icons, upgrades } from "./loadGameData";
export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {};
@@ -72,10 +72,10 @@ export function currentLevelInfo(gameState: GameState) {
}
export function isTelekinesisActive(gameState: GameState, ball: Ball) {
- return gameState.perks.telekinesis && ball.vy < 0;
+ return gameState.perks.telekinesis && ball.vy < 0;
}
export function isYoyoActive(gameState: GameState, ball: Ball) {
- return gameState.perks.yoyo && ball.vy > 0;
+ return gameState.perks.yoyo && ball.vy > 0;
}
export function findLast(
@@ -103,9 +103,8 @@ export function distanceBetween(
return Math.sqrt(distance2(a, b));
}
-
-export function clamp(value, min, max){
- return Math.max(min, Math.min(value, max))
+export function clamp(value: number, min: number, max: number) {
+ return Math.max(min, Math.min(value, max));
}
export function defaultSounds() {
return {
@@ -123,29 +122,29 @@ export function defaultSounds() {
}
export function shouldPierceByColor(
- gameState: GameState,
- vhit: number | undefined,
- hhit: number | undefined,
- chit: number | undefined,
+ gameState: GameState,
+ vhit: number | undefined,
+ hhit: number | undefined,
+ chit: number | undefined,
) {
- if (!gameState.perks.pierce_color) return false;
- if (
- typeof vhit !== "undefined" &&
- gameState.bricks[vhit] !== gameState.ballsColor
- ) {
- return false;
- }
- if (
- typeof hhit !== "undefined" &&
- gameState.bricks[hhit] !== gameState.ballsColor
- ) {
- return false;
- }
- if (
- typeof chit !== "undefined" &&
- gameState.bricks[chit] !== gameState.ballsColor
- ) {
- return false;
- }
- return true;
-}
\ No newline at end of file
+ if (!gameState.perks.pierce_color) return false;
+ if (
+ typeof vhit !== "undefined" &&
+ gameState.bricks[vhit] !== gameState.ballsColor
+ ) {
+ return false;
+ }
+ if (
+ typeof hhit !== "undefined" &&
+ gameState.bricks[hhit] !== gameState.ballsColor
+ ) {
+ return false;
+ }
+ if (
+ typeof chit !== "undefined" &&
+ gameState.bricks[chit] !== gameState.ballsColor
+ ) {
+ return false;
+ }
+ return true;
+}
diff --git a/src/render.ts b/src/render.ts
index 3113cea..6c4ca60 100644
--- a/src/render.ts
+++ b/src/render.ts
@@ -1,26 +1,26 @@
-import {baseCombo, forEachLiveOne, liveCount} from "./gameStateMutators";
+import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
import {
- brickCenterX,
- brickCenterY,
- currentLevelInfo,
- isTelekinesisActive,
- isYoyoActive,
- max_levels,
+ brickCenterX,
+ brickCenterY,
+ currentLevelInfo,
+ isTelekinesisActive,
+ isYoyoActive,
+ max_levels,
} from "./game_utils";
-import {colorString, GameState} from "./types";
-import {t} from "./i18n/i18n";
-import {gameState} from "./game";
-import {isOptionOn} from "./options";
+import { colorString, GameState } from "./types";
+import { t } from "./i18n/i18n";
+import { gameState } from "./game";
+import { isOptionOn } from "./options";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
export const ctx = gameCanvas.getContext("2d", {
- alpha: false,
+ alpha: false,
}) as CanvasRenderingContext2D;
export const bombSVG = document.createElement("img");
bombSVG.src =
- "data:image/svg+xml;base64," +
- btoa(`