Looping mode

This commit is contained in:
Renan LE CARO 2025-03-28 19:40:59 +01:00
parent 3d5547e786
commit 5012076039
21 changed files with 2852 additions and 2696 deletions

View file

@ -14,33 +14,39 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131) - [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
# Todo
- people assume unbounded allows for wrap around
- coin magnet and viscosity : only one level ~2.5
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
- wind : move coins based on puck movement not position
- show -N points in red when combo resets
- reach : this is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
- respawn: N% of bricks respawn after N seconds
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
- [jaceys] A visual indication of whether a ball has hit a brick this serve
- [obigre] Offer to level ups perks separately
- bring back detailed help of perks as "intel"
- https://weblate.org/fr/
# Premium: allow looping # Premium: allow looping
Allow players to loop the game : Allow players to loop the game :
- [x] keep your score - [x] keep your score
- [x] keep 1 perk - [x] keep 1 perk
- [x] add one hasard - [x] add one hasard
- [ ] add one HP to all bricks - [x] add one HP to all bricks - as a debuff
- [ ] advertise looping in normal game over screen - [ ] advertise looping in normal game over screen
- [ ] save score at the end of first loop, in addition to the final one ? - real time stats as the option says.
- [ ] check that stats like max level are correct - [x] Noise of coins against side is annoying.
- Change look of loop, to avoid picking randomly at loop end.
- make red coins scarier,
- add blue coins that only freeze puck.
- Make fullscreen an option and turn it back on when playing
- +1 combo de base par rerolls
- +1 combo de base par vie restantes (pas attrapable)
# Todo
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
- people assume unbounded allows for wrap around
- coin magnet and viscosity : only one level ~2.5
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
- wind : move coins based on puck movement not position
- show -N points in red when combo resets
- reach is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
- respawn: N% of bricks respawn after N seconds
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
- [jaceys] A visual indication of whether a ball has hit a brick this serve
- [obigre] Offer to level ups perks separately
- bring back detailed help of perks as "intel"
- https://weblate.org/fr/
# System requirements # System requirements

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29050375 versionCode = 29053110
versionName = "29050375" versionName = "29053110"
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

102
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
// The version of the cache. // The version of the cache.
const VERSION = "29050375"; const VERSION = "29053110";
// 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 @@
"29050375" "29053110"

View file

@ -1,11 +1,11 @@
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {Debuff} from "./types"; import { Debuff } from "./types";
export const debuffs = [ export const debuffs = [
{ {
id: "negative_coins", id: "negative_coins",
max: 20, max: 20,
name: (lvl: number) => t("debuffs.negative_coins.help",{lvl}), name: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }), help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
}, },
{ {
@ -17,8 +17,10 @@ export const debuffs = [
{ {
id: "banned", id: "banned",
max: 50, max: 50,
name: (lvl: number,banned:string) => t("debuffs.banned.description",{lvl,banned}), name: (lvl: number, banned: string) =>
help: (lvl: number,perk:string) => t("debuffs.banned.help", { lvl,perk }), t("debuffs.banned.description", { lvl, banned }),
help: (lvl: number, perk: string) =>
t("debuffs.banned.help", { lvl, perk }),
}, },
{ {
id: "interference", id: "interference",
@ -30,8 +32,14 @@ export const debuffs = [
{ {
id: "fragility", id: "fragility",
max: 5, max: 5,
name: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }), name: (lvl: number) => t("debuffs.fragility.help", { percent: lvl * 20 }),
help: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }), help: (lvl: number) => t("debuffs.fragility.help", { percent: lvl * 20 }),
},
{
id: "sturdiness",
max: 5,
name: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
help: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
}, },
] as const as Debuff[]; ] as const as Debuff[];

View file

@ -14,7 +14,8 @@ import {
import { getAudioContext, playPendingSounds } from "./sounds"; import { getAudioContext, playPendingSounds } from "./sounds";
import { import {
bannedUpgradesHTMl, bannedUpgradesHTMl,
currentLevelInfo, debuffsHTMl, currentLevelInfo,
debuffsHTMl,
getRowColIndex, getRowColIndex,
levelsListHTMl, levelsListHTMl,
max_levels, max_levels,
@ -447,15 +448,14 @@ document.addEventListener("visibilitychange", () => {
async function openScorePanel() { async function openScorePanel() {
pause(true); pause(true);
const cb = await asyncAlert({ const cb = await asyncAlert({
title: title: gameState.loop
gameState.loop ? ? t("score_panel.title_looped", {
t("score_panel.title_looped", { loop: gameState.loop,
loop:gameState.loop,
score: gameState.score, score: gameState.score,
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState), max: max_levels(gameState),
}): })
t("score_panel.title", { : t("score_panel.title", {
score: gameState.score, score: gameState.score,
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState), max: max_levels(gameState),
@ -1013,22 +1013,23 @@ restart(
// // unbounded: 1, // // unbounded: 1,
// // pierce_color: 1, // // pierce_color: 1,
// pierce: 1, // pierce: 1,
streak_shots:1, // streak_shots: 1,
// multiball: 6, // multiball: 6,
// base_combo: 7, base_combo: 7,
// telekinesis: 2, telekinesis: 2,
// yoyo: 2, yoyo: 2,
pierce:10, pierce: 10,
// metamorphosis: 1, // metamorphosis: 1,
// implosions: 1, // implosions: 1,
// sturdy_bricks:5 // sturdy_bricks:5
extra_life:3 coin_magnet:2,
extra_life: 3,
}, },
debuffs:{ debuffs: {
// fragility:3 // fragility:3
negative_coins:1 negative_coins: 100,
// interference:20, // interference:20,
} },
}) || }) ||
{}, {},
); );

View file

@ -136,7 +136,7 @@ export function gameOver(title: string, intro: string) {
], ],
}).then(() => }).then(() =>
restart({ restart({
levelToAvoid: currentLevelInfo(gameState).name levelToAvoid: currentLevelInfo(gameState).name,
}), }),
); );
} }
@ -271,6 +271,7 @@ export function getHistograms() {
(r) => r.max_combo, (r) => r.max_combo,
"", "",
); );
runStats += makeHistogram(t("gameOver.stats.loops"), (r) => r.loops, "");
if (runStats) { if (runStats) {
runStats = runStats =

View file

@ -2,7 +2,10 @@ import {
Ball, Ball,
BallLike, BallLike,
Coin, Coin,
colorString, Debuff, DebuffId, Debuffs, colorString,
Debuff,
DebuffId,
Debuffs,
GameState, GameState,
LightFlash, LightFlash,
ParticleFlash, ParticleFlash,
@ -30,12 +33,16 @@ import {
sample, sample,
shouldPierceByColor, shouldPierceByColor,
} from "./game_utils"; } from "./game_utils";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {icons, upgrades} from "./loadGameData"; import { icons, upgrades } from "./loadGameData";
import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings"; import {
import {background} from "./render"; addToTotalScore,
import {gameOver} from "./gameOver"; getCurrentMaxCoins,
getCurrentMaxParticles,
} from "./settings";
import { background } from "./render";
import { gameOver } from "./gameOver";
import { import {
brickIndex, brickIndex,
fitSize, fitSize,
@ -46,12 +53,12 @@ import {
pause, pause,
play, play,
} from "./game"; } from "./game";
import {stopRecording} from "./recording"; import { stopRecording } from "./recording";
import {isOptionOn} from "./options"; import { isOptionOn } from "./options";
import {isPremium} from "./premium"; import { isPremium } from "./premium";
import {getRunLevels} from "./newGameState"; import { getRunLevels } from "./newGameState";
import {debuffs} from "./debuffs"; import { debuffs } from "./debuffs";
import {requiredAsyncAlert} from "./asyncAlert"; import { requiredAsyncAlert } from "./asyncAlert";
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
@ -175,7 +182,7 @@ export function normalizeGameState(gameState: GameState) {
} }
export function baseCombo(gameState: GameState) { export function baseCombo(gameState: GameState) {
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5; return gameState.baseCombo + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
} }
export function resetCombo( export function resetCombo(
@ -333,15 +340,14 @@ export function explosionAt(
if (gameState.perks.zen) { if (gameState.perks.zen) {
resetCombo(gameState, x, y); resetCombo(gameState, x, y);
} }
if(gameState.debuffs.fragility){ if (gameState.debuffs.fragility) {
resetCombo(gameState, x, y); resetCombo(gameState, x, y);
forEachLiveOne(gameState.coins, (coin, index)=>{ forEachLiveOne(gameState.coins, (coin, index) => {
// Also destroys cursed coins // Also destroys cursed coins
if(Math.random()<gameState.debuffs.fragility/5){ if (Math.random() < gameState.debuffs.fragility / 5) {
destroy(gameState.coins, index) destroy(gameState.coins, index);
} }
}) });
} }
} }
@ -395,7 +401,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;
} }
@ -413,7 +419,9 @@ export function explodeBrick(
cy, cy,
ball.previousVX * (0.5 + Math.random()), ball.previousVX * (0.5 + Math.random()),
ball.previousVY * (0.5 + Math.random()), ball.previousVY * (0.5 + Math.random()),
gameState.perks.metamorphosis || isOptionOn('colorful_coins') ? color : "gold", gameState.perks.metamorphosis || isOptionOn("colorful_coins")
? color
: "gold",
points, points,
); );
} }
@ -554,14 +562,11 @@ export function addToScore(gameState: GameState, coin: Coin) {
gameState.score += coin.points; gameState.score += coin.points;
gameState.lastScoreIncrease = gameState.levelTime; gameState.lastScoreIncrease = gameState.levelTime;
addToTotalScore(gameState, coin.points); addToTotalScore(gameState, coin.points);
if ( if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) {
gameState.score > gameState.highScore &&
!gameState.isCreativeModeRun
) {
gameState.highScore = gameState.score; gameState.highScore = gameState.score;
localStorage.setItem("breakout-3-hs", gameState.score.toString()); localStorage.setItem("breakout-3-hs", gameState.score.toString());
} }
if (!isOptionOn("basic") ) { if (!isOptionOn("basic")) {
makeParticle( makeParticle(
gameState, gameState,
coin.previousX, coin.previousX,
@ -583,46 +588,67 @@ export function addToScore(gameState: GameState, coin: Coin) {
} }
export async function gotoNextLoop(gameState: GameState) { export async function gotoNextLoop(gameState: GameState) {
pause(false) pause(false);
gameState.loop++ gameState.loop++;
gameState.runStatistics.loops++ gameState.runStatistics.loops++;
gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {}) gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {});
gameState.upgradesOfferedFor = -1 gameState.upgradesOfferedFor = -1;
// Add random debuf
// gameState.debuffs[randomDebuff]++ let comboText=''
const userPerks=upgrades.filter(u => gameState.perks[u.id]) if(gameState.rerolls) {
const {keep, comboText=t('loop.converted_rerolls',{n:gameState.rerolls})
debuff, gameState.baseCombo += gameState.rerolls
targetPerk} = await requiredAsyncAlert<{ keep:PerkId, debuff:DebuffId, targetPerk:PerkId }>({ gameState.rerolls=0
title: t('loop.title', {loop: gameState.loop}), }else{
comboText=t('loop.no_rerolls')
}
const userPerks = upgrades.filter((u) => gameState.perks[u.id]);
const { keep, debuff, targetPerk } = await requiredAsyncAlert<{
keep: PerkId;
debuff: DebuffId;
targetPerk: PerkId;
}>({
title: t("loop.title", { loop: gameState.loop }),
content: [ content: [
t('loop.instructions'), t("loop.instructions"),
...userPerks comboText,
.map(u => {
const randomDebuff = sample(debuffs.filter(d => gameState.debuffs[d.id] < d.max)) || sample(debuffs); ...userPerks.map((u) => {
const targetPerk = sample(userPerks.filter(tp=>tp.id!==u.id)) const randomDebuff =
return ({ sample(debuffs.filter((d) => gameState.debuffs[d.id] < d.max)) ||
text: u.name + t('level_up.upgrade_perk_to_level', {level: gameState.perks[u.id]}), sample(debuffs);
help: randomDebuff.help(gameState.debuffs[randomDebuff.id]+1, targetPerk.name), const targetPerk = sample(userPerks.filter((tp) => tp.id !== u.id));
return {
text:
u.name +
t("level_up.upgrade_perk_to_level", {
level: gameState.perks[u.id],
}),
help: randomDebuff.help(
gameState.debuffs[randomDebuff.id] + 1,
targetPerk.name,
),
icon: u.icon, icon: u.icon,
value: { value: {
keep: u.id, keep: u.id,
debuff: randomDebuff.id, debuff: randomDebuff.id,
targetPerk:targetPerk.id targetPerk: targetPerk.id,
} },
}) };
}) }),
] ],
}) });
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {[keep]: gameState.perks[keep]}) Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {
gameState.debuffs[debuff]++ [keep]: gameState.perks[keep],
if(debuff=='banned'){ });
gameState.bannedPerks[targetPerk]++ gameState.debuffs[debuff]++;
if (debuff == "banned") {
gameState.bannedPerks[targetPerk]++;
} }
await setLevel(gameState, 0) await setLevel(gameState, 0);
} }
export async function setLevel(gameState: GameState, l: number) { export async function setLevel(gameState: GameState, l: number) {
@ -635,7 +661,6 @@ export async function setLevel(gameState: GameState, l: number) {
pause(false); pause(false);
stopRecording(); stopRecording();
if (l > 0) { if (l > 0) {
await openShortRunUpgradesPicker(gameState); await openShortRunUpgradesPicker(gameState);
} }
gameState.currentLevel = l; gameState.currentLevel = l;
@ -685,10 +710,7 @@ export async function setLevel(gameState: GameState, l: number) {
while (attemps < 100 && changed < gameState.debuffs.more_bombs) { while (attemps < 100 && changed < gameState.debuffs.more_bombs) {
attemps++; attemps++;
const index = Math.floor(Math.random() * gameState.bricks.length); const index = Math.floor(Math.random() * gameState.bricks.length);
if ( if (gameState.bricks[index] && gameState.bricks[index] !== "black") {
gameState.bricks[index] &&
gameState.bricks[index] !== "black"
) {
gameState.bricks[index] = "black"; gameState.bricks[index] = "black";
gameState.brickHP[index] = 1; gameState.brickHP[index] = 1;
changed++; changed++;
@ -707,7 +729,8 @@ function setBrick(gameState: GameState, index: number, color: string) {
gameState.bricks[index] = color || ""; gameState.bricks[index] = color || "";
gameState.brickHP[index] = gameState.brickHP[index] =
(color === "black" && 1) || (color === "black" && 1) ||
(color && 1 + gameState.perks.sturdy_bricks+gameState.loop) || (color &&
1 + gameState.perks.sturdy_bricks + gameState.debuffs.sturdiness) ||
0; 0;
} }
@ -823,7 +846,7 @@ 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) {
// 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);
@ -882,7 +905,7 @@ export function bordersHitCheck(
(gameState.offsetX + gameState.gameZoneWidth / 2)) / (gameState.offsetX + gameState.gameZoneWidth / 2)) /
gameState.gameZoneWidth) * gameState.gameZoneWidth) *
gameState.perks.wind * gameState.perks.wind *
0.5 ; 0.5;
} }
let vhit = 0, let vhit = 0,
@ -974,28 +997,22 @@ export function gameStateTick(
} }
if ( if (
gameState.running && (gameState.running &&
// Delayed win when coins are still flying // Delayed win when coins are still flying
(gameState.winAt && gameState.levelTime > gameState.winAt) || gameState.winAt &&
gameState.levelTime > gameState.winAt) ||
// instant win condition // instant win condition
( (gameState.levelTime && !remainingBricks && !liveCount(gameState.coins))
gameState.levelTime &&
!remainingBricks &&
!liveCount(gameState.coins))
) {
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 {
if (isPremium()) { if (isPremium()) {
gotoNextLoop(gameState) gotoNextLoop(gameState);
} 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 }),
); );
} }
} }
@ -1030,7 +1047,11 @@ export function gameStateTick(
}); });
} }
const ratio = 1 - ((coin.color==='crimson' ? 2:gameState.perks.viscosity)* 0.03 + 0.005) * frames; const ratio =
1 -
((coin.color === "crimson" ? 3 : gameState.perks.viscosity) * 0.03 +
0.005) *
frames;
coin.vy *= ratio; coin.vy *= ratio;
coin.vx *= ratio; coin.vx *= ratio;
@ -1064,6 +1085,21 @@ export function gameStateTick(
} }
} }
if(coin.color === "crimson" && !isOptionOn('basic')){
const angle=Math.random()*Math.PI*2
makeParticle(
gameState,
coin.x,
coin.y,
Math.cos(angle)*gameState.baseSpeed*2,
Math.sin(angle)*gameState.baseSpeed*2,
'red',
true,
5,
250,
);
}
const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10; const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames); const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
@ -1076,15 +1112,15 @@ export function gameStateTick(
// a bit of margin to be nice , negative in case it's a negative coin // a bit of margin to be nice , negative in case it's a negative coin
gameState.puckHeight * (coin.points ? 1 : -1) gameState.puckHeight * (coin.points ? 1 : -1)
) { ) {
if(coin.points) { if (coin.points) {
addToScore(gameState, coin); addToScore(gameState, coin);
}else if(gameState.perks.extra_life && gameState.balls.length){ } else if (gameState.perks.extra_life && gameState.balls.length) {
justLostALife(gameState, gameState.balls[0], coin.x,coin.y) justLostALife(gameState, gameState.balls[0], coin.x, coin.y);
}else{ } else {
gameOver( gameOver(
t('gameOver.because_cursed_coin'), t("gameOver.because_cursed_coin"),
t('gameOver.because_cursed_coin_intro') t("gameOver.because_cursed_coin_intro"),
) );
} }
destroy(gameState.coins, coinIndex); destroy(gameState.coins, coinIndex);
} else if (coin.y > gameState.canvasHeight + coinRadius) { } else if (coin.y > gameState.canvasHeight + coinRadius) {
@ -1102,10 +1138,7 @@ export function gameStateTick(
} }
const hitBrick = coinBrickHitCheck(gameState, coin); const hitBrick = coinBrickHitCheck(gameState, coin);
if ( if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
gameState.perks.metamorphosis &&
typeof hitBrick !== "undefined"
) {
if ( if (
gameState.bricks[hitBrick] && gameState.bricks[hitBrick] &&
coin.color !== gameState.bricks[hitBrick] && coin.color !== gameState.bricks[hitBrick] &&
@ -1127,20 +1160,21 @@ export function gameStateTick(
coin.vx *= 0.8; coin.vx *= 0.8;
coin.vy *= 0.8; coin.vy *= 0.8;
coin.sa *= 0.9; coin.sa *= 0.9;
if (speed > 20) { if (speed > 20 && !coin.collidedLastFrame) {
schedulGameSound(gameState, "coinBounce", coin.x, 0.2); schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
} }
coin.collidedLastFrame = true;
if (Math.abs(coin.vy) < 3) { if (Math.abs(coin.vy) < 3) {
coin.vy = 0; coin.vy = 0;
} }
} else {
coin.collidedLastFrame = false;
} }
}); });
gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
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) => {
@ -1307,7 +1341,6 @@ export function gameStateTick(
} }
} }
forEachLiveOne(gameState.particles, (p, pi) => { forEachLiveOne(gameState.particles, (p, pi) => {
if (gameState.levelTime > p.time + p.duration) { if (gameState.levelTime > p.time + p.duration) {
destroy(gameState.particles, pi); destroy(gameState.particles, pi);
@ -1341,15 +1374,16 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
ball.vx += ball.vx +=
((gameState.puckPosition - ball.x) / 1000) * ((gameState.puckPosition - ball.x) / 1000) *
delta * delta *
gameState.perks.telekinesis gameState.perks.telekinesis *
* interferenceFactor(gameState) interferenceFactor(gameState);
;
} }
if (isYoyoActive(gameState, ball)) { if (isYoyoActive(gameState, ball)) {
speedLimitDampener += 3; speedLimitDampener += 3;
ball.vx += ball.vx +=
((gameState.puckPosition - ball.x) / 1000) * delta * gameState.perks.yoyo ((gameState.puckPosition - ball.x) / 1000) *
* interferenceFactor(gameState); delta *
gameState.perks.yoyo *
interferenceFactor(gameState);
} }
if ( if (
ball.vx * ball.vx + ball.vy * ball.vy < ball.vx * ball.vx + ball.vy * ball.vy <
@ -1408,7 +1442,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;
@ -1493,8 +1527,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
schedulGameSound(gameState, "wallBeep", ball.x, 1); schedulGameSound(gameState, "wallBeep", ball.x, 1);
} else { } else {
ball.vy *= -1; ball.vy *= -1;
justLostALife(gameState, ball, ball.x,ball.y) justLostALife(gameState, ball, ball.x, ball.y);
} }
if (gameState.perks.streak_shots) { if (gameState.perks.streak_shots) {
resetCombo(gameState, ball.x, ball.y); resetCombo(gameState, ball.x, ball.y);
@ -1513,7 +1546,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") {
// respawns with full hp // respawns with full hp
setBrick(gameState, index, color); setBrick(gameState, index, color);
@ -1563,13 +1596,13 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
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);
@ -1629,11 +1662,16 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
setBrick(gameState, hitBrick, "black"); setBrick(gameState, hitBrick, "black");
ball.sapperUses++; ball.sapperUses++;
} }
}else{ } else {
schedulGameSound(gameState, 'wallBeep',x,1) schedulGameSound(gameState, "wallBeep", x, 1);
makeLight(gameState, brickCenterX(gameState, hitBrick), makeLight(
brickCenterY(gameState,hitBrick), "white", gameState.brickWidth+2 , gameState,
50*gameState.brickHP[hitBrick]); brickCenterX(gameState, hitBrick),
brickCenterY(gameState, hitBrick),
"white",
gameState.brickWidth + 2,
50 * gameState.brickHP[hitBrick],
);
} }
} }
@ -1671,7 +1709,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
} }
} }
function justLostALife(gameState:GameState, ball:Ball, x:number,y:number){ function justLostALife(gameState: GameState, ball: Ball, x: number, y: number) {
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;
@ -1697,7 +1735,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
150, 150,
); );
} }
} }
function makeCoin( function makeCoin(
gameState: GameState, gameState: GameState,
@ -1708,13 +1746,17 @@ function makeCoin(
color = "gold", color = "gold",
points = 1, points = 1,
) { ) {
if (gameState.debuffs.negative_coins *points> Math.random() * 10000) { if (y<gameState.gameZoneWidth*2/3 &&
gameState.debuffs.negative_coins * points > Math.random() * 10000) {
points = 0; points = 0;
color = "crimson"; color = "crimson";
vx=0
vy=0
} }
append(gameState.coins, (p: Partial<Coin>) => { append(gameState.coins, (p: Partial<Coin>) => {
p.x = x; p.x = x;
p.y = y; p.y = y;
p.collidedLastFrame = true;
p.size = gameState.coinSize; p.size = gameState.coinSize;
p.previousX = x; p.previousX = x;
p.previousY = y; p.previousY = y;
@ -1730,11 +1772,11 @@ function makeCoin(
}); });
} }
export function interferenceFactor(gameState:GameState){ export function interferenceFactor(gameState: GameState) {
if(!gameState.debuffs.interference) return 1 if (!gameState.debuffs.interference) return 1;
const cycleLength = (7+gameState.debuffs.interference)*1000 const cycleLength = (7 + gameState.debuffs.interference) * 1000;
const position = gameState.levelTime % cycleLength const position = gameState.levelTime % cycleLength;
return position>7000 ? -1 :1 return position > 7000 ? -1 : 1;
} }
function makeParticle( function makeParticle(
@ -1815,7 +1857,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,7 +1,7 @@
import { Ball, GameState, PerkId, PerksMap } from "./types"; import { Ball, GameState, PerkId, PerksMap } from "./types";
import { icons, upgrades } from "./loadGameData"; import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {debuffs} from "./debuffs"; import { debuffs } from "./debuffs";
export function getMajorityValue(arr: string[]): string { export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {}; const count: { [k: string]: number } = {};
@ -55,7 +55,6 @@ export function getPossibleUpgrades(gameState: GameState) {
} }
export function max_levels(gameState: GameState) { export function max_levels(gameState: GameState) {
return 7 + gameState.perks.extra_levels; return 7 + gameState.perks.extra_levels;
} }
@ -70,10 +69,15 @@ export function pickedUpgradesHTMl(gameState: GameState) {
return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`; return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`;
} }
export function debuffsHTMl(gameState: GameState): string {
export function debuffsHTMl(gameState: GameState):string { const banned = upgrades
const banned = upgrades.filter(u=>gameState.bannedPerks[u.id]).map(u=>u.name).join(', ') .filter((u) => gameState.bannedPerks[u.id])
let list = debuffs.filter(d=>gameState.debuffs[d.id]).map(d=>d.name(gameState.debuffs[d.id], banned)).join(' '); .map((u) => u.name)
.join(", ");
let list = debuffs
.filter((d) => gameState.debuffs[d.id])
.map((d) => d.name(gameState.debuffs[d.id], banned))
.join(" ");
if (!list) return ""; if (!list) return "";
return `<p>${t("score_panel.bebuffs_list")} ${list}</p>`; return `<p>${t("score_panel.bebuffs_list")} ${list}</p>`;

View file

@ -202,6 +202,26 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>sturdiness</name>
<children>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@ -470,6 +490,21 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>loops</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>total_score</name> <name>total_score</name>
<description/> <description/>
@ -787,6 +822,21 @@
<folder_node> <folder_node>
<name>loop</name> <name>loop</name>
<children> <children>
<concept_node>
<name>converted_rerolls</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>instructions</name> <name>instructions</name>
<description/> <description/>
@ -802,6 +852,21 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>no_rerolls</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>title</name> <name>title</name>
<description/> <description/>

View file

@ -9,6 +9,7 @@
"debuffs.interference.help": "Telekinesis and yo-yo glitch for {{lvl}}s every 7s.", "debuffs.interference.help": "Telekinesis and yo-yo glitch for {{lvl}}s every 7s.",
"debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs.", "debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs.",
"debuffs.negative_coins.help": "{{lvl}}/10000 of coins spawn cursed, blinking red. Game over if you catch them.", "debuffs.negative_coins.help": "{{lvl}}/10000 of coins spawn cursed, blinking red. Game over if you catch them.",
"debuffs.sturdiness.help": "All bricks have +{{lvl}} HP",
"gameOver.because_cursed_coin": "Game over", "gameOver.because_cursed_coin": "Game over",
"gameOver.because_cursed_coin_intro": "You cough a cursed coin (bright red coins) and didn't have a extra life to spare. ", "gameOver.because_cursed_coin_intro": "You cough a cursed coin (bright red coins) and didn't have a extra life to spare. ",
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.", "gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
@ -26,6 +27,7 @@
"gameOver.stats.hit_rate": "Hit rate", "gameOver.stats.hit_rate": "Hit rate",
"gameOver.stats.intro": "Find below your run statistics compared to your {{count}} best runs.", "gameOver.stats.intro": "Find below your run statistics compared to your {{count}} best runs.",
"gameOver.stats.level_reached": "Level reached", "gameOver.stats.level_reached": "Level reached",
"gameOver.stats.loops": "Loops",
"gameOver.stats.total_score": "Total score", "gameOver.stats.total_score": "Total score",
"gameOver.stats.upgrades_applied": "Upgrades applied", "gameOver.stats.upgrades_applied": "Upgrades applied",
"gameOver.test_run": "This test run and its score are not being recorded", "gameOver.test_run": "This test run and its score are not being recorded",
@ -46,7 +48,9 @@
"level_up.unlocked_level": " (Level)", "level_up.unlocked_level": " (Level)",
"level_up.unlocked_perk": " (Perk)", "level_up.unlocked_perk": " (Perk)",
"level_up.upgrade_perk_to_level": " lvl {{level}}", "level_up.upgrade_perk_to_level": " lvl {{level}}",
"loop.converted_rerolls": "Your {{n}} leftover re-rolls where converted to +{{n}} base combo.",
"loop.instructions": "All your perks will be erased except one that you can pick below. Each option comes with an additional hazard will appear on all levels going forward. ", "loop.instructions": "All your perks will be erased except one that you can pick below. Each option comes with an additional hazard will appear on all levels going forward. ",
"loop.no_rerolls": "You didn't have any leftover re-rolls, so your base combo stayed the same. ",
"loop.title": "Starting loop {{loop}}", "loop.title": "Starting loop {{loop}}",
"main_menu.basic": "Basic graphics", "main_menu.basic": "Basic graphics",
"main_menu.basic_help": "Better performance.", "main_menu.basic_help": "Better performance.",

View file

@ -9,6 +9,7 @@
"debuffs.interference.help": "Télékinésie et problème de yo-yo pendant {{lvl}}s toutes les 7 s.", "debuffs.interference.help": "Télékinésie et problème de yo-yo pendant {{lvl}}s toutes les 7 s.",
"debuffs.more_bombs.help": "{{lvl}} briques remplacées par des bombes.", "debuffs.more_bombs.help": "{{lvl}} briques remplacées par des bombes.",
"debuffs.negative_coins.help": "{{lvl}}/10000 pièces apparaissent maudites et clignotent en rouge. La partie est terminée si vous les attrapez.", "debuffs.negative_coins.help": "{{lvl}}/10000 pièces apparaissent maudites et clignotent en rouge. La partie est terminée si vous les attrapez.",
"debuffs.sturdiness.help": "Toutes les briques résistent à +{{lvl}} chocs",
"gameOver.because_cursed_coin": "Jeu terminé", "gameOver.because_cursed_coin": "Jeu terminé",
"gameOver.because_cursed_coin_intro": "Vous avez craché une pièce maudite (pièces rouge vif) et vous n'aviez pas de vie supplémentaire à revendre.", "gameOver.because_cursed_coin_intro": "Vous avez craché une pièce maudite (pièces rouge vif) et vous n'aviez pas de vie supplémentaire à revendre.",
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.", "gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
@ -26,6 +27,7 @@
"gameOver.stats.hit_rate": "Précision", "gameOver.stats.hit_rate": "Précision",
"gameOver.stats.intro": "Vous trouverez ci-dessous les statistiques de cette partie comparées à vos {{count}} meilleures parties.", "gameOver.stats.intro": "Vous trouverez ci-dessous les statistiques de cette partie comparées à vos {{count}} meilleures parties.",
"gameOver.stats.level_reached": "Niveau atteint", "gameOver.stats.level_reached": "Niveau atteint",
"gameOver.stats.loops": "Boucles",
"gameOver.stats.total_score": "Score total", "gameOver.stats.total_score": "Score total",
"gameOver.stats.upgrades_applied": "Mises à jour appliquées", "gameOver.stats.upgrades_applied": "Mises à jour appliquées",
"gameOver.test_run": "Cette partie de test et son score ne sont pas enregistrés.", "gameOver.test_run": "Cette partie de test et son score ne sont pas enregistrés.",
@ -46,7 +48,9 @@
"level_up.unlocked_level": " (Niveau)", "level_up.unlocked_level": " (Niveau)",
"level_up.unlocked_perk": " (Amélioration)", "level_up.unlocked_perk": " (Amélioration)",
"level_up.upgrade_perk_to_level": " niveau {{level}}", "level_up.upgrade_perk_to_level": " niveau {{level}}",
"loop.converted_rerolls": "",
"loop.instructions": "Tous vos avantages seront supprimés, sauf un que vous pouvez choisir ci-dessous. Chaque option comporte un danger supplémentaire qui apparaîtra à tous les niveaux.", "loop.instructions": "Tous vos avantages seront supprimés, sauf un que vous pouvez choisir ci-dessous. Chaque option comporte un danger supplémentaire qui apparaîtra à tous les niveaux.",
"loop.no_rerolls": "",
"loop.title": "Boucle de départ {{loop}}", "loop.title": "Boucle de départ {{loop}}",
"main_menu.basic": "Graphismes simplifiés", "main_menu.basic": "Graphismes simplifiés",
"main_menu.basic_help": "Meilleures performances.", "main_menu.basic_help": "Meilleures performances.",

View file

@ -45,5 +45,5 @@ export const allLevels = rawLevelsList
export const upgrades = rawUpgrades.map((u) => ({ export const upgrades = rawUpgrades.map((u) => ({
...u, ...u,
icon: icons["icon:" + u.id] icon: icons["icon:" + u.id],
})) as Upgrade[]; })) as Upgrade[];

View file

@ -11,10 +11,9 @@ import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
import { debuffs } from "./debuffs"; import { debuffs } from "./debuffs";
export function getRunLevels(totalScoreAtRunStart: number, params: RunParams) {
export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){ const firstLevel = params?.level
const firstLevel = ? allLevels.filter((l) => l.name === params?.level)
params?.level ? allLevels.filter((l) => l.name === params?.level)
: []; : [];
const restInRandomOrder = allLevels const restInRandomOrder = allLevels
@ -23,7 +22,7 @@ export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
.filter((l) => l.name !== params?.levelToAvoid) .filter((l) => l.name !== params?.levelToAvoid)
.sort(() => Math.random() - 0.5); .sort(() => Math.random() - 0.5);
return firstLevel.concat( return firstLevel.concat(
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey), restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
); );
} }
@ -31,7 +30,7 @@ return firstLevel.concat(
export function newGameState(params: RunParams): GameState { export function newGameState(params: RunParams): GameState {
const totalScoreAtRunStart = getTotalScore(); const totalScoreAtRunStart = getTotalScore();
const runLevels =getRunLevels(totalScoreAtRunStart, params) const runLevels = getRunLevels(totalScoreAtRunStart, params);
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) }; const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
@ -41,7 +40,7 @@ export function newGameState(params: RunParams): GameState {
currentLevel: 0, currentLevel: 0,
upgradesOfferedFor: -1, upgradesOfferedFor: -1,
perks, perks,
bannedPerks:makeEmptyPerksMap(upgrades), bannedPerks: makeEmptyPerksMap(upgrades),
debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) }, debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) },
puckWidth: 200, puckWidth: 200,
baseSpeed: 12, baseSpeed: 12,
@ -110,7 +109,8 @@ export function newGameState(params: RunParams): GameState {
autoCleanUses: 0, autoCleanUses: 0,
...defaultSounds(), ...defaultSounds(),
rerolls: 0, rerolls: 0,
loop:0 loop: 0,
baseCombo: 1,
}; };
resetBalls(gameState); resetBalls(gameState);

