Overcomplicated looping mechanic

This commit is contained in:
Renan LE CARO 2025-03-29 09:25:17 +01:00
parent 5012076039
commit 39b326a15b
18 changed files with 296 additions and 125 deletions

View file

@ -25,10 +25,10 @@ Allow players to loop the game :
- real time stats as the option says. - real time stats as the option says.
- [x] Noise of coins against side is annoying. - [x] Noise of coins against side is annoying.
- Change look of loop, to avoid picking randomly at loop end. - Change look of loop, to avoid picking randomly at loop end.
- make red coins scarier, - [x] make red coins scarier,
- add blue coins that only freeze puck. - [x] add blue coins that only freeze puck.
- Make fullscreen an option and turn it back on when playing - Make fullscreen an option and turn it back on when playing
- +1 combo de base par rerolls - [x] +1 combo de base par rerolls
- +1 combo de base par vie restantes (pas attrapable) - +1 combo de base par vie restantes (pas attrapable)
# Todo # Todo
@ -132,6 +132,7 @@ There's also an easy mode for kids (slower ball).
- [colin] side pucks - same as above but with two side pucks : hard to know where to put them - [colin] side pucks - same as above but with two side pucks : hard to know where to put them
# to sort # to sort
- double coin value when they hit the sides
- [colin]Brambles — coins that touch the walls and ceiling get stuck and are thrown back when the last brick is destroyed - [colin]Brambles — coins that touch the walls and ceiling get stuck and are thrown back when the last brick is destroyed
- [colin]Ball of Greed — the ball can collect coins (might be worth dividing into levels: lvl 1, can collect coins only after two bounces on bricks or walls. lvl 2, can collect after 1 bounce. lvl 3, can collect coins anytime)(or change the ball collection radius as the level grows) - [colin]Ball of Greed — the ball can collect coins (might be worth dividing into levels: lvl 1, can collect coins only after two bounces on bricks or walls. lvl 2, can collect after 1 bounce. lvl 3, can collect coins anytime)(or change the ball collection radius as the level grows)
- [colin]Fountain toss — each coin lost has a 1 in 10 chance to give +1 combo (until combo 50) - [colin]Fountain toss — each coin lost has a 1 in 10 chance to give +1 combo (until combo 50)

View file

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

