This commit is contained in:
Renan LE CARO 2025-04-06 10:13:10 +02:00
parent 0cef60f90d
commit bdbf8b846c
19 changed files with 1306 additions and 1591 deletions

View file

@ -17,23 +17,32 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
## To do
- remove the slow mode
- ignore scores in creative mode
- avoid showing a +1 and -1 at the same time when a combo increase is reset
- add unlock conditions for levels in the form "reach high score X with perk A,B,C but without perk B,C,D"
- archive each version as an html file and apk
## Done
- remove loop mode :
- remove basecombo
- remove mode
- clear old runs in other mode
- ignore scores in creative mode
- remove the slow mode
- adjusted the light effects
- added white border around dark grey bricks
- remove the opaque coin options, all coins are opaque, but dark grey ones have white border
- archive each version as an html file and apk
- publish 29062687 on play store
- redo video
- review fastlane text
- tried and cancelled native desktop app build with tauri because :
- there's no cross compilation, so no exe build on linux
- you need to sign executable differently for each platform
- the .deb and .rmp files were 3.8M for a 0.1M app
- the appimage was crazy big (100M)
- I'd need a mac to make a mac version that probably wouldn't run without doing the app store dance with apple
- publish 29062687 on play store
- redo video
- review fastlane text
## 29062687

