Looping works, almost

This commit is contained in:
Renan LE CARO 2025-03-28 11:58:58 +01:00
parent 46f87556e1
commit 3d5547e786
12 changed files with 1116 additions and 918 deletions

View file

@ -158,7 +158,6 @@ There's also an easy mode for kids (slower ball).
- [colin] piggy bank - bricks absorb coins that fall onto it, and release them back as they are broken, with added value : equivalent to Asceticism - [colin] piggy bank - bricks absorb coins that fall onto it, and release them back as they are broken, with added value : equivalent to Asceticism
- [colin] ball coins - coins share the same physics as coins and bounce on walls and bricks : really hard to balance with speeds and all - [colin] ball coins - coins share the same physics as coins and bounce on walls and bricks : really hard to balance with speeds and all
- non brick-shaped bricks, tilted bricks,moving blocks : very difficult because of engine optimisations - non brick-shaped bricks, tilted bricks,moving blocks : very difficult because of engine optimisations
- transparents coins, why ?
- 3 random perks immediately, or maybe "all level get twice as many upgrades, but they are applied randomly, and you aren't told which ones you have." - 3 random perks immediately, or maybe "all level get twice as many upgrades, but they are applied randomly, and you aren't told which ones you have."
- coins repulse coins, could get really laggy ? - coins repulse coins, could get really laggy ?
- russian roulette: 5/6 chances to get a free upgrade, 1/6 chance of game over. Not really fun - russian roulette: 5/6 chances to get a free upgrade, 1/6 chance of game over. Not really fun