109
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 = "29053110"; const VERSION = "29053158";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -82,6 +82,7 @@ export async function asyncAlert<t>({
content content
?.filter((i) => i) ?.filter((i) => i)
.forEach((entry, index) => { .forEach((entry, index) => {
if(!entry) return;
if (typeof entry == "string") { if (typeof entry == "string") {
const p = document.createElement("div"); const p = document.createElement("div");
p.innerHTML = entry; p.innerHTML = entry;

View file

@ -1 +1 @@
"29053110" "29053158"

View file

@ -3,10 +3,16 @@ import { Debuff } from "./types";
export const debuffs = [ export const debuffs = [
{ {
id: "negative_coins", id: "deadly_coins",
max: 20, max: 20,
name: (lvl: number) => t("debuffs.negative_coins.help", { lvl }), name: (lvl: number) => t("debuffs.deadly_coins.help", { lvl }),
help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }), help: (lvl: number) => t("debuffs.deadly_coins.help", { lvl }),
},
{
id: "frozen_coins",
max: 20,
name: (lvl: number) => t("debuffs.frozen_coins.help", { lvl }),
help: (lvl: number) => t("debuffs.frozen_coins.help", { lvl }),
}, },
{ {
id: "more_bombs", id: "more_bombs",

View file

@ -466,6 +466,8 @@ async function openScorePanel() {
pickedUpgradesHTMl(gameState), pickedUpgradesHTMl(gameState),
levelsListHTMl(gameState), levelsListHTMl(gameState),
debuffsHTMl(gameState), debuffsHTMl(gameState),
gameState.rerolls?
t('score_panel.rerolls_count', {rerolls:gameState.rerolls}):''
], ],
allowClose: true, allowClose: true,
}); });
@ -1008,8 +1010,8 @@ restart(
(window.location.search.includes("stressTest") && { (window.location.search.includes("stressTest") && {
level: "Bird", level: "Bird",
perks: { perks: {
// sapper: 5, sapper: 5,
// bigger_explosions: 20, bigger_explosions: 20,
// // unbounded: 1, // // unbounded: 1,
// // pierce_color: 1, // // pierce_color: 1,
// pierce: 1, // pierce: 1,
@ -1022,12 +1024,15 @@ restart(
// metamorphosis: 1, // metamorphosis: 1,
// implosions: 1, // implosions: 1,
// sturdy_bricks:5 // sturdy_bricks:5
coin_magnet:2, coin_magnet: 2,
extra_life: 3, extra_life: 3,
}, },
levelsPerLoop:2,
debuffs: { debuffs: {
// fragility:3 // fragility:3
negative_coins: 100, // deadly_coins: 20,
// frozen_coins: 20,
// interference:20, // interference:20,
}, },
}) || }) ||

View file

@ -61,8 +61,8 @@ 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) {
gameState.desiredPuckPosition = x;
// 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
gameState.puckPosition = x;
gameState.needsRender = true; gameState.needsRender = true;
} }
@ -166,6 +166,11 @@ export function normalizeGameState(gameState: GameState) {
gameState.gameZoneWidthRoundedUp - gameState.gameZoneWidthRoundedUp -
gameState.puckWidth / 2; gameState.puckWidth / 2;
if (gameState.puckFrozenUntil < gameState.levelTime || !gameState.levelTime) {
// Frozen, ignore
gameState.puckPosition = gameState.desiredPuckPosition;
}
gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX); gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX);
if (gameState.ballStickToPuck) { if (gameState.ballStickToPuck) {
@ -182,7 +187,11 @@ export function normalizeGameState(gameState: GameState) {
} }
export function baseCombo(gameState: GameState) { export function baseCombo(gameState: GameState) {
return gameState.baseCombo + 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(
@ -594,13 +603,13 @@ export async function gotoNextLoop(gameState: GameState) {
gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {}); gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {});
gameState.upgradesOfferedFor = -1; gameState.upgradesOfferedFor = -1;
let comboText='' let comboText = "";
if(gameState.rerolls) { if (gameState.rerolls) {
comboText=t('loop.converted_rerolls',{n:gameState.rerolls}) comboText = t("loop.converted_rerolls", { n: gameState.rerolls });
gameState.baseCombo += gameState.rerolls gameState.baseCombo += gameState.rerolls;
gameState.rerolls=0 gameState.rerolls = 0;
}else{ } else {
comboText=t('loop.no_rerolls') comboText = t("loop.no_rerolls");
} }
const userPerks = upgrades.filter((u) => gameState.perks[u.id]); const userPerks = upgrades.filter((u) => gameState.perks[u.id]);
@ -612,7 +621,7 @@ export async function gotoNextLoop(gameState: GameState) {
title: t("loop.title", { loop: gameState.loop }), title: t("loop.title", { loop: gameState.loop }),
content: [ content: [
t("loop.instructions"), t("loop.instructions"),
comboText, comboText,
...userPerks.map((u) => { ...userPerks.map((u) => {
const randomDebuff = const randomDebuff =
@ -675,6 +684,7 @@ export async function setLevel(gameState: GameState, l: number) {
gameState.levelStartScore = gameState.score; gameState.levelStartScore = gameState.score;
gameState.levelSpawnedCoins = 0; gameState.levelSpawnedCoins = 0;
gameState.levelMisses = 0; gameState.levelMisses = 0;
gameState.puckFrozenUntil = 0;
gameState.runStatistics.levelsPlayed++; gameState.runStatistics.levelsPlayed++;
// Reset combo silently // Reset combo silently
@ -1049,7 +1059,10 @@ export function gameStateTick(
const ratio = const ratio =
1 - 1 -
((coin.color === "crimson" ? 3 : gameState.perks.viscosity) * 0.03 + ((coin.color === "crimson" || coin.color === "LightSkyBlue"
? 3
: gameState.perks.viscosity) *
0.03 +
0.005) * 0.005) *
frames; frames;
@ -1085,19 +1098,19 @@ export function gameStateTick(
} }
} }
if(coin.color === "crimson" && !isOptionOn('basic')){ if (coin.color === "crimson" && !isOptionOn("basic")) {
const angle=Math.random()*Math.PI*2 const angle = Math.random() * Math.PI * 2;
makeParticle( makeParticle(
gameState, gameState,
coin.x, coin.x,
coin.y, coin.y,
Math.cos(angle)*gameState.baseSpeed*2, Math.cos(angle) * gameState.baseSpeed * 2,
Math.sin(angle)*gameState.baseSpeed*2, Math.sin(angle) * gameState.baseSpeed * 2,
'red', "red",
true, true,
5, 5,
250, 250,
); );
} }
const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10; const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
@ -1112,16 +1125,22 @@ 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.color === "crimson") {
addToScore(gameState, coin); 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"), );
); }
} }
if (coin.color === "LightSkyBlue") {
gameState.puckFrozenUntil = gameState.levelTime + 500;
schedulGameSound(gameState, "freeze", coin.x, 1);
}
addToScore(gameState, coin);
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);
@ -1746,12 +1765,21 @@ function makeCoin(
color = "gold", color = "gold",
points = 1, points = 1,
) { ) {
if (y<gameState.gameZoneWidth*2/3 && let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
gameState.debuffs.negative_coins * points > Math.random() * 10000) { if (
y < (gameState.gameZoneWidth * 2) / 3 &&
gameState.debuffs.deadly_coins * points > Math.random() * 10000
) {
points = 0; points = 0;
color = "crimson"; color = "crimson";
vx=0 vx = 0;
vy=0 vy = 0;
schedulGameSound(gameState, "void", x, 0.5);
weight = 1;
} else if (gameState.debuffs.frozen_coins * points > Math.random() * 10000) {
color = "LightSkyBlue";
schedulGameSound(gameState, "freeze", x, 0.5);
weight = 1;
} }
append(gameState.coins, (p: Partial<Coin>) => { append(gameState.coins, (p: Partial<Coin>) => {
p.x = x; p.x = x;
@ -1767,8 +1795,8 @@ function makeCoin(
p.color = color; p.color = color;
p.a = Math.random() * Math.PI * 2; p.a = Math.random() * Math.PI * 2;
p.sa = Math.random() - 0.5; p.sa = Math.random() - 0.5;
p.weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
p.points = points; p.points = points;
p.weight = weight;
}); });
} }

View file

@ -15,6 +15,12 @@ export function sample<T>(arr: T[]): T {
return arr[Math.floor(arr.length * Math.random())]; return arr[Math.floor(arr.length * Math.random())];
} }
export function sampleN<T>(arr: T[],n:number): T[] {
return [...arr].sort(()=>Math.random()-0.5)
.slice(0,n)
}
export function sumOfValues(obj: { [key: string]: number } | undefined | null) { export function sumOfValues(obj: { [key: string]: number } | undefined | null) {
if (!obj) return 0; if (!obj) return 0;
return Object.values(obj)?.reduce((a, b) => a + b, 0) || 0; return Object.values(obj)?.reduce((a, b) => a + b, 0) || 0;
@ -55,7 +61,7 @@ export function getPossibleUpgrades(gameState: GameState) {
} }
export function max_levels(gameState: GameState) { export function max_levels(gameState: GameState) {
return 7 + gameState.perks.extra_levels; return gameState.levelsPerLoop + gameState.perks.extra_levels;
} }
export function pickedUpgradesHTMl(gameState: GameState) { export function pickedUpgradesHTMl(gameState: GameState) {
@ -143,6 +149,7 @@ export function defaultSounds() {
coinCatch: { vol: 0, x: 0 }, coinCatch: { vol: 0, x: 0 },
colorChange: { vol: 0, x: 0 }, colorChange: { vol: 0, x: 0 },
void: { vol: 0, x: 0 }, void: { vol: 0, x: 0 },
freeze: { vol: 0, x: 0 },
}, },
}; };
} }

View file

@ -122,6 +122,26 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>deadly_coins</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>
<folder_node> <folder_node>
<name>fragility</name> <name>fragility</name>
<children> <children>
@ -142,6 +162,26 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>frozen_coins</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>
<folder_node> <folder_node>
<name>interference</name> <name>interference</name>
<children> <children>
@ -182,26 +222,6 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>negative_coins</name>
<children>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node> <folder_node>
<name>sturdiness</name> <name>sturdiness</name>
<children> <children>
@ -2077,6 +2097,21 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>rerolls_count</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>test_run</name> <name>test_run</name>
<description/> <description/>

View file

@ -5,10 +5,11 @@
"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.fragility.help": "Explosions destroy {{percent}} of coins and reset combo.", "debuffs.deadly_coins.help": "{{lvl}} ou of 10 000 coins spawn deadly, blinking red. Game over if you catch them, unless you had an extra life to spare.",
"debuffs.fragility.help": "Explosions destroy {{percent}}% of coins and reset combo.",
"debuffs.frozen_coins.help": "{{lvl}} coins out of 10000 spawn frozen, and will lock the puck for 0.5s if caught.",
"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.sturdiness.help": "All bricks have +{{lvl}} HP", "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. ",
@ -130,6 +131,7 @@
"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": "De-buffs :", "score_panel.bebuffs_list": "De-buffs :",
"score_panel.rerolls_count": "You have accumulated {{rerolls}} rerolls",
"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

@ -5,10 +5,11 @@
"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": "{{perk}} est banni pour cette course.", "debuffs.banned.help": "{{perk}} est banni pour cette course.",
"debuffs.fragility.help": "Les explosions détruisent {{percent}} pièces et réinitialisent le combo.", "debuffs.deadly_coins.help": "{{lvl}} pièces sur 10000 apparaissent maudites et clignotent en rouge. La partie est terminée si vous les attrapez, sauf si vous avez une vie en stock.",
"debuffs.fragility.help": "Les explosions détruisent {{percent}}% des pièces et réinitialisent le combo.",
"debuffs.frozen_coins.help": "{{lvl}} pièces sur 10000 apparaît gelée, et glace le palet pendant 0.5 si vous l'attrapez. ",
"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.sturdiness.help": "Toutes les briques résistent à +{{lvl}} chocs", "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.",
@ -48,9 +49,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.converted_rerolls": "Vos {{n}} relances restantes ont été converties en +{{n}} combo de base.",
"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.no_rerolls": "Vous n'aviez plus de relances, donc votre combo de base est resté le même.",
"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.",
@ -130,6 +131,7 @@
"sandbox.title": "Mode bac à sable", "sandbox.title": "Mode bac à sable",
"sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}", "sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}",
"score_panel.bebuffs_list": "Handicapes : ", "score_panel.bebuffs_list": "Handicapes : ",
"score_panel.rerolls_count": "Vous avez accumulé {{rerolls}} rerolls",
"score_panel.test_run": "Il s'agit d'une partie d'essai, le score n'est pas enregistré.", "score_panel.test_run": "Il s'agit d'une partie d'essai, le score n'est pas enregistré.",
"score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ", "score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ",
"score_panel.title_looped": "{{score}} points au niveau {{level}}/{{max}} ", "score_panel.title_looped": "{{score}} points au niveau {{level}}/{{max}} ",

View file

@ -51,6 +51,7 @@ export function newGameState(params: RunParams): GameState {
ballStickToPuck: true, ballStickToPuck: true,
puckPosition: 400, puckPosition: 400,
lastPuckPosition: 400, lastPuckPosition: 400,
desiredPuckPosition: 400,
lastPuckMove: 0, lastPuckMove: 0,
pauseTimeout: null, pauseTimeout: null,
canvasWidth: 0, canvasWidth: 0,
@ -111,6 +112,8 @@ export function newGameState(params: RunParams): GameState {
rerolls: 0, rerolls: 0,
loop: 0, loop: 0,
baseCombo: 1, baseCombo: 1,
puckFrozenUntil: 0,
levelsPerLoop: params?.levelsPerLoop ?? 7,
}; };
resetBalls(gameState); resetBalls(gameState);

View file

@ -200,27 +200,49 @@ export function render(gameState: GameState) {
); );
}); });
if (gameState.debuffs.negative_coins) { if (gameState.debuffs.deadly_coins || gameState.debuffs.frozen_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") {
drawBall(ctx, red ? "red" : "black", coin.size * 3, coin.x, coin.y); drawBall(ctx, red ? "red" : "black", coin.size * 3, coin.x, coin.y);
}
if (coin.color == "LightSkyBlue") {
drawBall(
ctx,
red ? "LightSkyBlue" : "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") {
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,
); );
}
if (coin.color == "LightSkyBlue") {
drawCoin(
ctx,
!red ? "LightSkyBlue" : "black",
coin.size,
coin.x,
coin.y,
"LightSkyBlue",
coin.a,
);
}
}); });
} }
} }
@ -324,7 +346,9 @@ export function render(gameState: GameState) {
drawPuck( drawPuck(
ctx, ctx,
gameState.puckColor, gameState.puckFrozenUntil > gameState.levelTime
? "LightSkyBlue"
: gameState.puckColor,
gameState.puckWidth, gameState.puckWidth,
gameState.puckHeight, gameState.puckHeight,
0, 0,

View file

@ -54,6 +54,11 @@ export const sounds = {
createSingleBounceSound(1200, pan, volume, 0.5, "sawtooth"); createSingleBounceSound(1200, pan, volume, 0.5, "sawtooth");
createSingleBounceSound(600, pan, volume, 0.3, "sawtooth"); createSingleBounceSound(600, pan, volume, 0.3, "sawtooth");
}, },
freeze: (volume: number, pan: number) => {
if (!isOptionOn("sound")) return;
createSingleBounceSound(220, pan, volume, 0.5, "square");
createSingleBounceSound(440, pan, volume, 0.5, "square");
},
explode: (volume: number, pan: number, combo: number) => { explode: (volume: number, pan: number, combo: number) => {
if (!isOptionOn("sound")) return; if (!isOptionOn("sound")) return;
createExplosionSound(pan); createExplosionSound(pan);

5
src/types.d.ts vendored
View file

@ -285,10 +285,14 @@ export type GameState = {
coinCatch: { vol: number; x: number }; coinCatch: { vol: number; x: number };
colorChange: { vol: number; x: number }; colorChange: { vol: number; x: number };
void: { vol: number; x: number }; void: { vol: number; x: number };
freeze: { vol: number; x: number };
}; };
rerolls: number; rerolls: number;
loop: number; loop: number;
baseCombo: number; baseCombo: number;
puckFrozenUntil: number;
desiredPuckPosition: number;
levelsPerLoop:number;
}; };
export type RunParams = { export type RunParams = {
@ -296,6 +300,7 @@ export type RunParams = {
levelToAvoid?: string; levelToAvoid?: string;
perks?: Partial<PerksMap>; perks?: Partial<PerksMap>;
debuffs?: Partial<DebuffsMap>; debuffs?: Partial<DebuffsMap>;
levelsPerLoop?:number;
}; };
export type OptionDef = { export type OptionDef = {
default: boolean; default: boolean;