View file

@ -1,9 +1,9 @@
import {GameState} from "./types"; import { GameState } from "./types";
import {icons} from "./loadGameData"; import { icons } from "./loadGameData";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {getSettingValue, setSettingValue} from "./settings"; import { getSettingValue, setSettingValue } from "./settings";
import {asyncAlert} from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import {openMainMenu} from "./game"; import { openMainMenu } from "./game";
const publicKeyString = `-----BEGIN PUBLIC KEY----- const publicKeyString = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
@ -94,32 +94,32 @@ export function premiumMenuEntry(gameState: GameState) {
text: t("premium.thanks"), text: t("premium.thanks"),
help: t("premium.thanks_help"), help: t("premium.thanks_help"),
value: async () => { value: async () => {
navigator.clipboard.writeText(getSettingValue('license', '')) navigator.clipboard.writeText(getSettingValue("license", ""));
openMainMenu() openMainMenu();
}, },
}; };
} }
let text = t("premium.title") let text = t("premium.title");
let help = t("premium.buy") let help = t("premium.buy");
try { try {
const timePlayed = localStorage.getItem('breakout_71_total_play_time') const timePlayed = localStorage.getItem("breakout_71_total_play_time");
if (timePlayed && !isGooglePlayInstall) { if (timePlayed && !isGooglePlayInstall) {
const hours = parseFloat(timePlayed) / 1000 / 60 / 60 const hours = parseFloat(timePlayed) / 1000 / 60 / 60;
const pricePerHours = 4.99 / hours const pricePerHours = 4.99 / hours;
const args = { const args = {
hours: Math.floor(hours), hours: Math.floor(hours),
pricePerHours: pricePerHours.toFixed(2) pricePerHours: pricePerHours.toFixed(2),
} };
if (pricePerHours > 0 && pricePerHours < 0.5) { if (pricePerHours > 0 && pricePerHours < 0.5) {
text = t("premium.per_hours", args) text = t("premium.per_hours", args);
help = t("premium.per_hours_help", args) help = t("premium.per_hours_help", args);
} }
console.log({args}) console.log({ args });
} }
} catch (e) { } catch (e) {
console.warn(e) console.warn(e);
} }
return { return {
@ -131,12 +131,9 @@ export function premiumMenuEntry(gameState: GameState) {
} }
const isGooglePlayInstall = const isGooglePlayInstall =
new URLSearchParams(location.search).get("source") === new URLSearchParams(location.search).get("source") === "com.android.vending";
"com.android.vending";
async function openPremiumMenu(text) { async function openPremiumMenu(text) {
const cb = await asyncAlert({ const cb = await asyncAlert({
title: t("premium.title"), title: t("premium.title"),
content: [ content: [
@ -160,11 +157,11 @@ async function openPremiumMenu(text) {
text: t("premium.enter"), text: t("premium.enter"),
help: t("premium.enter_help"), help: t("premium.enter_help"),
async value() { async value() {
const value = (prompt("Please paste your license key") || "").replace( const value = (
/\s+/g, prompt("Please paste your license key") || ""
"", )?.replace(/\s+/g, "");
);
const problem = await checkKey(value); const problem = await checkKey(value || "");
if (problem) { if (problem) {
openPremiumMenu(problem).then(); openPremiumMenu(problem).then();
} else { } else {

View file

@ -52,9 +52,7 @@ export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
recordCanvasCtx.textAlign = "left"; recordCanvasCtx.textAlign = "left";
recordCanvasCtx.fillText( recordCanvasCtx.fillText(
"Level " + "Level " + (gameState.currentLevel + 1) + "/" + max_levels(gameState),
(gameState.currentLevel + 1) +
"/" + max_levels(gameState),
12, 12,
12, 12,
); );

View file

@ -1,4 +1,9 @@
import {baseCombo, forEachLiveOne, interferenceFactor, liveCount} from "./gameStateMutators"; import {
baseCombo,
forEachLiveOne,
interferenceFactor,
liveCount,
} from "./gameStateMutators";
import { import {
brickCenterX, brickCenterX,
brickCenterY, brickCenterY,
@ -9,10 +14,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", {
@ -32,15 +37,17 @@ export function render(gameState: GameState) {
const level = currentLevelInfo(gameState); const level = currentLevelInfo(gameState);
const hasCombo = gameState.combo > baseCombo(gameState); const hasCombo = gameState.combo > baseCombo(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) {
menuLabel.innerText = gameState.loop ? t("play.current_lvl_loop", { menuLabel.innerText = gameState.loop
? t("play.current_lvl_loop", {
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState), max: max_levels(gameState),
loop: gameState.loop loop: gameState.loop,
}) : t("play.current_lvl", { })
: t("play.current_lvl", {
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState), max: max_levels(gameState),
}); });
@ -91,7 +98,7 @@ export function render(gameState: GameState) {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
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);
@ -134,7 +141,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);
@ -162,7 +169,7 @@ export function render(gameState: GameState) {
// Coins // Coins
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
forEachLiveOne(gameState.coins, (coin) => { forEachLiveOne(gameState.coins, (coin) => {
ctx.globalCompositeOperation = 'source-over' ctx.globalCompositeOperation = "source-over";
// ctx.globalCompositeOperation = // ctx.globalCompositeOperation =
// coin.color === "gold" || level.color ? "source-over" : "screen"; // coin.color === "gold" || level.color ? "source-over" : "screen";
drawCoin( drawCoin(
@ -195,52 +202,43 @@ export function render(gameState: GameState) {
if (gameState.debuffs.negative_coins) { if (gameState.debuffs.negative_coins) {
// Render crimson coins very bright // Render crimson coins very bright
ctx.globalCompositeOperation = 'source-over' ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 0.8 ctx.globalAlpha = 0.8;
const red = Math.floor(gameState.levelTime / 100) % 2 > 0 const red = Math.floor(gameState.levelTime / 100) % 2 > 0;
forEachLiveOne(gameState.coins, (coin) => { forEachLiveOne(gameState.coins, (coin) => {
if (coin.color !== 'crimson') return if (coin.color !== "crimson") return;
drawBall( drawBall(ctx, red ? "red" : "black", coin.size * 3, coin.x, coin.y);
ctx,
red ? 'red' : 'black',
coin.size * 3,
coin.x,
coin.y
);
}); });
ctx.globalAlpha = 1 ctx.globalAlpha = 1;
forEachLiveOne(gameState.coins, (coin) => { forEachLiveOne(gameState.coins, (coin) => {
if (coin.color !== 'crimson') return if (coin.color !== "crimson") return;
drawCoin( drawCoin(
ctx, ctx,
!red ? 'red' : 'black', !red ? "red" : "black",
coin.size, coin.size,
coin.x, coin.x,
coin.y, coin.y,
'red', "red",
coin.a coin.a,
); );
}); });
} }
} }
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
renderAllBricks(); renderAllBricks();
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
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) * 0.5; ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5;
drawBrick(ctx, color, x, y, -1) drawBrick(ctx, color, x, y, -1);
}); });
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";
@ -248,7 +246,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";
@ -289,15 +287,13 @@ export function render(gameState: GameState) {
); );
if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) { if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight); ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
if (interferenceFactor(gameState) == -1) { if (interferenceFactor(gameState) == -1) {
ctx.lineWidth = 2 ctx.lineWidth = 2;
ctx.strokeStyle = 'red' ctx.strokeStyle = "red";
ctx.setLineDash(redBorderDash) ctx.setLineDash(redBorderDash);
ctx.lineDashOffset = getDashOffset(gameState) ctx.lineDashOffset = getDashOffset(gameState);
} else { } else {
ctx.strokeStyle = gameState.puckColor; ctx.strokeStyle = gameState.puckColor;
} }
@ -311,8 +307,8 @@ export function render(gameState: GameState) {
); );
ctx.stroke(); ctx.stroke();
ctx.lineWidth = 2 ctx.lineWidth = 2;
ctx.setLineDash(emptyArray) ctx.setLineDash(emptyArray);
} }
if (gameState.perks.clairvoyant && gameState.ballStickToPuck) { if (gameState.perks.clairvoyant && gameState.ballStickToPuck) {
ctx.strokeStyle = gameState.ballsColor; ctx.strokeStyle = gameState.ballsColor;

12
src/types.d.ts vendored
View file

@ -83,6 +83,7 @@ export type Coin = {
sa: number; sa: number;
weight: number; weight: number;
destroyed?: boolean; destroyed?: boolean;
collidedLastFrame?: boolean;
coloredABrick?: boolean; coloredABrick?: boolean;
}; };
export type Ball = { export type Ball = {
@ -155,12 +156,12 @@ export type PerksMap = {
[k in PerkId]: number; [k in PerkId]: number;
}; };
type Debuff={ type Debuff = {
id: DebuffId; id: DebuffId;
max:number; max: number;
name:(lvl: number,banned:string)=>string; name: (lvl: number, banned: string) => string;
help:(lvl: number,perk:string)=>string; help: (lvl: number, perk: string) => string;
} };
export type DebuffId = (typeof debuffs)[number]["id"]; export type DebuffId = (typeof debuffs)[number]["id"];
export type DebuffsMap = { export type DebuffsMap = {
@ -287,6 +288,7 @@ export type GameState = {
}; };
rerolls: number; rerolls: number;
loop: number; loop: number;
baseCombo: number;
}; };
export type RunParams = { export type RunParams = {