129
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -27,19 +27,21 @@ export const debuffs = [
help: (lvl: number) => t("debuffs.interference.help", { lvl }), help: (lvl: number) => t("debuffs.interference.help", { lvl }),
}, },
{
id: "fragility",
max: 5,
name: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }),
help: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }),
},
] as const as Debuff[]; ] as const as Debuff[];
/* /*
Possible challenges : Possible challenges :
- interference : telekinesis works backward for lvl/2 seconds every 5 seconds (show timer ?)
- exclusion : one of your current perks (except the kept one) is banned - exclusion : one of your current perks (except the kept one) is banned
- fireworks : some bricks are explosive, you're not told which ones - fireworks : some bricks are explosive, you're not told which ones
-
- graphical effects like trail, contrast, blur to make it harder to see what's going on
- ball creates a draft behind itself that blows coins in odd patterns - ball creates a draft behind itself that blows coins in odd patterns
- bricks are invisible
- downward wind - downward wind
- side wind - side wind
- add red anti-coins that apply downgrades - add red anti-coins that apply downgrades

View file

@ -1008,22 +1008,26 @@ restart(
(window.location.search.includes("stressTest") && { (window.location.search.includes("stressTest") && {
level: "Bird", level: "Bird",
perks: { perks: {
// sapper: 1, // sapper: 5,
// bigger_explosions: 20, // bigger_explosions: 20,
// // unbounded: 1, // // unbounded: 1,
// // pierce_color: 1, // // pierce_color: 1,
// pierce: 1, // pierce: 1,
streak_shots:1,
// multiball: 6, // multiball: 6,
// base_combo: 7, // base_combo: 7,
telekinesis: 2, // telekinesis: 2,
yoyo: 2, // yoyo: 2,
pierce:10,
// metamorphosis: 1, // metamorphosis: 1,
// implosions: 1, // implosions: 1,
// sturdy_bricks:5 // sturdy_bricks:5
extra_life:3
}, },
debuffs:{ debuffs:{
// fragility:3
negative_coins:1
// interference:20,
} }
}) || }) ||
{}, {},

View file

@ -143,12 +143,12 @@ export function gameOver(title: string, intro: string) {
export function getHistograms() { export function getHistograms() {
let runStats = ""; let runStats = "";
// TODO separate adventure and normal runs
try { try {
// Stores only top 100 runs // Stores only top 100 runs
let runsHistory = JSON.parse( let runsHistory = JSON.parse(
localStorage.getItem("breakout_71_runs_history") || "[]", localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[]; ) as RunHistoryItem[];
runsHistory.sort((a, b) => a.score - b.score).reverse(); runsHistory.sort((a, b) => a.score - b.score).reverse();
runsHistory = runsHistory.slice(0, 100); runsHistory = runsHistory.slice(0, 100);

View file

@ -36,7 +36,16 @@ import {icons, upgrades} from "./loadGameData";
import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings"; import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings";
import {background} from "./render"; import {background} from "./render";
import {gameOver} from "./gameOver"; import {gameOver} from "./gameOver";
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openShortRunUpgradesPicker, pause,} from "./game"; import {
brickIndex,
fitSize,
gameState,
hasBrick,
hitsSomething,
openShortRunUpgradesPicker,
pause,
play,
} 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";
@ -324,6 +333,16 @@ export function explosionAt(
if (gameState.perks.zen) { if (gameState.perks.zen) {
resetCombo(gameState, x, y); resetCombo(gameState, x, y);
} }
if(gameState.debuffs.fragility){
resetCombo(gameState, x, y);
forEachLiveOne(gameState.coins, (coin, index)=>{
// Also destroys cursed coins
if(Math.random()<gameState.debuffs.fragility/5){
destroy(gameState.coins, index)
}
})
}
} }
export function explodeBrick( export function explodeBrick(
@ -335,14 +354,14 @@ export function explodeBrick(
const color = gameState.bricks[index]; const color = gameState.bricks[index];
if (!color) return; if (!color) return;
if (color === "black" || color === "transparent") { if (color === "black") {
const x = brickCenterX(gameState, index), const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index); y = brickCenterY(gameState, index);
if (color === "transparent") { // if (color === "transparent") {
schedulGameSound(gameState, "void", x, 1); // schedulGameSound(gameState, "void", x, 1);
resetCombo(gameState, x, y); // resetCombo(gameState, x, y);
} // }
setBrick(gameState, index, ""); setBrick(gameState, index, "");
explosionAt(gameState, index, x, y, ball); explosionAt(gameState, index, x, y, ball);
} else if (color) { } else if (color) {
@ -534,7 +553,6 @@ export function schedulGameSound(
export function addToScore(gameState: GameState, coin: Coin) { 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.score > gameState.highScore &&
@ -543,7 +561,7 @@ export function addToScore(gameState: GameState, coin: Coin) {
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,
@ -557,11 +575,7 @@ export function addToScore(gameState: GameState, coin: Coin) {
); );
} }
if (coin.points > 0) {
schedulGameSound(gameState, "coinCatch", coin.x, 1); schedulGameSound(gameState, "coinCatch", coin.x, 1);
} else {
resetCombo(gameState, coin.x, coin.y);
}
gameState.runStatistics.score += coin.points; gameState.runStatistics.score += coin.points;
if (gameState.perks.asceticism) { if (gameState.perks.asceticism) {
resetCombo(gameState, coin.x, coin.y); resetCombo(gameState, coin.x, coin.y);
@ -571,6 +585,7 @@ 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.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {}) gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {})
gameState.upgradesOfferedFor = -1 gameState.upgradesOfferedFor = -1
// Add random debuf // Add random debuf
@ -867,7 +882,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,
@ -1015,7 +1030,7 @@ export function gameStateTick(
}); });
} }
const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames; const ratio = 1 - ((coin.color==='crimson' ? 2:gameState.perks.viscosity)* 0.03 + 0.005) * frames;
coin.vy *= ratio; coin.vy *= ratio;
coin.vx *= ratio; coin.vx *= ratio;
@ -1061,7 +1076,16 @@ 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) {
addToScore(gameState, coin); addToScore(gameState, coin);
}else if(gameState.perks.extra_life && gameState.balls.length){
justLostALife(gameState, gameState.balls[0], coin.x,coin.y)
}else{
gameOver(
t('gameOver.because_cursed_coin'),
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) {
destroy(gameState.coins, coinIndex); destroy(gameState.coins, coinIndex);
@ -1317,12 +1341,15 @@ 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)
;
} }
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) * delta * gameState.perks.yoyo
* interferenceFactor(gameState);
} }
if ( if (
ball.vx * ball.vx + ball.vy * ball.vy < ball.vx * ball.vx + ball.vy * ball.vy <
@ -1466,31 +1493,8 @@ 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)
gameState.perks.extra_life -= 1;
if (gameState.perks.extra_life < 0) {
gameState.perks.extra_life = 0;
} else if (gameState.perks.sacrifice) {
gameState.bricks.forEach(
(color, index) => color && explodeBrick(gameState, index, ball, true),
);
}
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) { if (gameState.perks.streak_shots) {
resetCombo(gameState, ball.x, ball.y); resetCombo(gameState, ball.x, ball.y);
@ -1667,6 +1671,34 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
} }
} }
function justLostALife(gameState:GameState, ball:Ball, x:number,y:number){
gameState.perks.extra_life -= 1;
if (gameState.perks.extra_life < 0) {
gameState.perks.extra_life = 0;
} else if (gameState.perks.sacrifice) {
gameState.bricks.forEach(
(color, index) => color && explodeBrick(gameState, index, ball, true),
);
}
schedulGameSound(gameState, "lifeLost", ball.x, 1);
if (!isOptionOn("basic")) {
for (let i = 0; i < 10; i++)
makeParticle(
gameState,
x,
y,
Math.random() * gameState.baseSpeed * 3,
gameState.baseSpeed * 3,
"red",
false,
gameState.coinSize / 2,
150,
);
}
}
function makeCoin( function makeCoin(
gameState: GameState, gameState: GameState,
x: number, x: number,
@ -1676,9 +1708,9 @@ function makeCoin(
color = "gold", color = "gold",
points = 1, points = 1,
) { ) {
if (gameState.debuffs.negative_coins > Math.random() * 100) { if (gameState.debuffs.negative_coins *points> Math.random() * 10000) {
points = 0; points = 0;
color = "transparent"; color = "crimson";
} }
append(gameState.coins, (p: Partial<Coin>) => { append(gameState.coins, (p: Partial<Coin>) => {
p.x = x; p.x = x;
@ -1698,6 +1730,13 @@ function makeCoin(
}); });
} }
export function interferenceFactor(gameState:GameState){
if(!gameState.debuffs.interference) return 1
const cycleLength = (7+gameState.debuffs.interference)*1000
const position = gameState.levelTime % cycleLength
return position>7000 ? -1 :1
}
function makeParticle( function makeParticle(
gameState: GameState, gameState: GameState,
x: number, x: number,

View file

@ -55,8 +55,7 @@ export function getPossibleUpgrades(gameState: GameState) {
} }
export function max_levels(gameState: GameState) { export function max_levels(gameState: GameState) {
// TODO
return 2
return 7 + gameState.perks.extra_levels; return 7 + gameState.perks.extra_levels;
} }
@ -74,12 +73,10 @@ export function pickedUpgradesHTMl(gameState: GameState) {
export function debuffsHTMl(gameState: GameState):string { export function debuffsHTMl(gameState: GameState):string {
const banned = upgrades.filter(u=>gameState.bannedPerks[u.id]).map(u=>u.name).join(', ') const banned = upgrades.filter(u=>gameState.bannedPerks[u.id]).map(u=>u.name).join(', ')
let list = debuffs.filter(d=>gameState.debuffs[d.id]).map(d=>d.name(gameState.debuffs[d.id], banned)).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>`;
} }
export function levelsListHTMl(gameState: GameState) { export function levelsListHTMl(gameState: GameState) {

View file

@ -116,7 +116,27 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>true</approved> <approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>fragility</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> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -176,7 +196,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>true</approved> <approved>false</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -187,6 +207,36 @@
<folder_node> <folder_node>
<name>gameOver</name> <name>gameOver</name>
<children> <children>
<concept_node>
<name>because_cursed_coin</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>
<name>because_cursed_coin_intro</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>cumulative_total</name> <name>cumulative_total</name>
<description/> <description/>

View file

@ -3,11 +3,14 @@
"confirmRestart.text": "You're about to start a new run, is that really what you wanted ?", "confirmRestart.text": "You're about to start a new run, is that really what you wanted ?",
"confirmRestart.title": "Start a new run ?", "confirmRestart.title": "Start a new run ?",
"confirmRestart.yes": "Restart game", "confirmRestart.yes": "Restart game",
"debuffs.banned.description": "{{lvl}} banned perk(s) : {{banned}}", "debuffs.banned.description": "{{lvl}} banned perk(s) : {{banned}}.",
"debuffs.banned.help": "{{perk}} is banned for this run", "debuffs.banned.help": "{{perk}} is banned for this run.",
"debuffs.interference.help": "Telekinesis and yo-yo stop working for {{lvl}}s every 7s", "debuffs.fragility.help": "Explosions destroy {{percent}} of coins and reset combo.",
"debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs", "debuffs.interference.help": "Telekinesis and yo-yo glitch for {{lvl}}s every 7s.",
"debuffs.negative_coins.help": "{{lvl}}% of coins spawn void and break the combo if caught", "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.",
"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.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.", "gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
"gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.", "gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.",
"gameOver.lost.title": "Game Over", "gameOver.lost.title": "Game Over",
@ -68,7 +71,7 @@
"main_menu.max_particles_help": "Limits the number of particles show on screen for visual effect. ", "main_menu.max_particles_help": "Limits the number of particles show on screen for visual effect. ",
"main_menu.mobile": "Mobile mode", "main_menu.mobile": "Mobile mode",
"main_menu.mobile_help": "Leaves space under the puck.", "main_menu.mobile_help": "Leaves space under the puck.",
"main_menu.normal": "New 7 levels run", "main_menu.normal": "New run",
"main_menu.normal_help": "Start a quick run with random starting perk", "main_menu.normal_help": "Start a quick run with random starting perk",
"main_menu.pointer_lock": "Mouse pointer lock", "main_menu.pointer_lock": "Mouse pointer lock",
"main_menu.pointer_lock_help": "Locks and hides the mouse cursor.", "main_menu.pointer_lock_help": "Locks and hides the mouse cursor.",
@ -122,7 +125,7 @@
"sandbox.start": "Start test run", "sandbox.start": "Start test run",
"sandbox.title": "Sandbox mode", "sandbox.title": "Sandbox mode",
"sandbox.unlocks_at": "Unlocks at total score {{score}}", "sandbox.unlocks_at": "Unlocks at total score {{score}}",
"score_panel.bebuffs_list": "Debuffs :", "score_panel.bebuffs_list": "De-buffs :",
"score_panel.test_run": "This is a test run, score is not recorded permanently", "score_panel.test_run": "This is a test run, score is not recorded permanently",
"score_panel.title": "{{score}} points at level {{level}}/{{max}} ", "score_panel.title": "{{score}} points at level {{level}}/{{max}} ",
"score_panel.title_looped": "{{score}} points at level {{level}}/{{max}} of loop {{loop}}", "score_panel.title_looped": "{{score}} points at level {{level}}/{{max}} of loop {{loop}}",

View file

@ -3,11 +3,14 @@
"confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?", "confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?",
"confirmRestart.title": "Démarrer une nouvelle partie ?", "confirmRestart.title": "Démarrer une nouvelle partie ?",
"confirmRestart.yes": "Commencer une nouvelle partie", "confirmRestart.yes": "Commencer une nouvelle partie",
"debuffs.banned.description": "{{lvl}} amélioration(s) bannie(s) : {{banned}}", "debuffs.banned.description": "{{lvl}} amélioration(s) bannie(s) : {{banned}}.",
"debuffs.banned.help": "", "debuffs.banned.help": "{{perk}} est banni pour cette course.",
"debuffs.interference.help": "", "debuffs.fragility.help": "Les explosions détruisent {{percent}} pièces et réinitialisent le combo.",
"debuffs.more_bombs.help": "", "debuffs.interference.help": "Télékinésie et problème de yo-yo pendant {{lvl}}s toutes les 7 s.",
"debuffs.negative_coins.help": "", "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.",
"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.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.", "gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
"gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.", "gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.",
"gameOver.lost.title": "Balle perdue", "gameOver.lost.title": "Balle perdue",
@ -43,12 +46,12 @@
"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.instructions": "", "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.title": "", "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.",
"main_menu.colorful_coins": "", "main_menu.colorful_coins": "Pièces colorées",
"main_menu.colorful_coins_help": "", "main_menu.colorful_coins_help": "Les pièces apparaissent toujours de la couleur de la brique",
"main_menu.download_save_file": "Sauvegarder mes progrès", "main_menu.download_save_file": "Sauvegarder mes progrès",
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde", "main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde",
"main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>", "main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>",
@ -68,8 +71,8 @@
"main_menu.max_particles_help": "Limite le nombre de particules affichées à l'écran pour les effets visuels", "main_menu.max_particles_help": "Limite le nombre de particules affichées à l'écran pour les effets visuels",
"main_menu.mobile": "Mode mobile", "main_menu.mobile": "Mode mobile",
"main_menu.mobile_help": "Laisse un espace sous le palet.", "main_menu.mobile_help": "Laisse un espace sous le palet.",
"main_menu.normal": "Nouvelle partie rapide", "main_menu.normal": "Nouvelle partie",
"main_menu.normal_help": "Jouez 7 niveaux avec un avantage de départ aléatoire", "main_menu.normal_help": "Avec un avantage de départ aléatoire",
"main_menu.pointer_lock": "Verrouillage du pointeur", "main_menu.pointer_lock": "Verrouillage du pointeur",
"main_menu.pointer_lock_help": "Cache aussi le curseur de la souris.", "main_menu.pointer_lock_help": "Cache aussi le curseur de la souris.",
"main_menu.record": "Enregistrer des vidéos de jeu", "main_menu.record": "Enregistrer des vidéos de jeu",
@ -90,8 +93,8 @@
"main_menu.settings_title": "Paramètre", "main_menu.settings_title": "Paramètre",
"main_menu.show_fps": "Compteur de FPS", "main_menu.show_fps": "Compteur de FPS",
"main_menu.show_fps_help": "Surveiller la perf du jeu", "main_menu.show_fps_help": "Surveiller la perf du jeu",
"main_menu.show_stats": "", "main_menu.show_stats": "Afficher les statistiques en temps réel",
"main_menu.show_stats_help": "", "main_menu.show_stats_help": "Pièces, temps, rebonds, ratés",
"main_menu.sounds": "Sons du jeu", "main_menu.sounds": "Sons du jeu",
"main_menu.sounds_help": "Ralentis certains téléphones.", "main_menu.sounds_help": "Ralentis certains téléphones.",
"main_menu.title": "Breakout 71", "main_menu.title": "Breakout 71",
@ -114,9 +117,9 @@
"premium.help_google": "Bien que je prévoie de proposer des licences premium via Google Play, je n'ai pas encore eu l'occasion de le faire ; il n'y a donc pas de lien d'achat ici. Si vous possédez déjà une clé de licence, vous pouvez la saisir ci-dessous.", "premium.help_google": "Bien que je prévoie de proposer des licences premium via Google Play, je n'ai pas encore eu l'occasion de le faire ; il n'y a donc pas de lien d'achat ici. Si vous possédez déjà une clé de licence, vous pouvez la saisir ci-dessous.",
"premium.per_hours": "Vous avez passé {{hours}} heures à jouer", "premium.per_hours": "Vous avez passé {{hours}} heures à jouer",
"premium.per_hours_help": "Donnez 4.99€ pour être premium", "premium.per_hours_help": "Donnez 4.99€ pour être premium",
"premium.thanks": "", "premium.thanks": "Vous êtes premium, merci !",
"premium.thanks_help": "", "premium.thanks_help": "Copiez votre clé de licence",
"premium.title": "", "premium.title": "Débloquez la boucle avec Premium",
"sandbox.help": "Tester n'importe quelle combinaison d'améliorations", "sandbox.help": "Tester n'importe quelle combinaison d'améliorations",
"sandbox.instructions": "Sélectionnez les amélioration ci-dessous et appuyez sur \"Démarrer la partie de test\" pour les tester. Les scores et les statistiques ne seront pas enregistrés.", "sandbox.instructions": "Sélectionnez les amélioration ci-dessous et appuyez sur \"Démarrer la partie de test\" pour les tester. Les scores et les statistiques ne seront pas enregistrés.",
"sandbox.start": "Démarrer la partie de test", "sandbox.start": "Démarrer la partie de test",

View file

@ -1,4 +1,4 @@
import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators"; import {baseCombo, forEachLiveOne, interferenceFactor, liveCount} from "./gameStateMutators";
import { import {
brickCenterX, brickCenterX,
brickCenterY, brickCenterY,
@ -9,10 +9,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,14 +32,14 @@ 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),
@ -62,6 +62,7 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
ctx.globalAlpha = 0.6; ctx.globalAlpha = 0.6;
forEachLiveOne(gameState.coins, (coin) => { forEachLiveOne(gameState.coins, (coin) => {
drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y); drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y);
}); });
@ -90,7 +91,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);
@ -133,7 +134,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);
@ -161,8 +162,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(
@ -192,23 +192,55 @@ export function render(gameState: GameState) {
ball.y, ball.y,
); );
}); });
if (gameState.debuffs.negative_coins) {
// Render crimson coins very bright
ctx.globalCompositeOperation = 'source-over'
ctx.globalAlpha = 0.8
const red = Math.floor(gameState.levelTime / 100) % 2 > 0
forEachLiveOne(gameState.coins, (coin) => {
if (coin.color !== 'crimson') return
drawBall(
ctx,
red ? 'red' : 'black',
coin.size * 3,
coin.x,
coin.y
);
});
ctx.globalAlpha = 1
forEachLiveOne(gameState.coins, (coin) => {
if (coin.color !== 'crimson') return
drawCoin(
ctx,
!red ? 'red' : 'black',
coin.size,
coin.x,
coin.y,
'red',
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";
@ -216,7 +248,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";
@ -257,9 +289,18 @@ export function render(gameState: GameState) {
); );
if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) { if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) {
ctx.strokeStyle = gameState.puckColor;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight); ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
if (interferenceFactor(gameState) == -1) {
ctx.lineWidth = 2
ctx.strokeStyle = 'red'
ctx.setLineDash(redBorderDash)
ctx.lineDashOffset = getDashOffset(gameState)
} else {
ctx.strokeStyle = gameState.puckColor;
}
ctx.bezierCurveTo( ctx.bezierCurveTo(
gameState.puckPosition, gameState.puckPosition,
gameState.gameZoneHeight, gameState.gameZoneHeight,
@ -269,6 +310,9 @@ export function render(gameState: GameState) {
ball.y, ball.y,
); );
ctx.stroke(); ctx.stroke();
ctx.lineWidth = 2
ctx.setLineDash(emptyArray)
} }
if (gameState.perks.clairvoyant && gameState.ballStickToPuck) { if (gameState.perks.clairvoyant && gameState.ballStickToPuck) {
ctx.strokeStyle = gameState.ballsColor; ctx.strokeStyle = gameState.ballsColor;
@ -471,7 +515,7 @@ function drawStraightLine(
ctx.lineTo(x2, y2); ctx.lineTo(x2, y2);
ctx.stroke(); ctx.stroke();
if (mode == "red") { if (mode == "red") {
ctx.setLineDash([]); ctx.setLineDash(emptyArray);
ctx.lineWidth = 1; ctx.lineWidth = 1;
} }
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
@ -552,7 +596,7 @@ export function renderAllBricks() {
!countBricksBelow(gameState, index); !countBricksBelow(gameState, index);
let redBorder = let redBorder =
color === "transparent" || color === "crimson" ||
(gameState.ballsColor !== color && (gameState.ballsColor !== color &&
color !== "black" && color !== "black" &&
redBorderOnBricksWithWrongColor) || redBorderOnBricksWithWrongColor) ||
@ -831,7 +875,7 @@ export function drawBrick(
canctx.fillStyle = color; canctx.fillStyle = color;
canctx.setLineDash(offset !== -1 ? redBorderDash : []); canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray);
canctx.lineDashOffset = offset; canctx.lineDashOffset = offset;
canctx.strokeStyle = offset !== -1 ? "red" : color; canctx.strokeStyle = offset !== -1 ? "red" : color;
canctx.lineJoin = "round"; canctx.lineJoin = "round";
@ -941,6 +985,7 @@ export const scoreDisplay = document.getElementById(
) as HTMLButtonElement; ) as HTMLButtonElement;
const menuLabel = document.getElementById("menuLabel") as HTMLButtonElement; const menuLabel = document.getElementById("menuLabel") as HTMLButtonElement;
const emptyArray = [];
const redBorderDash = [5, 5]; const redBorderDash = [5, 5];
export function getDashOffset(gameState: GameState) { export function getDashOffset(gameState: GameState) {

3
src/types.d.ts vendored
View file

@ -137,6 +137,7 @@ interface LightFlash extends BaseFlash {
export type RunStats = { export type RunStats = {
started: number; started: number;
levelsPlayed: number; levelsPlayed: number;
loops: number;
runTime: number; runTime: number;
coins_spawned: number; coins_spawned: number;
score: number; score: number;
@ -292,7 +293,7 @@ export type RunParams = {
level?: string; level?: string;
levelToAvoid?: string; levelToAvoid?: string;
perks?: Partial<PerksMap>; perks?: Partial<PerksMap>;
debuffs?: boolean; debuffs?: Partial<DebuffsMap>;
}; };
export type OptionDef = { export type OptionDef = {
default: boolean; default: boolean;