535
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -2,47 +2,36 @@ import { GameState, Level, PerkId, Upgrade } from "./types";
import { allLevels, icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import { confirmRestart, creativeModeThreshold, restart } from "./game";
import { requiredAsyncAlert } from "./asyncAlert";
import { describeLevel, highScoreForMode, sumOfValues } from "./game_utils";
import {confirmRestart, creativeModeThreshold, gameState, restart} from "./game";
import {asyncAlert, requiredAsyncAlert} from "./asyncAlert";
import { describeLevel, highScoreText, sumOfValues } from "./game_utils";
export function creativeMode(gameState: GameState) {
return {
icon: icons["icon:sandbox"],
text: t("lab.menu_entry"),
help:
highScoreForMode("creative") ||
// highScoreForMode("creative") ||
(getTotalScore() < creativeModeThreshold &&
t("lab.unlocks_at", { score: creativeModeThreshold })) ||
t("lab.help"),
disabled: getTotalScore() < creativeModeThreshold,
async value() {
if (await confirmRestart(gameState)) {
restart({ mode: "creative" });
}
openCreativeModePerksPicker()
},
};
}
export async function openCreativeModePerksPicker(
gameState,
currentLevel: number,
) {
gameState.readyToRender = false;
export async function openCreativeModePerksPicker() {
let creativeModePerks: Partial<{ [id in PerkId]: number }> = getSettingValue(
"creativeModePerks_" + currentLevel,
"creativeModePerks" ,
{},
),
choice: Upgrade | Level | "reset" | void;
upgrades.forEach((u) => {
creativeModePerks[u.id] = Math.min(
creativeModePerks[u.id] || 0,
u.max - gameState.bannedPerks[u.id],
);
});
let noCreative: PerkId[] = [
"extra_levels",
"shunt",
@ -51,8 +40,8 @@ export async function openCreativeModePerksPicker(
];
while (
(choice = await requiredAsyncAlert<Upgrade | Level | "reset">({
title: t("lab.title", { lvl: currentLevel + 1 }),
(choice = await asyncAlert<Upgrade | Level | "reset">({
title: t("lab.menu_entry"),
className: "actionsAsGrid",
content: [
t("lab.instructions"),
@ -69,9 +58,8 @@ export async function openCreativeModePerksPicker(
help:
(creativeModePerks[u.id] || 0) +
"/" +
(u.max - gameState.bannedPerks[u.id]),
u.max,
value: u,
disabled: u.max - gameState.bannedPerks[u.id] <= 0,
className: creativeModePerks[u.id]
? "sandbox"
: "sandbox grey-out-unless-hovered",
@ -92,17 +80,18 @@ export async function openCreativeModePerksPicker(
creativeModePerks[u.id] = 0;
});
} else if ("bricks" in choice) {
setSettingValue("creativeModePerks_" + currentLevel, creativeModePerks);
upgrades.forEach((u) => {
gameState.perks[u.id] = creativeModePerks[u.id];
gameState.bannedPerks[u.id] += creativeModePerks[u.id];
});
gameState.runLevels[currentLevel] = choice;
break;
setSettingValue("creativeModePerks" , creativeModePerks);
if (await confirmRestart(gameState)) {
restart({ perks:creativeModePerks, level:choice.name});
}
return
} else if (choice) {
creativeModePerks[choice.id] =
((creativeModePerks[choice.id] || 0) + 1) %
(choice.max - gameState.bannedPerks[choice.id] + 1);
(choice.max +1);
}else{
return
}
}
}

View file

@ -1,7 +1,7 @@
{
"_": "",
"B": "black",
"W": "white",
"W": "#FFFFFF",
"g": "#231f20",
"y": "#ffd300",
"b": "#6262EA",

View file

@ -16,7 +16,7 @@ import {
currentLevelInfo,
describeLevel,
getRowColIndex,
highScoreForMode,
highScoreText,
levelsListHTMl,
max_levels,
pickedUpgradesHTMl,
@ -72,6 +72,7 @@ import { creativeMode } from "./creative";
import { setupTooltips } from "./tooltip";
import { startingPerkMenuButton } from "./startingPerks";
import "./migrations";
import {getCreativeModeWarning} from "./gameOver";
export async function play() {
if (await applyFullScreenChoice()) return;
@ -441,20 +442,15 @@ async function openScorePanel() {
pause(true);
const cb = await asyncAlert({
title: gameState.loop
? t("score_panel.title_looped", {
loop: gameState.loop,
score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels(gameState),
})
: t("score_panel.title", {
title: t("score_panel.title", {
score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels(gameState),
}),
content: [
getCreativeModeWarning(gameState),
pickedUpgradesHTMl(gameState),
levelsListHTMl(gameState, gameState.currentLevel),
gameState.rerolls
@ -486,31 +482,13 @@ export async function openMainMenu() {
{
icon: icons["icon:7_levels_run"],
text: t("main_menu.normal"),
help: highScoreForMode("short") || t("main_menu.normal_help"),
help: highScoreText() || t("main_menu.normal_help"),
value: () => {
restart({
levelToAvoid: currentLevelInfo(gameState).name,
mode: "short",
levelToAvoid: currentLevelInfo(gameState).name
});
},
},
{
icon: icons["icon:loop"],
text: t("main_menu.loop_run"),
help:
highScoreForMode("long") ||
(getTotalScore() < creativeModeThreshold &&
t("lab.unlocks_at", { score: creativeModeThreshold })) ||
t("main_menu.loop_run_help"),
value: () => {
restart({
levelToAvoid: currentLevelInfo(gameState).name,
mode: "long",
});
},
disabled: getTotalScore() < creativeModeThreshold,
},
creativeMode(gameState),
{
icon: icons["icon:unlocks"],
@ -880,15 +858,16 @@ async function openUnlocksList() {
});
if (tryOn) {
if (await confirmRestart(gameState)) {
restart({ ...tryOn, mode: "short" });
restart({ ...tryOn });
}
}
}
export async function confirmRestart(gameState) {
if (!gameState.currentLevel) return true;
if (alertsOpen) return true;
pause(true)
return asyncAlert({
title: t("confirmRestart.title"),
content: [
@ -970,8 +949,7 @@ document.addEventListener("keyup", async (e) => {
// When doing ctrl + R in dev to refresh, i don't want to instantly restart a run
if (await confirmRestart(gameState)) {
restart({
levelToAvoid: currentLevelInfo(gameState).name,
mode: gameState.mode,
levelToAvoid: currentLevelInfo(gameState).name
});
}
} else {
@ -980,7 +958,7 @@ document.addEventListener("keyup", async (e) => {
e.preventDefault();
});
export const gameState = newGameState({ mode: "short" });
export const gameState = newGameState({});
export function restart(params: RunParams) {
fitSize();
@ -989,26 +967,7 @@ export function restart(params: RunParams) {
setLevel(gameState, 0);
}
restart(
(window.location.search.includes("stressTest") && {
// level: "Bird",
perks: {
// shocks: 10,
// multiball: 6,
// telekinesis: 2,
// ghost_coins: 1,
pierce: 2,
// clairvoyant: 2,
// sturdy_bricks:2,
bigger_explosions: 10,
sapper: 3,
// unbounded: 1,
},
mode: "long",
}) || {
mode: "short",
},
);
restart({ });
tick();
setupTooltips();

View file

@ -118,11 +118,12 @@ export function gameOver(title: string, intro: string) {
allowClose: true,
title,
content: [
getCreativeModeWarning(gameState),
`
<p>${intro}</p>
<p>${t("gameOver.cumulative_total", { startTs, endTs })}</p>
${unlocksInfo}
`,
unlocksInfo,
{
value: null,
text: t("gameOver.restart"),
@ -135,13 +136,19 @@ export function gameOver(title: string, intro: string) {
],
}).then(() =>
restart({
levelToAvoid: currentLevelInfo(gameState).name,
mode: gameState.mode,
levelToAvoid: currentLevelInfo(gameState).name
}),
);
}
export function getCreativeModeWarning(gameState: GameState){
if(gameState.creative){
return '<p>'+t('gameOver.creative')+'</p>'
}
return ''
}
export function getHistograms(gameState: GameState) {
if(gameState.creative) return ''
let runStats = "";
try {
// Stores only top 100 runs
@ -155,7 +162,6 @@ export function getHistograms(gameState: GameState) {
runsHistory.push({
...gameState.runStatistics,
perks: gameState.perks,
mode: gameState.mode,
appVersion,
});
@ -172,7 +178,6 @@ export function getHistograms(gameState: GameState) {
unit: string,
) => {
let values = runsHistory
.filter((h) => (h.mode || "short") === gameState.mode)
.map((h) => getter(h) || 0);
let min = Math.min(...values);
let max = Math.max(...values);

View file

@ -76,10 +76,10 @@ export function resetBalls(gameState: GameState) {
const count = 1 + (gameState.perks?.multiball || 0);
const perBall = gameState.puckWidth / (count + 1);
gameState.balls = [];
gameState.ballsColor = "#FFF";
gameState.ballsColor = "#FFFFFF";
if (gameState.perks.picky_eater || gameState.perks.pierce_color) {
gameState.ballsColor =
getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF";
getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFFFFF";
}
for (let i = 0; i < count; i++) {
const x =
@ -131,8 +131,7 @@ export function normalizeGameState(gameState: GameState) {
gameState.gameZoneWidth / 12 / 10 +
gameState.currentLevel / 3 +
gameState.levelTime / (30 * 1000) -
gameState.perks.slow_down * 2 +
gameState.loop,
gameState.perks.slow_down * 2
);
gameState.puckWidth = Math.max(
@ -174,7 +173,7 @@ export function normalizeGameState(gameState: GameState) {
export function baseCombo(gameState: GameState) {
return (
gameState.baseCombo +
1 +
gameState.perks.base_combo * 3 +
gameState.perks.smaller_puck * 5
);
@ -206,7 +205,7 @@ export function resetCombo(
gameState,
x,
y,
"red",
"#FF0000",
"-" + lost,
20,
500 + clamp(lost, 0, 500),
@ -231,7 +230,7 @@ export function increaseCombo(
typeof x !== "undefined" &&
typeof y !== "undefined"
) {
makeText(gameState, x, y, "gold", "+" + by, 25, 400 + by);
makeText(gameState, x, y, "#ffd300", "+" + by, 25, 400 + by);
}
}
export function decreaseCombo(
@ -247,7 +246,7 @@ export function decreaseCombo(
if (lost) {
schedulGameSound(gameState, "comboDecrease", x, 1);
if (typeof x !== "undefined" && typeof y !== "undefined") {
makeText(gameState, x, y, "red", "-" + lost, 20, 400 + lost);
makeText(gameState, x, y, "#FF0000", "-" + lost, 20, 400 + lost);
}
}
}
@ -342,11 +341,10 @@ export function explosionAt(
});
gameState.lastExplosion = Date.now();
// makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150);
if (gameState.perks.implosions) {
spawnImplosion(gameState, 7 * size, x, y, "white");
spawnImplosion(gameState, 7 * size, x, y, "#FFFFFF");
} else {
spawnExplosion(gameState, 7 * size, x, y, "white");
spawnExplosion(gameState, 7 * size, x, y, "#FFFFFF");
}
gameState.runStatistics.bricks_broken++;
@ -566,13 +564,14 @@ export function schedulGameSound(
}
export function addToScore(gameState: GameState, coin: Coin) {
gameState.score += coin.points;
gameState.lastScoreIncrease = gameState.levelTime;
addToTotalScore(gameState, coin.points);
if (gameState.score > gameState.highScore) {
if (gameState.score > gameState.highScore && !gameState.creative) {
gameState.highScore = gameState.score;
localStorage.setItem(
"breakout-3-hs-" + gameState.mode,
"breakout-3-hs-short" ,
gameState.score.toString(),
);
}
@ -585,7 +584,7 @@ export function addToScore(gameState: GameState, coin: Coin) {
-coin.y / 100,
gameState.perks.metamorphosis || isOptionOn("colorful_coins")
? coin.color
: "gold",
: "#ffd300",
true,
gameState.coinSize / 2,
@ -605,62 +604,6 @@ export function addToScore(gameState: GameState, coin: Coin) {
}
}
export async function gotoNextLoop(gameState: GameState) {
pause(false);
gameState.loop++;
gameState.runStatistics.loops++;
gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {});
gameState.upgradesOfferedFor = -1;
let comboText = "";
if (gameState.rerolls) {
comboText = t("loop.converted_rerolls", { n: gameState.rerolls });
gameState.baseCombo += gameState.rerolls;
gameState.rerolls = 0;
} else {
comboText = t("loop.no_rerolls");
}
const userPerks = upgrades.filter((u) => gameState.perks[u.id]);
const keep = await requiredAsyncAlert<PerkId>({
title: t("loop.title", { loop: gameState.loop }),
content: [
t("loop.instructions"),
comboText,
...userPerks
.filter((u) => u.id !== "instant_upgrade")
.map((u) => {
return {
text:
u.name +
t("level_up.upgrade_perk_to_level", {
level: gameState.perks[u.id] + 1,
}),
icon: u.icon,
value: u.id,
help: u.help(gameState.perks[u.id] + 1),
};
}),
],
});
// Decrease max level of all perks
userPerks.forEach((u) => {
if (u.id !== keep) {
gameState.bannedPerks[u.id] += gameState.perks[u.id];
}
});
// Increase max level of kept perk by 2
gameState.bannedPerks[keep] -= 2;
// Increase current level of kept perk by 1
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {
[keep]: gameState.perks[keep] + 1,
});
await setLevel(gameState, 0);
}
function recordBestWorstLevelScore(gameState: GameState) {
const levelScore = gameState.score - gameState.levelStartScore;
const { runStatistics } = gameState;
@ -689,9 +632,7 @@ export async function setLevel(gameState: GameState, l: number) {
stopRecording();
recordBestWorstLevelScore(gameState);
if (gameState.mode === "creative") {
await openCreativeModePerksPicker(gameState, l);
} else if (l > 0) {
if (l > 0) {
await openUpgradesPicker(gameState);
}
gameState.currentLevel = l;
@ -749,7 +690,6 @@ export async function setLevel(gameState: GameState, l: number) {
background.src = "data:image/svg+xml;UTF8," + lvl.svg;
document.body.style.setProperty("--level-background", lvl.color || "#000");
gameState.readyToRender = true;
}
function setBrick(gameState: GameState, index: number, color: string) {
@ -760,8 +700,17 @@ function setBrick(gameState: GameState, index: number, color: string) {
0;
}
const rainbow=[
'#ff2e2e',
'#ffe02e',
'#70ff33',
'#33ffa7',
'#38acff',
'#7038ff',
'#ff3de5',
]
export function rainbowColor(): colorString {
return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`;
return rainbow[Math.floor(gameState.levelTime / 50) %rainbow.length ]
}
export function repulse(
@ -1050,12 +999,10 @@ export function gameStateTick(
) {
if (gameState.currentLevel + 1 < max_levels(gameState)) {
setLevel(gameState, gameState.currentLevel + 1);
} else if (gameState.loop < (gameState.mode === "long" ? 7 : 0)) {
gotoNextLoop(gameState);
} else {
gameOver(
t("gameOver.7_loop.title", { loop: gameState.loop }),
t("gameOver.7_loop.summary", { score: gameState.score }),
t("gameOver.win.title" ),
t("gameOver.win.summary", { score: gameState.score }),
);
}
} else if (gameState.running || gameState.levelTime) {
@ -1091,7 +1038,7 @@ export function gameStateTick(
const ratio =
1 -
((gameState.perks.viscosity * 0.03 + 0.005) * frames) /
((gameState.perks.viscosity * 0.03 + 0.002) * frames) /
(1 + gameState.perks.etherealcoins);
coin.vy *= ratio;
@ -1121,7 +1068,7 @@ export function gameStateTick(
gameState.baseSpeed,
gameState.perks.metamorphosis || isOptionOn("colorful_coins")
? coin.color
: "gold",
: "#ffd300",
true,
5,
250,
@ -1300,7 +1247,7 @@ export function gameStateTick(
0,
(Math.random() - 0.5) * 10,
5,
"red",
"#FF0000",
true,
gameState.coinSize / 2,
100 * (Math.random() + 1),
@ -1314,7 +1261,7 @@ export function gameStateTick(
Math.random() * gameState.gameZoneHeight,
5,
(Math.random() - 0.5) * 10,
"red",
"#FF0000",
true,
gameState.coinSize / 2,
100 * (Math.random() + 1),
@ -1328,7 +1275,7 @@ export function gameStateTick(
Math.random() * gameState.gameZoneHeight,
-5,
(Math.random() - 0.5) * 10,
"red",
"#FF0000",
true,
gameState.coinSize / 2,
100 * (Math.random() + 1),
@ -1354,7 +1301,7 @@ export function gameStateTick(
gameState.gameZoneHeight,
(Math.random() - 0.5) * 10,
-5,
"red",
"#FF0000",
true,
gameState.coinSize / 2,
100 * (Math.random() + 1),
@ -1368,7 +1315,7 @@ export function gameStateTick(
gameState.gameZoneHeight - gameState.puckHeight,
pos * 10,
-5,
"red",
"#FF0000",
true,
gameState.coinSize / 2,
100 * (Math.random() + 1),
@ -1593,7 +1540,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
gameState,
gameState.puckPosition,
gameState.gameZoneHeight - gameState.puckHeight * 2,
"red",
"#FF0000",
t("play.missed_ball"),
gameState.puckHeight,
500,
@ -1706,7 +1653,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
gameState,
brickCenterX(gameState, hitBrick),
brickCenterY(gameState, hitBrick),
"white",
"#FFFFFF",
gameState.brickWidth + 2,
50 * gameState.brickHP[hitBrick],
);
@ -1726,8 +1673,8 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
(extraCombo && Math.random() > 0.1 / (1 + extraCombo))
) {
const color =
(remainingSapper && (Math.random() > 0.5 ? "orange" : "red")) ||
(willMiss && "red") ||
(remainingSapper && (Math.random() > 0.5 ? "#ffb92a" : "#FF0000")) ||
(willMiss && "#FF0000") ||
gameState.ballsColor;
makeParticle(
@ -1770,7 +1717,7 @@ function justLostALife(gameState: GameState, ball: Ball, x: number, y: number) {
y,
Math.random() * gameState.baseSpeed * 3,
gameState.baseSpeed * 3,
"red",
"#FF0000",
false,
gameState.coinSize / 2,
150,
@ -1784,7 +1731,7 @@ function makeCoin(
y: number,
vx: number,
vy: number,
color = "gold",
color = "#ffd300",
points = 1,
) {
let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);

View file

@ -1,8 +1,9 @@
import { Ball, GameState, Level, PerkId, PerksMap } from "./types";
import {Ball, Coin, GameState, Level, PerkId, PerksMap} from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { brickAt } from "./level_editor/levels_editor_util";
import { clamp } from "./pure_functions";
import {isOptionOn} from "./options";
export function describeLevel(level: Level) {
let bricks = 0,
@ -78,8 +79,8 @@ export function getPossibleUpgrades(gameState: GameState) {
}
export function max_levels(gameState: GameState) {
if (gameState.mode === "creative") return 3;
return Math.max(7 + gameState.perks.extra_levels - gameState.loop, 1);
if (gameState.creative ) return 1;
return 7 + gameState.perks.extra_levels
}
export function pickedUpgradesHTMl(gameState: GameState) {
@ -122,7 +123,7 @@ export function pickedUpgradesHTMl(gameState: GameState) {
export function levelsListHTMl(gameState: GameState, level: number) {
if (!gameState.perks.clairvoyant) return "";
if (gameState.mode === "creative") return "";
if (gameState.creative) return "";
let list = "";
for (let i = 0; i < max_levels(gameState); i++) {
list += `<span style="opacity: ${i >= level ? 1 : 0.2}" title="${gameState.runLevels[i].name}">${icons[gameState.runLevels[i].name]}</span>`;
@ -255,15 +256,19 @@ export function isMovingWhilePassiveIncome(gameState: GameState) {
);
}
export function highScoreForMode(mode: GameState["mode"]) {
export function getHighScore(){
try {
const score = parseInt(
localStorage.getItem("breakout-3-hs-" + mode) || "0",
return parseInt(
localStorage.getItem("breakout-3-hs-short" ) || "0",
);
if (score) {
return t("main_menu.high_score", { score });
}
} catch (e) {}
return 0
}
export function highScoreText( ) {
if (getHighScore()) {
return t("main_menu.high_score", { score:getHighScore() });
}
return "";
}

View file

@ -87,41 +87,6 @@
<folder_node>
<name>gameOver</name>
<children>
<folder_node>
<name>7_loop</name>
<children>
<concept_node>
<name>summary</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node>
<name>cumulative_total</name>
<description/>
@ -450,6 +415,21 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
@ -532,21 +512,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>unlocks_at</name>
<description/>
@ -1312,21 +1277,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>loop_run</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>loop_run_help</name>
<description/>
@ -1462,36 +1412,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>opaque_coins</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>opaque_coins_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>
<concept_node>
<name>pointer_lock</name>
<description/>

View file

@ -3,8 +3,6 @@
"confirmRestart.text": "You're about to start a new game, is that really what you wanted ?",
"confirmRestart.title": "Start a new game ?",
"confirmRestart.yes": "Restart game",
"gameOver.7_loop.summary": "This game is over. You stashed {{score}} coins. ",
"gameOver.7_loop.title": "You completed this game",
"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.title": "Game Over",
@ -25,13 +23,13 @@
"gameOver.stats.upgrades_applied": "Upgrades applied",
"gameOver.test_run": "This is a test game, its score is not recorded.",
"gameOver.unlocked_count": "You unlocked {{count}} items :",
"gameOver.win.summary": "You cleared all levels for this game, catching {{score}} coins in total.",
"lab.help": "Try to come up with 3 good builds",
"lab.instructions": "Select upgrades below, then pick the level to play. Each time you upgrade a perk, it cannot be upgraded as much in the following levels. ",
"lab.menu_entry": "New lab game",
"gameOver.win.summary": "This game is over. You stashed {{score}} coins. ",
"gameOver.win.title": "You completed this game",
"lab.help": "Try any build you want",
"lab.instructions": "Select upgrades below, then pick the level to play. ",
"lab.menu_entry": "Creative mode",
"lab.reset": "Reset all to 0",
"lab.select_level": "Select a level for this build",
"lab.title": "Choose perks for level {{lvl}}/3",
"lab.select_level": "Select a level to play on",
"lab.unlocks_at": "Unlocks at total score {{score}}",
"level_up.after_buttons": "You just finished level {{level}}/{{max}}.",
"level_up.before_buttons": "You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds {{timeGain}}.\n\nYou missed {{levelMisses}} times {{missesGain}} and hit the walls or ceiling {{levelWallBounces}} times{{wallHitsGain}}.\n\n{{compliment}}",
@ -82,7 +80,6 @@
"main_menu.language_help": "Choose the game's language",
"main_menu.load_save_file": "Load save file",
"main_menu.load_save_file_help": "Select a save file on your device",
"main_menu.loop_run": "New long game",
"main_menu.loop_run_help": "Allows you to loop up to 7 times",
"main_menu.max_coins": " {{max}} coins on screen maximum",
"main_menu.max_coins_help": "Cosmetic only, no effect on score",
@ -90,10 +87,8 @@
"main_menu.max_particles_help": "Limits the number of particles show on screen for visual effect. ",
"main_menu.mobile": "Mobile mode",
"main_menu.mobile_help": "Leaves space under the puck.",
"main_menu.normal": "New short game",
"main_menu.normal": "New game",
"main_menu.normal_help": "Play 7 levels with a random starting perk",
"main_menu.opaque_coins": "Opaque coins with white border",
"main_menu.opaque_coins_help": "Less pretty but more readable",
"main_menu.pointer_lock": "Mouse pointer lock",
"main_menu.pointer_lock_help": "Locks and hides the mouse cursor.",
"main_menu.record": "Record gameplay videos",
@ -143,7 +138,7 @@
"score_panel.upcoming_levels": "Upcoming levels :",
"score_panel.upgrades_picked": "Upgrades picked so far : ",
"unlocks.greyed_out_help": "The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.",
"unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer. Click an upgrade or level below to start a short game with it.",
"unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer. Click an upgrade or level below to start a game with it.",
"unlocks.level": "Here are all the game levels, click one to start a game with that starting level. ",
"unlocks.level_description": "A {{size}}x{{size}} level with {{bricks}} bricks, {{colors}} colors and {{bombs}} bombs.",
"unlocks.title": "You unlocked {{percentUnlock}}% of the game. ",

View file

@ -3,8 +3,6 @@
"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.yes": "Commencer une nouvelle partie",
"gameOver.7_loop.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces. ",
"gameOver.7_loop.title": "Vous avez terminé cette partie",
"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.title": "Balle perdue",
@ -25,13 +23,13 @@
"gameOver.stats.upgrades_applied": "Mises à jour appliquées",
"gameOver.test_run": "Cette partie de test et son score ne sont pas enregistrés.",
"gameOver.unlocked_count": "Vous avez débloqué {{count}} objet(s) :",
"gameOver.win.summary": "Vous avez nettoyé tous les niveaux pour cette partie, en attrapant {{score}} pièces au total.",
"lab.help": "Créez trois bonnes combinaisons d'améliorations",
"lab.instructions": "Sélectionnez les améliorations ci-dessous, puis choisissez le niveau à jouer. Chaque amélioration d'un avantage ne peut pas être améliorée autant dans les niveaux suivants.",
"lab.menu_entry": "Nouvelle partie \"lab\" ",
"gameOver.win.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces. ",
"gameOver.win.title": "Vous avez terminé cette partie",
"lab.help": "Essayez n'importe quel build",
"lab.instructions": "Sélectionnez les améliorations ci-dessous, puis choisissez le niveau à jouer. ",
"lab.menu_entry": "Mode créatif",
"lab.reset": "RAZ toutes les améliorations",
"lab.select_level": "Sélectionnez un niveau pour continue",
"lab.title": "Choisissez les avantages pour le niveau {{lvl}}/3",
"lab.select_level": "Sélectionnez un niveau sur lequel jouer",
"lab.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}",
"level_up.after_buttons": "Vous venez de terminer le niveau {{level}}/{{max}}.",
"level_up.before_buttons": "Vous avez attrapé {{score}} pièces {{catchGain}} sur {{levelSpawnedCoins}} en {{time}} secondes {{timeGain}}.\n\nVous avez raté les briques {{levelMisses}} fois {{missesGain}} et touché les bords de la zone de jeu {{levelWallBounces}} fois {{wallHitsGain}}.\n\n{{compliment}}",
@ -82,7 +80,6 @@
"main_menu.language_help": "Changer la langue d'affichage",
"main_menu.load_save_file": "Charger une sauvegarde",
"main_menu.load_save_file_help": "Depuis un fichier ",
"main_menu.loop_run": "Nouvelle partie longue ",
"main_menu.loop_run_help": "Permet de boucler le jeu jusqu'à 7 fois",
"main_menu.max_coins": "{{max}} pièces affichées maximum",
"main_menu.max_coins_help": "Visuel uniquement, pas d'impact sur le score",
@ -90,10 +87,8 @@
"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_help": "Laisse un espace sous le palet.",
"main_menu.normal": "Nouvelle partie rapide",
"main_menu.normal": "Nouvelle partie",
"main_menu.normal_help": "Avec un avantage de départ aléatoire",
"main_menu.opaque_coins": "Pièces opaques avec bordure blanche",
"main_menu.opaque_coins_help": "Moins joli mais plus lisible",
"main_menu.pointer_lock": "Verrouillage du pointeur",
"main_menu.pointer_lock_help": "Cache aussi le curseur de la souris.",
"main_menu.record": "Enregistrer des vidéos de jeu",
@ -143,7 +138,7 @@
"score_panel.upcoming_levels": "Niveaux de la parties : ",
"score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :",
"unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu.",
"unlocks.intro": "Votre score total est de {{ts}}. Vous trouverez ci-dessous toutes les améliorations et tous les niveaux que le jeu peut offrir. Cliquez sur l'un d'entre eux pour commencer une nouvelle partie rapide. ",
"unlocks.intro": "Votre score total est de {{ts}}. Vous trouverez ci-dessous toutes les améliorations et tous les niveaux que le jeu peut offrir. Cliquez sur l'un d'entre eux pour commencer une nouvelle partie. ",
"unlocks.level": "Voici tous les niveaux du jeu. Cliquez sur un niveau pour commencer une nouvelle partie avec ce niveau de départ. ",
"unlocks.level_description": "Un niveau {{size}}x{{size}} avec {{bricks}} briques, {{colors}} couleurs et {{bombs}} bombes.",
"unlocks.title": "Vous avez débloqué {{percentUnlock}}% du jeu.",

View file

@ -35,3 +35,23 @@ migrate("recover_high_scores", () => {
}
});
});
migrate("remove_long_and_creative_mode_data", () => {
let runsHistory = JSON.parse(
localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[];
let cleaned=runsHistory.filter(r=> {
if('mode' in r){
if(r.mode !== 'short'){
return false
}
}
return true
})
if(cleaned.length!==runsHistory.length)
localStorage.setItem(
"breakout_71_runs_history",
JSON.stringify(cleaned),
);
});

View file

@ -2,8 +2,8 @@ import { GameState, RunParams } from "./types";
import { getTotalScore } from "./settings";
import { allLevels, upgrades } from "./loadGameData";
import {
defaultSounds,
getPossibleUpgrades,
defaultSounds, getHighScore,
getPossibleUpgrades, highScoreText,
makeEmptyPerksMap,
sumOfValues,
} from "./game_utils";
@ -63,11 +63,9 @@ export function newGameState(params: RunParams): GameState {
lastScoreIncrease: -1000,
lastExplosion: -1000,
lastBrickBroken: 0,
highScore: parseFloat(
localStorage.getItem("breakout-3-hs-" + params?.mode || "short") || "0",
),
highScore: getHighScore(),
balls: [],
ballsColor: "white",
ballsColor: "#FFFFFF",
bricks: [],
brickHP: [],
lights: { indexMin: 0, total: 0, list: [] },
@ -78,7 +76,7 @@ export function newGameState(params: RunParams): GameState {
levelStartScore: 0,
levelMisses: 0,
levelSpawnedCoins: 0,
puckColor: "#FFF",
puckColor: "#FFFFFF",
ballSize: 20,
coinSize: 14,
puckHeight: 20,
@ -112,10 +110,7 @@ export function newGameState(params: RunParams): GameState {
autoCleanUses: 0,
...defaultSounds(),
rerolls: 0,
loop: 0,
baseCombo: 1,
mode: params?.mode || "short",
readyToRender: true,
creative: sumOfValues(params.perks)>1
};
resetBalls(gameState);

View file

@ -25,11 +25,6 @@ export const options = {
name: t("main_menu.colorful_coins"),
help: t("main_menu.colorful_coins_help"),
},
opaque_coins: {
default: true,
name: t("main_menu.opaque_coins"),
help: t("main_menu.opaque_coins_help"),
},
extra_bright: {
default: true,
name: t("main_menu.extra_bright"),

View file

@ -40,7 +40,7 @@ export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
);
// Here we don't use drawText as we don't want to cache a picture for each distinct value of score
recordCanvasCtx.fillStyle = "#FFF";
recordCanvasCtx.fillStyle = "#FFFFFF";
recordCanvasCtx.textBaseline = "top";
recordCanvasCtx.font = "12px monospace";
recordCanvasCtx.textAlign = "right";

View file

@ -1,21 +1,19 @@
import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
import {baseCombo, forEachLiveOne, liveCount} from "./gameStateMutators";
import {
brickCenterX,
brickCenterY,
// countBricksAbove,
// countBricksBelow,
currentLevelInfo,
isMovingWhilePassiveIncome,
isPickyEatingPossible,
telekinesisEffectRate,
yoyoEffectRate,
max_levels,
reachRedRowIndex,
telekinesisEffectRate,
yoyoEffectRate,
} from "./game_utils";
import { colorString, GameState } from "./types";
import { t } from "./i18n/i18n";
import { gameState, lastMeasuredFPS } from "./game";
import { isOptionOn } from "./options";
import {Coin, colorString, GameState} from "./types";
import {t} from "./i18n/i18n";
import {gameState, lastMeasuredFPS} from "./game";
import {isOptionOn} from "./options";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
export const ctx = gameCanvas.getContext("2d", {
@ -42,21 +40,14 @@ const haloCanvasCtx = haloCanvas.getContext("2d", {
export const haloScale = 8;
export function render(gameState: GameState) {
if (!gameState.readyToRender) return;
const level = currentLevelInfo(gameState);
const hasCombo = gameState.combo > baseCombo(gameState);
const { width, height } = gameCanvas;
const {width, height} = gameCanvas;
if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) {
menuLabel.innerText = gameState.loop
? t("play.current_lvl_loop", {
level: gameState.currentLevel + 1,
max: max_levels(gameState),
loop: gameState.loop,
})
: t("play.current_lvl", {
menuLabel.innerText = t("play.current_lvl", {
level: gameState.currentLevel + 1,
max: max_levels(gameState),
});
@ -102,92 +93,65 @@ export function render(gameState: GameState) {
// Clear
if (!isOptionOn("basic") && level.svg && level.color === "#000000") {
haloCanvasCtx.globalCompositeOperation = "source-over";
haloCanvasCtx.globalAlpha = 0.9;
haloCanvasCtx.globalAlpha = 0.99;
haloCanvasCtx.fillStyle = level.color;
haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale);
haloCanvasCtx.globalCompositeOperation = "screen";
const brightness = isOptionOn("extra_bright") ? 3:1
haloCanvasCtx.globalCompositeOperation = "lighten";
haloCanvasCtx.globalAlpha = 0.1;
forEachLiveOne(gameState.coins, (coin) => {
const color =
gameState.perks.metamorphosis || isOptionOn("colorful_coins")
? coin.color
: "gold";
haloCanvasCtx.globalAlpha = 0.5;
const color = getCoinRenderColor(gameState, coin)
drawFuzzyBall(
haloCanvasCtx,
color,
(gameState.coinSize * 2) / haloScale,
(gameState.coinSize * 2 * brightness) / haloScale,
coin.x / haloScale,
coin.y / haloScale,
);
if (isOptionOn("extra_bright")) {
haloCanvasCtx.globalAlpha = 0.2;
drawFuzzyBall(
haloCanvasCtx,
color,
(gameState.coinSize * 10) / haloScale,
coin.x / haloScale,
coin.y / haloScale,
);
}
});
haloCanvasCtx.globalAlpha = 0.1;
gameState.balls.forEach((ball) => {
haloCanvasCtx.globalAlpha = 0.5;
drawFuzzyBall(
haloCanvasCtx,
gameState.ballsColor,
(gameState.ballSize * 3) / haloScale,
(gameState.ballSize * 3*brightness) / haloScale,
ball.x / haloScale,
ball.y / haloScale,
);
if (isOptionOn("extra_bright")) {
haloCanvasCtx.globalAlpha = 0.2;
drawFuzzyBall(
haloCanvasCtx,
gameState.ballsColor,
(gameState.ballSize * 6) / haloScale,
ball.x / haloScale,
ball.y / haloScale,
);
}
});
haloCanvasCtx.globalAlpha = isOptionOn("extra_bright") ? 0.2 : 0.05;
haloCanvasCtx.globalAlpha = 0.05
gameState.bricks.forEach((color, index) => {
if (!color) return;
const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index);
drawFuzzyBall(
haloCanvasCtx,
color == "black" ? "#666" : color,
(gameState.brickWidth * 2) / haloScale,
color == "black" ? "#666666" : color,
(gameState.brickWidth * 2*brightness) / haloScale,
x / haloScale,
y / haloScale,
);
});
haloCanvasCtx.globalCompositeOperation = "screen";
haloCanvasCtx.globalAlpha = 0.3;
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;
haloCanvasCtx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
// haloCanvasCtx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawFuzzyBall(
haloCanvasCtx,
color,
(size * 3) / haloScale,
(size * 3*brightness) / haloScale,
x / haloScale,
y / haloScale,
);
if (isOptionOn("extra_bright")) {
haloCanvasCtx.globalAlpha *= 0.5;
drawFuzzyBall(
haloCanvasCtx,
color,
(size * 6) / haloScale,
x / haloScale,
y / haloScale,
);
}
});
ctx.globalAlpha = 1;
@ -215,7 +179,7 @@ export function render(gameState: GameState) {
const chars = lineWidth * lines;
let start = Math.ceil(Math.random() * (pageSource.length - chars));
for (let i = 0; i < lines; i++) {
bgctx.fillStyle = "white";
bgctx.fillStyle = "#FFFFFF";
bgctx.font = "20px Courier";
bgctx.fillText(
pageSource.slice(
@ -250,7 +214,7 @@ export function render(gameState: GameState) {
ctx.fillStyle = level.color || "#000";
ctx.fillRect(0, 0, width, height);
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;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawBall(ctx, color, size, x, y);
@ -273,34 +237,26 @@ export function render(gameState: GameState) {
// Coins
ctx.globalAlpha = 1;
forEachLiveOne(gameState.coins, (coin) => {
const color =
gameState.perks.metamorphosis || isOptionOn("colorful_coins")
? coin.color
: "gold";
const color = getCoinRenderColor(gameState,coin)
// ctx.globalCompositeOperation = "source-over";
ctx.globalCompositeOperation =
color === "gold" ||
level.color !== "#000000" ||
isOptionOn("opaque_coins")
? "source-over"
: "screen";
ctx.globalCompositeOperation = "source-over"
drawCoin(
ctx,
color,
coin.size,
coin.x,
coin.y,
(hasCombo && gameState.perks.asceticism && "red") ||
(color === "gold" && "gold") ||
isOptionOn("opaque_coins")
? gameState.puckColor
: color,
(hasCombo && gameState.perks.asceticism && "#FF0000") ||
(color === "#ffd300" && "#ffd300") ||
(color == "#231f20" && gameState.level.color == "#000000" && "#FFFFFF")
|| (gameState.level.color ),
coin.a,
);
});
console.log(gameState.level.color)
// Black shadow around balls
if (!isOptionOn("basic")) {
if (!isOptionOn("basic") ) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = Math.min(0.8, liveCount(gameState.coins) / 20);
gameState.balls.forEach((ball) => {
@ -319,15 +275,15 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "screen";
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;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5;
drawBrick(ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
drawBrick(gameState, ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
});
ctx.globalCompositeOperation = "screen";
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;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "source-over";
@ -335,7 +291,7 @@ export function render(gameState: GameState) {
});
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;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "screen";
@ -429,7 +385,7 @@ export function render(gameState: GameState) {
if (totalWidth < gameState.puckWidth) {
drawCoin(
ctx,
"gold",
"#ffd300",
gameState.coinSize,
left + gameState.coinSize / 2,
gameState.gameZoneHeight - gameState.puckHeight / 2,
@ -482,7 +438,7 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
(redLeftSide && "red") || "white",
(redLeftSide && "#FF0000") || "#FFFFFF",
gameState.offsetX - 1,
0,
gameState.offsetX - 1,
@ -493,7 +449,7 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
(redRightSide && "red") || "white",
(redRightSide && "#FF0000") || "#FFFFFF",
width - gameState.offsetX + 1,
0,
width - gameState.offsetX + 1,
@ -504,7 +460,7 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
(redLeftSide && "red") || "",
(redLeftSide && "#FF0000") || "",
0,
0,
0,
@ -515,7 +471,7 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
(redRightSide && "red") || "",
(redRightSide && "#FF0000") || "",
width - 1,
0,
width - 1,
@ -527,7 +483,7 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
"red",
"#FF0000",
gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown,
1,
gameState.perks.unbounded ? width : width - gameState.offsetXRoundedDown,
@ -539,8 +495,8 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
(hasCombo && gameState.perks.compound_interest && "red") ||
(isOptionOn("mobile-mode") && "white") ||
(hasCombo && gameState.perks.compound_interest && "#FF0000") ||
(isOptionOn("mobile-mode") && "#FFFFFF") ||
"",
gameState.offsetXRoundedDown,
gameState.gameZoneHeight,
@ -557,7 +513,7 @@ export function render(gameState: GameState) {
) {
// haloCanvasCtx.globalCompositeOperation = 'multiply';
// haloCanvasCtx.fillRect(0,0,haloCanvas.width,haloCanvas.height)
haloCanvasCtx.fillStyle = "white";
haloCanvasCtx.fillStyle = "#FFFFFF";
haloCanvasCtx.globalAlpha = 0.25;
haloCanvasCtx.globalCompositeOperation = "screen";
haloCanvasCtx.fillRect(0, 0, haloCanvas.width, haloCanvas.height);
@ -588,7 +544,7 @@ export function render(gameState: GameState) {
function drawStraightLine(
ctx: CanvasRenderingContext2D,
gameState: GameState,
mode: "white" | "" | "red",
mode: "#FFFFFF" | "" | "#FF0000",
x1,
y1,
x2,
@ -597,20 +553,20 @@ function drawStraightLine(
) {
ctx.globalAlpha = alpha;
if (!mode) return;
if (mode == "red") {
ctx.strokeStyle = "red";
if (mode == "#FF0000") {
ctx.strokeStyle = "#FF0000";
ctx.lineDashOffset = getDashOffset(gameState);
ctx.lineWidth = 2;
ctx.setLineDash(redBorderDash);
} else {
ctx.strokeStyle = "white";
ctx.strokeStyle = "#FFFFFF";
ctx.lineWidth = 1;
}
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
if (mode == "red") {
if (mode == "#FF0000") {
ctx.setLineDash(emptyArray);
ctx.lineWidth = 1;
}
@ -631,7 +587,7 @@ export function renderAllBricks() {
const redColorOnAllBricks = hasCombo && isMovingWhilePassiveIncome(gameState);
const redRowReach = reachRedRowIndex(gameState);
const {clairvoyant}=gameState.perks
let offset = getDashOffset(gameState);
if (
!(
@ -645,7 +601,7 @@ export function renderAllBricks() {
}
const clairVoyance =
gameState.perks.clairvoyant && gameState.brickHP.reduce((a, b) => a + b, 0);
clairvoyant && gameState.brickHP.reduce((a, b) => a + b, 0);
const newKey =
gameState.gameZoneWidth +
@ -697,21 +653,21 @@ export function renderAllBricks() {
redColorOnAllBricks;
canctx.globalCompositeOperation = "source-over";
drawBrick(
drawBrick(gameState,
canctx,
color,
x,
y,
redBorder ? offset : -1,
gameState.perks.clairvoyant >= 2,
clairvoyant >= 2,
);
if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) {
canctx.globalCompositeOperation =
gameState.perks.clairvoyant >= 2 ? "source-over" : "destination-out";
if (gameState.brickHP[index] > 1 && clairvoyant) {
canctx.globalCompositeOperation = "source-over"
drawText(
canctx,
gameState.brickHP[index].toString(),
color,
clairvoyant >= 2 ? color: gameState.level.color,
gameState.puckHeight,
x,
y,
@ -788,7 +744,7 @@ export function drawPuck(
canctx.fill();
if (redBorderOffset !== -1) {
canctx.strokeStyle = "red";
canctx.strokeStyle = "#FF0000";
canctx.lineWidth = 4;
canctx.setLineDash(redBorderDash);
canctx.lineDashOffset = redBorderOffset;
@ -864,7 +820,7 @@ export function drawCoin(
"_" +
borderColor +
"_" +
(color === "gold" ? angle : "whatever");
(color === "#ffd300" ? angle : "whatever");
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@ -880,13 +836,13 @@ export function drawCoin(
canctx.fill();
canctx.strokeStyle = borderColor;
if (borderColor == "red") {
if (borderColor == "#FF0000") {
canctx.lineWidth = 2;
canctx.setLineDash(redBorderDash);
}
canctx.stroke();
if (color === "gold") {
if (color === "#ffd300") {
// Fill in
canctx.beginPath();
canctx.arc(size / 2, size / 2, (size / 2) * 0.6, 0, 2 * Math.PI);
@ -935,6 +891,9 @@ export function drawFuzzyBall(
size / 2,
);
gradient.addColorStop(0, color);
console.log(color)
gradient.addColorStop(0.3, color+"88");
gradient.addColorStop(0.6, color+"22");
gradient.addColorStop(1, "transparent");
canctx.fillStyle = gradient;
canctx.fillRect(0, 0, size, size);
@ -948,6 +907,7 @@ export function drawFuzzyBall(
}
export function drawBrick(
gameState:GameState,
ctx: CanvasRenderingContext2D,
color: colorString,
x: number,
@ -962,6 +922,9 @@ export function drawBrick(
const width = brx - tlx,
height = bry - tly;
const whiteBorder = (offset == -1 && color == "#231f20" && gameState.level.color == "#000000" && "#FFFFFF")
const key =
"brick" +
color +
@ -973,7 +936,7 @@ export function drawBrick(
"_" +
offset +
"_" +
borderOnly;
borderOnly + '_' + whiteBorder;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@ -987,9 +950,9 @@ export function drawBrick(
canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray);
canctx.lineDashOffset = offset;
canctx.strokeStyle = offset !== -1 ? "red" : color;
canctx.strokeStyle = (offset !== -1 && "#FF0000") || whiteBorder || color;
canctx.lineJoin = "round";
canctx.lineWidth = bord;
canctx.lineWidth = whiteBorder ? 1 : bord;
roundRect(
canctx,
bord / 2,
@ -1106,3 +1069,10 @@ export function getDashOffset(gameState: GameState) {
}
return Math.floor(((gameState.levelTime % 500) / 500) * 10) % 10;
}
function getCoinRenderColor(gameState: GameState, coin: Coin) {
if(gameState.perks.metamorphosis || isOptionOn("colorful_coins"))
return coin.color
return "#ffd300"
}

View file

@ -30,6 +30,7 @@ export function getTotalScore() {
}
export function addToTotalScore(gameState: GameState, points: number) {
if(!gameState.creative)
setSettingValue("breakout_71_total_score", getTotalScore() + points);
}

7
src/types.d.ts vendored
View file

@ -167,7 +167,6 @@ export type ReusableArray<T> = {
export type RunHistoryItem = RunStats & {
perks?: PerksMap;
mode: GameState["mode"];
appVersion?: string;
};
export type GameState = {
@ -286,17 +285,13 @@ export type GameState = {
colorChange: { vol: number; x: number };
};
rerolls: number;
loop: number;
baseCombo: number;
mode: "short" | "long" | "creative";
readyToRender: boolean;
creative:boolean;
};
export type RunParams = {
level?: string;
levelToAvoid?: string;
perks?: Partial<PerksMap>;
mode: "short" | "long" | "creative";
};
export type OptionDef = {
default: boolean;