Build 29035725

This commit is contained in:
Renan LE CARO 2025-03-16 17:45:29 +01:00
parent a1bf54af71
commit 819197031f
64 changed files with 3494 additions and 6921 deletions

View file

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 715 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 445 B

Before After
Before After

8
src/PWA/icon.svg Normal file
View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" height="50" width="50">
<rect x="0" y="0" width="30" height="10" fill="#6262EA"/>
<rect x="20" y="10" width="10" height="10" fill="#6262EA"/>
<rect x="10" y="20" width="10" height="20" fill="#6262EA"/>
<rect x="20" y="20" width="10" height="10" fill="#5DA3EA"/>
<rect x="30" y="10" width="10" height="30" fill="#5DA3EA"/>
<rect x="20" y="40" width="40" height="30" fill="#5DA3EA"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View file

@ -1,5 +1,5 @@
// The version of the cache.
const VERSION = "29033878";
const VERSION = "29035725";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -1,122 +1,121 @@
import {t} from "./i18n/i18n";
import { t } from "./i18n/i18n";
export let alertsOpen = 0,
closeModal: null | (() => void) = null;
closeModal: null | (() => void) = null;
export type AsyncAlertAction<t> = {
text?: string;
value?: t;
help?: string;
disabled?: boolean;
icon?: string;
className?: string;
text?: string;
value?: t;
help?: string;
disabled?: boolean;
icon?: string;
className?: string;
};
export function asyncAlert<t>({
title,
text,
actions,
allowClose = true,
textAfterButtons = "",
actionsAsGrid = false,
}: {
title?: string;
text?: string;
actions?: AsyncAlertAction<t>[];
textAfterButtons?: string;
allowClose?: boolean;
actionsAsGrid?: boolean;
title,
text,
actions,
allowClose = true,
textAfterButtons = "",
actionsAsGrid = false,
}: {
title?: string;
text?: string;
actions?: AsyncAlertAction<t>[];
textAfterButtons?: string;
allowClose?: boolean;
actionsAsGrid?: boolean;
}): Promise<t | void> {
alertsOpen++;
return new Promise((resolve) => {
const popupWrap = document.createElement("div");
document.body.appendChild(popupWrap);
popupWrap.className = "popup " + (actionsAsGrid ? "actionsAsGrid " : "");
alertsOpen++;
return new Promise((resolve) => {
const popupWrap = document.createElement("div");
document.body.appendChild(popupWrap);
popupWrap.className = "popup " + (actionsAsGrid ? "actionsAsGrid " : "");
function closeWithResult(value: t | undefined) {
resolve(value);
// Doing this async lets the menu scroll persist if it's shown a second time
setTimeout(() => {
document.body.removeChild(popupWrap);
});
}
function closeWithResult(value: t | undefined) {
resolve(value);
// Doing this async lets the menu scroll persist if it's shown a second time
setTimeout(() => {
document.body.removeChild(popupWrap);
});
}
if (allowClose) {
const closeButton = document.createElement("button");
closeButton.title = t('play.close_modale_window_tooltip');
closeButton.className = "close-modale";
closeButton.addEventListener("click", (e) => {
e.preventDefault();
closeWithResult(undefined);
});
closeModal = () => {
closeWithResult(undefined);
};
popupWrap.appendChild(closeButton);
}
if (allowClose) {
const closeButton = document.createElement("button");
closeButton.title = t("play.close_modale_window_tooltip");
closeButton.className = "close-modale";
closeButton.addEventListener("click", (e) => {
e.preventDefault();
closeWithResult(undefined);
});
closeModal = () => {
closeWithResult(undefined);
};
popupWrap.appendChild(closeButton);
}
const popup = document.createElement("div");
const popup = document.createElement("div");
if (title) {
const p = document.createElement("h2");
p.innerHTML = title;
popup.appendChild(p);
}
if (title) {
const p = document.createElement("h2");
p.innerHTML = title;
popup.appendChild(p);
}
if (text) {
const p = document.createElement("div");
p.innerHTML = text;
popup.appendChild(p);
}
if (text) {
const p = document.createElement("div");
p.innerHTML = text;
popup.appendChild(p);
}
const buttons = document.createElement("section");
popup.appendChild(buttons);
const buttons = document.createElement("section");
popup.appendChild(buttons);
actions
?.filter((i) => i)
.forEach(({text, value, help, disabled, className = "", icon = ""}) => {
const button = document.createElement("button");
actions
?.filter((i) => i)
.forEach(({ text, value, help, disabled, className = "", icon = "" }) => {
const button = document.createElement("button");
button.innerHTML = `
button.innerHTML = `
${icon}
<div>
<strong>${text}</strong>
<em>${help || ""}</em>
</div>`;
if (disabled) {
button.setAttribute("disabled", "disabled");
} else {
button.addEventListener("click", (e) => {
e.preventDefault();
closeWithResult(value);
});
}
button.className = className;
buttons.appendChild(button);
});
if (textAfterButtons) {
const p = document.createElement("div");
p.className = "textAfterButtons";
p.innerHTML = textAfterButtons;
popup.appendChild(p);
if (disabled) {
button.setAttribute("disabled", "disabled");
} else {
button.addEventListener("click", (e) => {
e.preventDefault();
closeWithResult(value);
});
}
button.className = className;
buttons.appendChild(button);
});
popupWrap.appendChild(popup);
(
popup.querySelector("button:not([disabled])") as HTMLButtonElement
)?.focus();
}).then(
(v: unknown) => {
alertsOpen--;
closeModal = null;
return v as t | undefined;
},
() => {
closeModal = null;
alertsOpen--;
},
);
}
if (textAfterButtons) {
const p = document.createElement("div");
p.className = "textAfterButtons";
p.innerHTML = textAfterButtons;
popup.appendChild(p);
}
popupWrap.appendChild(popup);
(
popup.querySelector("button:not([disabled])") as HTMLButtonElement
)?.focus();
}).then(
(v: unknown) => {
alertsOpen--;
closeModal = null;
return v as t | undefined;
},
() => {
closeModal = null;
alertsOpen--;
},
);
}

View file

@ -8,7 +8,7 @@
},
{
"name": "Butterfly",
"bricks": "_________bb_t_t_bbbbb_t_bbbbbbbtbbbb_bbbtbbb____btb____bbbtbbb__bb_t_bb___________________",
"bricks": "_________bb_t_t_bbbbb_t_bbbbbbbtbbbb_bbbtbbb____btb____bbbtbbb__bb_t_bb__________",
"size": 9,
"svg": 20,
"color": ""
@ -834,5 +834,12 @@
"size": 6,
"bricks": "_W__W_WW__WW____________WW__WW_W__W_",
"svg": null
},
{
"name": "icon:concave_puck",
"size": 8,
"bricks": "___________W_______________W_______________W_____________WWWWW__",
"svg": null,
"color": ""
}
]

1
src/data/version.json Normal file
View file

@ -0,0 +1 @@
"29035725"

View file

@ -5,13 +5,13 @@
<title>Level editor</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎨</text></svg>"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔨</text></svg>"
/>
<link rel="stylesheet" href="./levels_editor.less" />
<link rel="stylesheet" href="./level_editor/levels_editor.less" />
</head>
<body>
<div id="app"></div>
<script type="module" src="levels_editor.tsx"></script>
<script type="module" src="./level_editor/levels_editor.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -1,251 +1,276 @@
import {allLevels, appVersion, upgrades} from "./loadGameData";
import {t} from "./i18n/i18n";
import {RunHistoryItem} from "./types";
import {gameState, pause, restart} from "./game";
import {currentLevelInfo, findLast} from "./game_utils";
import {getTotalScore} from "./settings";
import {stopRecording} from "./recording";
import {asyncAlert} from "./asyncAlert";
import { allLevels, appVersion, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { RunHistoryItem } from "./types";
import { gameState, pause, restart } from "./game";
import { currentLevelInfo, findLast } from "./game_utils";
import { getTotalScore } from "./settings";
import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert";
export function getUpgraderUnlockPoints() {
let list = [] as { threshold: number; title: string }[];
let list = [] as { threshold: number; title: string }[];
upgrades.forEach((u) => {
if (u.threshold) {
list.push({
threshold: u.threshold,
title: u.name + ' ' + t('level_up.unlocked_perk'),
});
}
upgrades.forEach((u) => {
if (u.threshold) {
list.push({
threshold: u.threshold,
title: u.name + " " + t("level_up.unlocked_perk"),
});
}
});
allLevels.forEach((l) => {
list.push({
threshold: l.threshold,
title: l.name + " " + t("level_up.unlocked_level"),
});
});
allLevels.forEach((l) => {
list.push({
threshold: l.threshold,
title: l.name + ' ' + t('level_up.unlocked_level'),
});
});
return list
.filter((o) => o.threshold)
.sort((a, b) => a.threshold - b.threshold);
return list
.filter((o) => o.threshold)
.sort((a, b) => a.threshold - b.threshold);
}
export function addToTotalPlayTime(ms: number) {
try {
localStorage.setItem(
"breakout_71_total_play_time",
JSON.stringify(
JSON.parse(localStorage.getItem("breakout_71_total_play_time") || "0") +
ms,
),
);
} catch (e) {
}
try {
localStorage.setItem(
"breakout_71_total_play_time",
JSON.stringify(
JSON.parse(localStorage.getItem("breakout_71_total_play_time") || "0") +
ms,
),
);
} catch (e) {}
}
export function gameOver(title: string, intro: string) {
if (!gameState.running) return;
pause(true);
stopRecording();
addToTotalPlayTime(gameState.runStatistics.runTime);
gameState.runStatistics.max_level = gameState.currentLevel + 1;
if (!gameState.running) return;
pause(true);
stopRecording();
addToTotalPlayTime(gameState.runStatistics.runTime);
gameState.runStatistics.max_level = gameState.currentLevel + 1;
let animationDelay = -300;
const getDelay = () => {
animationDelay += 800;
return "animation-delay:" + animationDelay + "ms;";
};
// unlocks
let unlocksInfo = "";
const endTs = getTotalScore();
const startTs = endTs - gameState.score;
const list = getUpgraderUnlockPoints();
list
.filter((u) => u.threshold > startTs && u.threshold < endTs)
.forEach((u) => {
unlocksInfo += `
let animationDelay = -300;
const getDelay = () => {
animationDelay += 800;
return "animation-delay:" + animationDelay + "ms;";
};
// unlocks
let unlocksInfo = "";
const endTs = getTotalScore();
const startTs = endTs - gameState.score;
const list = getUpgraderUnlockPoints();
list
.filter((u) => u.threshold > startTs && u.threshold < endTs)
.forEach((u) => {
unlocksInfo += `
<p class="progress" >
<span>${u.title}</span>
<span class="progress_bar_part" style="${getDelay()}"></span>
</p>
`;
});
const previousUnlockAt =
findLast(list, (u) => u.threshold <= endTs)?.threshold || 0;
const nextUnlock = list.find((u) => u.threshold > endTs);
});
const previousUnlockAt =
findLast(list, (u) => u.threshold <= endTs)?.threshold || 0;
const nextUnlock = list.find((u) => u.threshold > endTs);
if (nextUnlock) {
const total = nextUnlock?.threshold - previousUnlockAt;
const done = endTs - previousUnlockAt;
if (nextUnlock) {
const total = nextUnlock?.threshold - previousUnlockAt;
const done = endTs - previousUnlockAt;
intro += t('gameOver.next_unlock', {points: nextUnlock.threshold - endTs});
intro += t("gameOver.next_unlock", {
points: nextUnlock.threshold - endTs,
});
const scaleX = (done / total).toFixed(2);
unlocksInfo += `
const scaleX = (done / total).toFixed(2);
unlocksInfo += `
<p class="progress" >
<span>${nextUnlock.title}</span>
<span style="transform: scale(${scaleX},1);${getDelay()}" class="progress_bar_part"></span>
</p>
`;
list
.slice(list.indexOf(nextUnlock) + 1)
.slice(0, 3)
.forEach((u) => {
unlocksInfo += `
list
.slice(list.indexOf(nextUnlock) + 1)
.slice(0, 3)
.forEach((u) => {
unlocksInfo += `
<p class="progress" >
<span>${u.title}</span>
</p>
`;
});
}
});
}
let unlockedItems = list.filter(
(u) => u.threshold > startTs && u.threshold < endTs,
);
if (unlockedItems.length) {
let unlockedItems = list.filter(
(u) => u.threshold > startTs && u.threshold < endTs,
);
if (unlockedItems.length) {
unlocksInfo += `<p>${t("gameOver.unlocked_count", { count: unlockedItems.length })} ${unlockedItems.map((u) => u.title).join(", ")}</p>`;
}
unlocksInfo += `<p>${t('gameOver.unlocked_count', {count: unlockedItems.length})} ${unlockedItems.map((u) => u.title).join(", ")}</p>`;
}
// Avoid the sad sound right as we restart a new games
gameState.combo = 1;
// Avoid the sad sound right as we restart a new games
gameState.combo = 1;
asyncAlert({
allowClose: true,
title,
text: `
${gameState.isCreativeModeRun ? `<p>${t('gameOver.test_run')}</p> ` : ""}
asyncAlert({
allowClose: true,
title,
text: `
${gameState.isCreativeModeRun ? `<p>${t("gameOver.test_run")}</p> ` : ""}
<p>${intro}</p>
<p>${t('gameOver.cumulative_total', {startTs, endTs})}</p>
<p>${t("gameOver.cumulative_total", { startTs, endTs })}</p>
${unlocksInfo}
`,
actions: [
{
value: null,
text: t('gameOver.restart'),
help: "",
},
],
textAfterButtons: `<div id="level-recording-container"></div>
actions: [
{
value: null,
text: t("gameOver.restart"),
help: "",
},
],
textAfterButtons: `<div id="level-recording-container"></div>
${getHistograms()}
`,
}).then(() => restart({levelToAvoid: currentLevelInfo(gameState).name}));
}).then(() => restart({ levelToAvoid: currentLevelInfo(gameState).name }));
}
export function getHistograms() {
let runStats = "";
try {
// Stores only top 100 runs
let runsHistory = JSON.parse(
localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[];
runsHistory.sort((a, b) => a.score - b.score).reverse();
runsHistory = runsHistory.slice(0, 100);
let runStats = "";
try {
// Stores only top 100 runs
let runsHistory = JSON.parse(
localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[];
runsHistory.sort((a, b) => a.score - b.score).reverse();
runsHistory = runsHistory.slice(0, 100);
runsHistory.push({
...gameState.runStatistics,
perks: gameState.perks,
appVersion,
});
runsHistory.push({
...gameState.runStatistics,
perks: gameState.perks,
appVersion,
});
// Generate some histogram
if (!gameState.isCreativeModeRun)
localStorage.setItem(
"breakout_71_runs_history",
JSON.stringify(runsHistory, null, 2),
);
// Generate some histogram
if (!gameState.isCreativeModeRun)
localStorage.setItem(
"breakout_71_runs_history",
JSON.stringify(runsHistory, null, 2),
);
const makeHistogram = (
title: string,
getter: (hi: RunHistoryItem) => number,
unit: string,
) => {
let values = runsHistory.map((h) => getter(h) || 0);
let min = Math.min(...values);
let max = Math.max(...values);
// No point
if (min === max) return "";
if (max - min < 10) {
// This is mostly useful for levels
min = Math.max(0, max - 10);
max = Math.max(max, min + 10);
}
// One bin per unique value, max 10
const binsCount = Math.min(values.length, 10);
if (binsCount < 3) return "";
const bins = [] as number[];
const binsTotal = [] as number[];
for (let i = 0; i < binsCount; i++) {
bins.push(0);
binsTotal.push(0);
}
const binSize = (max - min) / bins.length;
const binIndexOf = (v: number) =>
Math.min(bins.length - 1, Math.floor((v - min) / binSize));
values.forEach((v) => {
if (isNaN(v)) return;
const index = binIndexOf(v);
bins[index]++;
binsTotal[index] += v;
});
if (bins.filter((b) => b).length < 3) return "";
const maxBin = Math.max(...bins);
const lastValue = values[values.length - 1];
const activeBin = binIndexOf(lastValue);
const makeHistogram = (
title: string,
getter: (hi: RunHistoryItem) => number,
unit: string,
) => {
let values = runsHistory.map((h) => getter(h) || 0);
let min = Math.min(...values);
let max = Math.max(...values);
// No point
if (min === max) return "";
if (max - min < 10) {
// This is mostly useful for levels
min = Math.max(0, max - 10);
max = Math.max(max, min + 10);
}
// One bin per unique value, max 10
const binsCount = Math.min(values.length, 10);
if (binsCount < 3) return "";
const bins = [] as number[];
const binsTotal = [] as number[];
for (let i = 0; i < binsCount; i++) {
bins.push(0);
binsTotal.push(0);
}
const binSize = (max - min) / bins.length;
const binIndexOf = (v: number) =>
Math.min(bins.length - 1, Math.floor((v - min) / binSize));
values.forEach((v) => {
if (isNaN(v)) return;
const index = binIndexOf(v);
bins[index]++;
binsTotal[index] += v;
});
if (bins.filter((b) => b).length < 3) return "";
const maxBin = Math.max(...bins);
const lastValue = values[values.length - 1];
const activeBin = binIndexOf(lastValue);
const bars = bins
.map((v, vi) => {
const style = `height: ${(v / maxBin) * 80}px`;
return `<span class="${vi === activeBin ? "active" : ""}"><span style="${style}" title="${v} run${v > 1 ? "s" : ""} between ${Math.floor(min + vi * binSize)} and ${Math.floor(min + (vi + 1) * binSize)}${unit}"
const bars = bins
.map((v, vi) => {
const style = `height: ${(v / maxBin) * 80}px`;
return `<span class="${vi === activeBin ? "active" : ""}"><span style="${style}" title="${v} run${v > 1 ? "s" : ""} between ${Math.floor(min + vi * binSize)} and ${Math.floor(min + (vi + 1) * binSize)}${unit}"
><span>${(!v && " ") || (vi == activeBin && lastValue + unit) || Math.round(binsTotal[vi] / v) + unit}</span></span></span>`;
})
.join("");
})
.join("");
return `<h2 class="histogram-title">${title} : <strong>${lastValue}${unit}</strong></h2>
return `<h2 class="histogram-title">${title} : <strong>${lastValue}${unit}</strong></h2>
<div class="histogram">${bars}</div>
`;
};
};
runStats += makeHistogram(t('gameOver.stats.total_score'), (r) => r.score, "");
runStats += makeHistogram(t('gameOver.stats.catch_rate'),
(r) => Math.round((r.score / r.coins_spawned) * 100),
"%",
);
runStats += makeHistogram(t('gameOver.stats.bricks_broken'), (r) => r.bricks_broken, "");
runStats += makeHistogram(
t('gameOver.stats.bricks_per_minute'),
(r) => Math.round((r.bricks_broken / r.runTime) * 1000 * 60),
"",
);
runStats += makeHistogram(
t('gameOver.stats.hit_rate'),
(r) => Math.round((1 - r.misses / r.puck_bounces) * 100),
"%",
);
runStats += makeHistogram(
t('gameOver.stats.duration_per_level'),
(r) => Math.round(r.runTime / 1000 / r.levelsPlayed),
"s",
);
runStats += makeHistogram(t('gameOver.stats.level_reached'), (r) => r.levelsPlayed, "");
runStats += makeHistogram(t('gameOver.stats.upgrades_applied'), (r) => r.upgrades_picked, "");
runStats += makeHistogram(t('gameOver.stats.balls_lost'), (r) => r.balls_lost, "");
runStats += makeHistogram(
t('gameOver.stats.combo_avg'),
(r) => Math.round(r.coins_spawned / r.bricks_broken),
"",
);
runStats += makeHistogram(t('gameOver.stats.combo_max'), (r) => r.max_combo, "");
runStats += makeHistogram(
t("gameOver.stats.total_score"),
(r) => r.score,
"",
);
runStats += makeHistogram(
t("gameOver.stats.catch_rate"),
(r) => Math.round((r.score / r.coins_spawned) * 100),
"%",
);
runStats += makeHistogram(
t("gameOver.stats.bricks_broken"),
(r) => r.bricks_broken,
"",
);
runStats += makeHistogram(
t("gameOver.stats.bricks_per_minute"),
(r) => Math.round((r.bricks_broken / r.runTime) * 1000 * 60),
"",
);
runStats += makeHistogram(
t("gameOver.stats.hit_rate"),
(r) => Math.round((1 - r.misses / r.puck_bounces) * 100),
"%",
);
runStats += makeHistogram(
t("gameOver.stats.duration_per_level"),
(r) => Math.round(r.runTime / 1000 / r.levelsPlayed),
"s",
);
runStats += makeHistogram(
t("gameOver.stats.level_reached"),
(r) => r.levelsPlayed,
"",
);
runStats += makeHistogram(
t("gameOver.stats.upgrades_applied"),
(r) => r.upgrades_picked,
"",
);
runStats += makeHistogram(
t("gameOver.stats.balls_lost"),
(r) => r.balls_lost,
"",
);
runStats += makeHistogram(
t("gameOver.stats.combo_avg"),
(r) => Math.round(r.coins_spawned / r.bricks_broken),
"",
);
runStats += makeHistogram(
t("gameOver.stats.combo_max"),
(r) => r.max_combo,
"",
);
if (runStats) {
runStats =
`<p>${t('gameOver.stats.intro', {count: runsHistory.length - 1})}</p>` +
runStats;
}
} catch (e) {
console.warn(e);
if (runStats) {
runStats =
`<p>${t("gameOver.stats.intro", { count: runsHistory.length - 1 })}</p>` +
runStats;
}
return runStats;
}
} catch (e) {
console.warn(e);
}
return runStats;
}

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,6 @@ import {
sample,
sumOfKeys,
} from "./game_utils";
import { Upgrade } from "./types";
describe("getMajorityValue", () => {
it("returns the most common string", () => {

View file

@ -1,5 +1,5 @@
import {Ball, GameState, PerkId, PerksMap} from "./types";
import {icons, upgrades} from "./loadGameData";
import { Ball, GameState, PerkId, PerksMap } from "./types";
import { icons, upgrades } from "./loadGameData";
export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {};
@ -26,8 +26,8 @@ export const makeEmptyPerksMap = (upgrades: { id: PerkId }[]) => {
export function brickCenterX(gameState: GameState, index: number) {
return (
gameState.offsetX +
((index % gameState.gridSize) + 0.5) * gameState.brickWidth
gameState.offsetX +
((index % gameState.gridSize) + 0.5) * gameState.brickWidth
);
}
@ -37,10 +37,10 @@ export function brickCenterY(gameState: GameState, index: number) {
export function getRowColIndex(gameState: GameState, row: number, col: number) {
if (
row < 0 ||
col < 0 ||
row >= gameState.gridSize ||
col >= gameState.gridSize
row < 0 ||
col < 0 ||
row >= gameState.gridSize ||
col >= gameState.gridSize
)
return -1;
return row * gameState.gridSize + col;
@ -48,8 +48,8 @@ export function getRowColIndex(gameState: GameState, row: number, col: number) {
export function getPossibleUpgrades(gameState: GameState) {
return upgrades
.filter((u) => gameState.totalScoreAtRunStart >= u.threshold)
.filter((u) => !u?.requires || gameState.perks[u?.requires]);
.filter((u) => gameState.totalScoreAtRunStart >= u.threshold)
.filter((u) => !u?.requires || gameState.perks[u?.requires]);
}
export function max_levels(gameState: GameState) {
@ -67,8 +67,8 @@ export function pickedUpgradesHTMl(gameState: GameState) {
export function currentLevelInfo(gameState: GameState) {
return gameState.runLevels[
gameState.currentLevel % gameState.runLevels.length
];
gameState.currentLevel % gameState.runLevels.length
];
}
export function isTelekinesisActive(gameState: GameState, ball: Ball) {
@ -76,8 +76,8 @@ export function isTelekinesisActive(gameState: GameState, ball: Ball) {
}
export function findLast<T>(
arr: T[],
predicate: (item: T, index: number, array: T[]) => boolean,
arr: T[],
predicate: (item: T, index: number, array: T[]) => boolean,
) {
let i = arr.length;
while (--i)
@ -87,15 +87,15 @@ export function findLast<T>(
}
export function distance2(
a: { x: number; y: number },
b: { x: number; y: number },
a: { x: number; y: number },
b: { x: number; y: number },
) {
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
}
export function distanceBetween(
a: { x: number; y: number },
b: { x: number; y: number },
a: { x: number; y: number },
b: { x: number; y: number },
) {
return Math.sqrt(distance2(a, b));
}
}

View file

@ -1,6 +1,6 @@
import { RawLevel } from "./types";
import _backgrounds from "./backgrounds.json";
import _backgrounds from "./data/backgrounds.json";
const backgrounds = _backgrounds as string[];
export function getLevelBackground(level: RawLevel) {

View file

@ -1792,6 +1792,56 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>concave_puck</name>
<children>
<concept_node>
<name>fullHelp</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>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>name</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>
<name>extra_levels</name>
<children>

View file

@ -25,8 +25,8 @@
"gameOver.win.summary": "You cleared all levels for this run, catching {{score}} coins in total.",
"gameOver.win.title": "Run finished",
"level_up.after_buttons": "You just finished level {{level}}/{{max}} and picked those upgrades so far :",
"level_up.before_buttons": "You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds ${timeGain}.\n\nYou missed {{levelMisses}} times {{missesGain}}.\n\n{{compliment}}",
"level_up.compliment_advice": "Try to catch all coins, never miss the bricks or clear the level under 30s to gain additional choices and upgrades.",
"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}}",
"level_up.compliment_advice": "Try to catch all coins, never miss the bricks, never hit the walls/ceiling or clear the level under 30s to gain additional choices and upgrades.",
"level_up.compliment_good": "Well done !",
"level_up.compliment_perfect": "Impressive, keep it up !",
"level_up.pick_upgrade_title": "Pick an upgrade",
@ -74,7 +74,7 @@
"sandbox.instructions": "Select perks below and press \"start run\" to try them out in a test run. Scores and stats are not recorded.",
"sandbox.start": "Start test run",
"sandbox.title": "Sandbox mode",
"sandbox.unlocks_at": "Unlocks at total score ${{score}}",
"sandbox.unlocks_at": "Unlocks at total score {{score}}",
"score_panel.restart": "Restart",
"score_panel.restart_help": "Start a brand new run",
"score_panel.resume": "Resume",
@ -111,6 +111,9 @@
"upgrades.compound_interest.fullHelp": "Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. \n\nBe sure however to catch every one of those coins with your puck, as any lost coin will reset your combo. \n\nOnce your combo is above the minimum, the bottom of the play area will have a red line to remind you that coins should not go there.\n\nThis perk combines with other combo perks, the combo will rise faster but reset more easily.",
"upgrades.compound_interest.help": "+1 combo per brick broken, resets on coin lost",
"upgrades.compound_interest.name": "Compound interest",
"upgrades.concave_puck.fullHelp": "Balls starts the level going straight up, and bounces with less angle.",
"upgrades.concave_puck.help": " Helps with aiming straight up",
"upgrades.concave_puck.name": "Concave puck",
"upgrades.extra_levels.fullHelp": "The default run can last a max of 7 levels, after which the game is over and whatever score you reached is your run score. \n\nEach level of this perk lets you go one level higher. The last levels are often the ones where you make the most score, so the difference can be dramatic.",
"upgrades.extra_levels.help": "Play {{count}} levels instead of 7",
"upgrades.extra_levels.name": "+1 level",
@ -187,7 +190,7 @@
"upgrades.telekinesis.name": "Telekinesis",
"upgrades.top_is_lava.fullHelp": "Whenever you break a brick, your combo will increase by one. However, your combo will reset as soon as your ball hit the top of the screen. \n\nWhen your combo is above the minimum, a red bar will appear at the top to remind you that you should avoid hitting it. \n\nThe effect stacks with other combo perks.",
"upgrades.top_is_lava.help": "More coins if you don't touch the top.",
"upgrades.top_is_lava.name": "Icarus",
"upgrades.top_is_lava.name": "Sky is the limit",
"upgrades.viscosity.fullHelp": "Coins normally accelerate with gravity and explosions to pretty high speeds. \n\nThis perk constantly makes them slow down, as if they were in some sort of viscous liquid. \n\nThis makes catching them easier, and combines nicely with perks that influence the coin's movement.",
"upgrades.viscosity.help": "Slower coin fall",
"upgrades.viscosity.name": "Viscosity",

View file

@ -25,8 +25,8 @@
"gameOver.win.summary": "Vous avez nettoyé tous les niveaux pour cette partie, en attrapant {{score}} pièces au total.",
"gameOver.win.title": "Partie terminée",
"level_up.after_buttons": "Vous venez de terminer le niveau {{level}}/{{max}} et vous avez choisi ces améliorations jusqu'à présent :",
"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}}.\n\n{{compliment}}",
"level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.",
"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 cotés et le haut de la zone de jeu {{levelWallBounces}} fois {{wallHitsGain}}.\n\n{{compliment}}",
"level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques, de ne pas toucher les murs ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.",
"level_up.compliment_good": "Bravo !",
"level_up.compliment_perfect": "Impressionnant, continuez comme ça !",
"level_up.pick_upgrade_title": "Choisir une amélioration",
@ -74,7 +74,7 @@
"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.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.restart": "Redémarrer",
"score_panel.restart_help": "Commencer une nouvelle partie",
"score_panel.resume": "Continuer la partie",
@ -111,6 +111,9 @@
"upgrades.compound_interest.fullHelp": "Votre combo augmentera d'une unité à chaque fois que vous casserez une brique, générant de plus en plus de pièces à chaque fois que vous casserez une brique. Veillez cependant à attraper chacune de ces pièces avec votre palet, car toute pièce perdue remettra votre combo à zéro. \n \nSi votre combinaison est supérieure au minimum, une ligne rouge s'affichera au bas de la zone de jeu pour vous le rappeler que les pièces ne doivent pas aller à cet endroit.\n\nCet avantage se combine avec d'autres avantages de combo, le combo augmentera plus rapidement mais se réinitialisera plus souvent.",
"upgrades.compound_interest.help": "+1 combo par brique cassée, remise à zéro quand une pièce est perdu",
"upgrades.compound_interest.name": "Intérêts",
"upgrades.concave_puck.fullHelp": " Les balles démarrent verticalement en début de niveau, et rebondi sur le palet de manière plus verticale et inversée.",
"upgrades.concave_puck.help": "Aide à éviter les bords.",
"upgrades.concave_puck.name": "Palet concave",
"upgrades.extra_levels.fullHelp": "La partie dure normalement 7 niveaux, après quoi le jeu est terminé et le score que vous avez atteint est votre score de partie.\n\nChoisir cette amélioration vous permet de prolonger la partie d'un niveau. Les derniers niveaux sont souvent ceux où vous faites le plus de points, la différence peut donc être spectaculaire.",
"upgrades.extra_levels.help": "Jouer {{count}} niveaux au lieu de 7",
"upgrades.extra_levels.name": "+1 niveau",
@ -186,8 +189,8 @@
"upgrades.telekinesis.help_plural": "Effet plus fort sur la balle",
"upgrades.telekinesis.name": "Télékinésie",
"upgrades.top_is_lava.fullHelp": "Chaque fois que vous cassez une brique, votre combo augmente d'une unité. Cependant, votre combo sera réinitialisé dès que votre balle atteindra le haut de l'écran.\n\nLorsque votre combo est supérieur au minimum, une barre rouge apparaît en haut de l'écran pour vous rappeler que vous devez éviter de la frapper.\n\nCet effet s'ajoute aux autres avantages du combo.",
"upgrades.top_is_lava.help": "Plus de pièces si vous ne touchez pas le sommet.",
"upgrades.top_is_lava.name": "Icare",
"upgrades.top_is_lava.help": "Plus de pièces si vous ne touchez pas le haut de la zone de jeu",
"upgrades.top_is_lava.name": "Icare ",
"upgrades.viscosity.fullHelp": "Les pièces accélèrent normalement avec la gravité et les explosions pour atteindre des vitesses assez élevées. \n\nCette compétence les ralentit constamment, comme si elles se trouvaient dans une sorte de liquide visqueux.\n\nCela permet de les attraper plus facilement et se combine bien avec les améliorations qui influencent le mouvement de la pièce.",
"upgrades.viscosity.help": "Chute plus lente des pièces",
"upgrades.viscosity.name": "Fluide visqueux ",

View file

@ -1,32 +1,31 @@
import fr from './fr.json'
import en from './en.json'
import {getSettingValue} from "../settings";
import fr from "./fr.json";
import en from "./en.json";
import { getSettingValue } from "../settings";
type translationKeys = keyof typeof en
type translation= { [key in translationKeys] : string }
const languages:Record<string, translation>= {fr,en}
export function getCurrentLang(){
return getSettingValue('lang',getFirstBrowserLanguage())
type translationKeys = keyof typeof en;
type translation = { [key in translationKeys]: string };
const languages: Record<string, translation> = { fr, en };
export function getCurrentLang() {
return getSettingValue("lang", getFirstBrowserLanguage());
}
export function t(key: translationKeys, params: {[key:string]:any} = {}):string {
const lang = getCurrentLang()
let template=languages[lang]?.[key] || languages.en[key]
for(let key in params){
template=template.split('{{'+key+'}}').join(`${params[key]}`)
}
return template
export function t(
key: translationKeys,
params: { [key: string]: any } = {},
): string {
const lang = getCurrentLang();
let template = languages[lang]?.[key] || languages.en[key];
for (let key in params) {
template = template.split("{{" + key + "}}").join(`${params[key]}`);
}
return template;
}
function getFirstBrowserLanguage() {
const preferred_languages = [
...navigator.languages,
navigator.language,
'en'
].filter(i => i)
.map(i => i.slice(0, 2).toLowerCase())
const supported = Object.keys(languages)
const preferred_languages = [...navigator.languages, navigator.language, "en"]
.filter((i) => i)
.map((i) => i.slice(0, 2).toLowerCase());
const supported = Object.keys(languages);
return preferred_languages.find(k=>supported.includes(k)) || 'en'
};
return preferred_languages.find((k) => supported.includes(k)) || "en";
}

View file

@ -12,15 +12,12 @@
name="description"
content="A breakout game with roguelite mechanics. Break bricks, catch coins, pick upgrades, repeat. Play for free on mobile and desktop."
/>
<link rel="manifest" href="manifest.json" />
<link rel="manifest" href="./PWA/manifest.json" />
<style>
@import "game.less";
</style>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🕹️</text></svg>"
/>
<link rel="icon" href="./PWA/icon.svg" />
</head>
<body>
<button id="menu"><span id="menuLabel">menu</span></button>

46
src/levelIcon.ts Normal file
View file

@ -0,0 +1,46 @@
let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx =
process.env.NODE_ENV !== "test" &&
(levelIconHTMLCanvas.getContext("2d", {
antialias: false,
alpha: true,
}) as CanvasRenderingContext2D);
export function levelIconHTML(
bricks: string[],
levelSize: number,
color: string,
) {
const size = 40;
const c = levelIconHTMLCanvas;
const ctx = levelIconHTMLCanvasCtx;
if (!ctx) return "";
c.width = size;
c.height = size;
if (color) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
} else {
ctx.clearRect(0, 0, size, size);
}
const pxSize = size / levelSize;
for (let x = 0; x < levelSize; x++) {
for (let y = 0; y < levelSize; y++) {
const c = bricks[y * levelSize + x];
if (c) {
ctx.fillStyle = c;
ctx.fillRect(
Math.floor(pxSize * x),
Math.floor(pxSize * y),
Math.ceil(pxSize),
Math.ceil(pxSize),
);
}
}
}
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
}

View file

@ -1,8 +1,8 @@
import { Palette, RawLevel } from "./types";
import _backgrounds from "./backgrounds.json";
import _palette from "./palette.json";
import _allLevels from "./levels.json";
import { getLevelBackground, hashCode } from "./getLevelBackground";
import { Palette, RawLevel } from "../types";
import _backgrounds from "../data/backgrounds.json";
import _palette from "../data/palette.json";
import _allLevels from "../data/levels.json";
import { getLevelBackground, hashCode } from "../getLevelBackground";
import { createRoot } from "react-dom/client";
import { useCallback, useEffect, useState } from "react";
import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util";
@ -34,7 +34,7 @@ function App() {
useEffect(() => {
const timoutId = setTimeout(() => {
return fetch("http://localhost:4400/src/levels.json", {
return fetch("http://localhost:4400/src/data/levels.json", {
method: "POST",
headers: {
"Content-Type": "text/plain",

View file

@ -1,4 +1,4 @@
import { RawLevel } from "./types";
import { RawLevel } from "../types";
export function resizeLevel(level: RawLevel, sizeDelta: number) {
const { size, bricks } = level;

View file

@ -1,6 +1,6 @@
import _palette from "./palette.json";
import _rawLevelsList from "./levels.json";
import _appVersion from "./version.json";
import _palette from "./data/palette.json";
import _rawLevelsList from "./data/levels.json";
import _appVersion from "./data/version.json";
describe("json data checks", () => {
it("_rawLevelsList has icon levels", () => {

View file

@ -1,53 +1,17 @@
import { Level, Palette, RawLevel, Upgrade } from "./types";
import _palette from "./palette.json";
import _rawLevelsList from "./levels.json";
import _appVersion from "./version.json";
import _palette from "./data/palette.json";
import _rawLevelsList from "./data/levels.json";
import _appVersion from "./data/version.json";
import { rawUpgrades } from "./rawUpgrades";
import { getLevelBackground } from "./getLevelBackground";
import { levelIconHTML } from "./levelIcon";
const palette = _palette as Palette;
const rawLevelsList = _rawLevelsList as RawLevel[];
export const appVersion = _appVersion as string;
let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
antialias: false,
alpha: true,
}) as CanvasRenderingContext2D;
function levelIconHTML(bricks: string[], levelSize: number, color: string) {
const size = 40;
const c = levelIconHTMLCanvas;
const ctx = levelIconHTMLCanvasCtx;
c.width = size;
c.height = size;
if (color) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
} else {
ctx.clearRect(0, 0, size, size);
}
const pxSize = size / levelSize;
for (let x = 0; x < levelSize; x++) {
for (let y = 0; y < levelSize; y++) {
const c = bricks[y * levelSize + x];
if (c) {
ctx.fillStyle = c;
ctx.fillRect(
Math.floor(pxSize * x),
Math.floor(pxSize * y),
Math.ceil(pxSize),
Math.ceil(pxSize),
);
}
}
}
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
}
export const icons = {} as { [k: string]: string };
export const allLevels = rawLevelsList

View file

@ -1,104 +1,109 @@
import {GameState, RunParams} from "./types";
import {getTotalScore} from "./settings";
import {allLevels, upgrades} from "./loadGameData";
import {getPossibleUpgrades, makeEmptyPerksMap, sumOfKeys} from "./game_utils";
import {dontOfferTooSoon, resetBalls} from "./gameStateMutators";
import {isOptionOn} from "./options";
import { GameState, RunParams } from "./types";
import { getTotalScore } from "./settings";
import { allLevels, upgrades } from "./loadGameData";
import {
getPossibleUpgrades,
makeEmptyPerksMap,
sumOfKeys,
} from "./game_utils";
import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options";
export function newGameState(params: RunParams): GameState {
const totalScoreAtRunStart = getTotalScore();
const firstLevel = params?.level
? allLevels.filter((l) => l.name === params?.level)
: [];
const totalScoreAtRunStart = getTotalScore();
const firstLevel = params?.level
? allLevels.filter((l) => l.name === params?.level)
: [];
const restInRandomOrder = allLevels
.filter((l) => totalScoreAtRunStart >= l.threshold)
.filter((l) => l.name !== params?.level)
.filter((l) => l.name !== params?.levelToAvoid)
.sort(() => Math.random() - 0.5);
const restInRandomOrder = allLevels
.filter((l) => totalScoreAtRunStart >= l.threshold)
.filter((l) => l.name !== params?.level)
.filter((l) => l.name !== params?.levelToAvoid)
.sort(() => Math.random() - 0.5);
const runLevels = firstLevel.concat(
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
);
const runLevels = firstLevel.concat(
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
);
const perks = {...makeEmptyPerksMap(upgrades), ...(params?.perks || {})};
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
const gameState: GameState = {
runLevels,
currentLevel: 0,
perks,
puckWidth: 200,
baseSpeed: 12,
combo: 1,
gridSize: 12,
running: false,
puckPosition: 400,
pauseTimeout: null,
canvasWidth: 0,
canvasHeight: 0,
offsetX: 0,
offsetXRoundedDown: 0,
gameZoneWidth: 0,
gameZoneWidthRoundedUp: 0,
gameZoneHeight: 0,
brickWidth: 0,
score: 0,
lastScoreIncrease: -1000,
lastExplosion: -1000,
highScore: parseFloat(localStorage.getItem("breakout-3-hs") || "0"),
balls: [],
ballsColor: "white",
bricks: [],
flashes: [],
coins: [],
levelStartScore: 0,
levelMisses: 0,
levelSpawnedCoins: 0,
lastPlayedCoinGrab: 0,
MAX_COINS: 400,
MAX_PARTICLES: 600,
puckColor: "#FFF",
ballSize: 20,
coinSize: 14,
puckHeight: 20,
totalScoreAtRunStart,
isCreativeModeRun: sumOfKeys(perks) > 1,
pauseUsesDuringRun: 0,
keyboardPuckSpeed: 0,
lastTick: performance.now(),
lastTickDown: 0,
runStatistics: {
started: Date.now(),
levelsPlayed: 0,
runTime: 0,
coins_spawned: 0,
score: 0,
bricks_broken: 0,
misses: 0,
balls_lost: 0,
puck_bounces: 0,
upgrades_picked: 1,
max_combo: 1,
max_level: 0,
},
lastOffered: {},
levelTime: 0,
autoCleanUses: 0,
};
resetBalls(gameState);
const gameState: GameState = {
runLevels,
currentLevel: 0,
perks,
puckWidth: 200,
baseSpeed: 12,
combo: 1,
gridSize: 12,
running: false,
puckPosition: 400,
pauseTimeout: null,
canvasWidth: 0,
canvasHeight: 0,
offsetX: 0,
offsetXRoundedDown: 0,
gameZoneWidth: 0,
gameZoneWidthRoundedUp: 0,
gameZoneHeight: 0,
brickWidth: 0,
score: 0,
lastScoreIncrease: -1000,
lastExplosion: -1000,
highScore: parseFloat(localStorage.getItem("breakout-3-hs") || "0"),
balls: [],
ballsColor: "white",
bricks: [],
flashes: [],
coins: [],
levelStartScore: 0,
levelMisses: 0,
levelSpawnedCoins: 0,
lastPlayedCoinGrab: 0,
MAX_COINS: 400,
MAX_PARTICLES: 600,
puckColor: "#FFF",
ballSize: 20,
coinSize: 14,
puckHeight: 20,
totalScoreAtRunStart,
isCreativeModeRun: sumOfKeys(perks) > 1,
pauseUsesDuringRun: 0,
keyboardPuckSpeed: 0,
lastTick: performance.now(),
lastTickDown: 0,
runStatistics: {
started: Date.now(),
levelsPlayed: 0,
runTime: 0,
coins_spawned: 0,
score: 0,
bricks_broken: 0,
misses: 0,
balls_lost: 0,
puck_bounces: 0,
wall_bounces: 0,
upgrades_picked: 1,
max_combo: 1,
max_level: 0,
},
lastOffered: {},
levelTime: 0,
autoCleanUses: 0,
};
resetBalls(gameState);
if (!sumOfKeys(gameState.perks)) {
const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable);
const randomGift =
(isOptionOn("easy") && "slow_down") ||
giftable[Math.floor(Math.random() * giftable.length)].id;
perks[randomGift] = 1;
dontOfferTooSoon(gameState, randomGift);
if (!sumOfKeys(gameState.perks)) {
const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable);
const randomGift =
(isOptionOn("easy") && "slow_down") ||
giftable[Math.floor(Math.random() * giftable.length)].id;
perks[randomGift] = 1;
dontOfferTooSoon(gameState, randomGift);
}
for (let perk of upgrades) {
if (gameState.perks[perk.id]) {
dontOfferTooSoon(gameState, perk.id);
}
for (let perk of upgrades) {
if (gameState.perks[perk.id]) {
dontOfferTooSoon(gameState, perk.id);
}
}
return gameState;
}
}
return gameState;
}

View file

@ -1,54 +1,57 @@
import {t} from "./i18n/i18n";
import { t } from "./i18n/i18n";
import {OptionDef, OptionId} from "./types";
import {getSettingValue, setSettingValue} from "./settings";
import { OptionDef, OptionId } from "./types";
import { getSettingValue, setSettingValue } from "./settings";
export const options = {
sound: {
default: true,
name: t('main_menu.sounds'),
help: t('main_menu.sounds_help'),
disabled: () => false,
},
"mobile-mode": {
default: window.innerHeight > window.innerWidth,
name: t('main_menu.mobile'),
help: t('main_menu.mobile_help'),
disabled: () => false,
},
basic: {
default: false,
name: t('main_menu.basic'),
help: t('main_menu.basic_help'),
disabled: () => false,
},
pointerLock: {
default: false,
name: t('main_menu.pointer_lock'),
help: t('main_menu.pointer_lock_help'),
disabled: () => !document.body.requestPointerLock,
},
easy: {
default: false,
name: t('main_menu.kid'),
help: t('main_menu.kid_help'),
disabled: () => false,
},
// Could not get the sharing to work without loading androidx and all the modern android things so for now I'll just disable sharing in the android app
record: {
default: false,
name: t('main_menu.record'),
help: t('main_menu.record_help'),
disabled() {
return window.location.search.includes("isInWebView=true");
},
sound: {
default: true,
name: t("main_menu.sounds"),
help: t("main_menu.sounds_help"),
disabled: () => false,
},
"mobile-mode": {
default: window.innerHeight > window.innerWidth,
name: t("main_menu.mobile"),
help: t("main_menu.mobile_help"),
disabled: () => false,
},
basic: {
default: false,
name: t("main_menu.basic"),
help: t("main_menu.basic_help"),
disabled: () => false,
},
pointerLock: {
default: false,
name: t("main_menu.pointer_lock"),
help: t("main_menu.pointer_lock_help"),
disabled: () => !document.body.requestPointerLock,
},
easy: {
default: false,
name: t("main_menu.kid"),
help: t("main_menu.kid_help"),
disabled: () => false,
},
// Could not get the sharing to work without loading androidx and all the modern android things so for now I'll just disable sharing in the android app
record: {
default: false,
name: t("main_menu.record"),
help: t("main_menu.record_help"),
disabled() {
return window.location.search.includes("isInWebView=true");
},
},
} as const satisfies { [k: string]: OptionDef };
export function isOptionOn(key: OptionId) {
return getSettingValue("breakout-settings-enable-" + key, options[key]?.default)
return getSettingValue(
"breakout-settings-enable-" + key,
options[key]?.default,
);
}
export function toggleOption(key: OptionId) {
setSettingValue("breakout-settings-enable-" +key, !isOptionOn(key))
}
setSettingValue("breakout-settings-enable-" + key, !isOptionOn(key));
}

View file

@ -1,4 +1,4 @@
import {t} from "./i18n/i18n";
import { t } from "./i18n/i18n";
export const rawUpgrades = [
{
@ -7,9 +7,12 @@ export const rawUpgrades = [
giftable: false,
id: "extra_life",
max: 7,
name: t('upgrades.extra_life.name'),
help: (lvl: number) => lvl === 1 ? t('upgrades.extra_life.help'): t('upgrades.extra_life.help_plural',{lvl}),
fullHelp: t('upgrades.extra_life.fullHelp'),
name: t("upgrades.extra_life.name"),
help: (lvl: number) =>
lvl === 1
? t("upgrades.extra_life.help")
: t("upgrades.extra_life.help_plural", { lvl }),
fullHelp: t("upgrades.extra_life.fullHelp"),
},
{
requires: "",
@ -17,9 +20,9 @@ export const rawUpgrades = [
id: "streak_shots",
giftable: true,
max: 1,
name: t('upgrades.streak_shots.name'),
help: (lvl: number) => t('upgrades.streak_shots.help',{lvl}) ,
fullHelp: t('upgrades.streak_shots.fullHelp'),
name: t("upgrades.streak_shots.name"),
help: (lvl: number) => t("upgrades.streak_shots.help", { lvl }),
fullHelp: t("upgrades.streak_shots.fullHelp"),
},
{
@ -28,9 +31,10 @@ export const rawUpgrades = [
id: "base_combo",
giftable: true,
max: 7,
name: t('upgrades.base_combo.name'),
help: (lvl: number) => t('upgrades.base_combo.help',{coins:1 + lvl * 3}),
fullHelp: t('upgrades.base_combo.fullHelp'),
name: t("upgrades.base_combo.name"),
help: (lvl: number) =>
t("upgrades.base_combo.help", { coins: 1 + lvl * 3 }),
fullHelp: t("upgrades.base_combo.fullHelp"),
},
{
requires: "",
@ -38,9 +42,9 @@ export const rawUpgrades = [
giftable: false,
id: "slow_down",
max: 2,
name: t('upgrades.slow_down.name'),
help: () => t('upgrades.slow_down.help' ),
fullHelp: t('upgrades.slow_down.fullHelp'),
name: t("upgrades.slow_down.name"),
help: () => t("upgrades.slow_down.help"),
fullHelp: t("upgrades.slow_down.fullHelp"),
},
{
requires: "",
@ -48,9 +52,9 @@ export const rawUpgrades = [
giftable: false,
id: "bigger_puck",
max: 2,
name: t('upgrades.bigger_puck.name'),
help: () => t('upgrades.bigger_puck.help' ),
fullHelp: t('upgrades.bigger_puck.fullHelp'),
name: t("upgrades.bigger_puck.name"),
help: () => t("upgrades.bigger_puck.help"),
fullHelp: t("upgrades.bigger_puck.fullHelp"),
},
{
requires: "",
@ -59,9 +63,9 @@ export const rawUpgrades = [
id: "viscosity",
max: 3,
name: t('upgrades.viscosity.name'),
help: () => t('upgrades.viscosity.help' ),
fullHelp: t('upgrades.viscosity.fullHelp'),
name: t("upgrades.viscosity.name"),
help: () => t("upgrades.viscosity.help"),
fullHelp: t("upgrades.viscosity.fullHelp"),
},
{
requires: "",
@ -70,10 +74,9 @@ export const rawUpgrades = [
giftable: true,
max: 1,
name: t('upgrades.left_is_lava.name'),
help: () => t('upgrades.left_is_lava.help' ),
fullHelp: t('upgrades.left_is_lava.fullHelp'),
name: t("upgrades.left_is_lava.name"),
help: () => t("upgrades.left_is_lava.help"),
fullHelp: t("upgrades.left_is_lava.fullHelp"),
},
{
requires: "",
@ -81,10 +84,9 @@ export const rawUpgrades = [
id: "right_is_lava",
giftable: true,
max: 1,
name: t('upgrades.right_is_lava.name'),
help: () => t('upgrades.right_is_lava.help' ),
fullHelp: t('upgrades.right_is_lava.fullHelp'),
name: t("upgrades.right_is_lava.name"),
help: () => t("upgrades.right_is_lava.help"),
fullHelp: t("upgrades.right_is_lava.fullHelp"),
},
{
requires: "",
@ -92,10 +94,9 @@ export const rawUpgrades = [
id: "top_is_lava",
giftable: true,
max: 1,
name: t('upgrades.top_is_lava.name'),
help: () => t('upgrades.top_is_lava.help' ),
fullHelp: t('upgrades.top_is_lava.fullHelp'),
name: t("upgrades.top_is_lava.name"),
help: () => t("upgrades.top_is_lava.help"),
fullHelp: t("upgrades.top_is_lava.fullHelp"),
},
{
requires: "",
@ -103,10 +104,12 @@ export const rawUpgrades = [
giftable: false,
id: "skip_last",
max: 7,
name: t('upgrades.skip_last.name'),
help: (lvl: number) => lvl==1 ? t('upgrades.skip_last.help' ) : t('upgrades.skip_last.help_plural', {lvl} ),
fullHelp: t('upgrades.skip_last.fullHelp'),
name: t("upgrades.skip_last.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.skip_last.help")
: t("upgrades.skip_last.help_plural", { lvl }),
fullHelp: t("upgrades.skip_last.fullHelp"),
},
{
requires: "",
@ -114,9 +117,12 @@ export const rawUpgrades = [
id: "telekinesis",
giftable: true,
max: 2,
name: t('upgrades.telekinesis.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.telekinesis.help'): t('upgrades.telekinesis.help_plural'),
fullHelp: t('upgrades.telekinesis.fullHelp'),
name: t("upgrades.telekinesis.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.telekinesis.help")
: t("upgrades.telekinesis.help_plural"),
fullHelp: t("upgrades.telekinesis.fullHelp"),
},
{
requires: "",
@ -124,9 +130,12 @@ export const rawUpgrades = [
giftable: false,
id: "coin_magnet",
max: 3,
name: t('upgrades.coin_magnet.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.coin_magnet.help'): t('upgrades.coin_magnet.help_plural'),
fullHelp: t('upgrades.coin_magnet.fullHelp'),
name: t("upgrades.coin_magnet.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.coin_magnet.help")
: t("upgrades.coin_magnet.help_plural"),
fullHelp: t("upgrades.coin_magnet.fullHelp"),
},
{
requires: "",
@ -134,11 +143,9 @@ export const rawUpgrades = [
id: "multiball",
giftable: true,
max: 6,
name: t('upgrades.multiball.name'),
help: (lvl: number) => t('upgrades.multiball.help',{count:lvl+1}) ,
fullHelp: t('upgrades.multiball.fullHelp'),
name: t("upgrades.multiball.name"),
help: (lvl: number) => t("upgrades.multiball.help", { count: lvl + 1 }),
fullHelp: t("upgrades.multiball.fullHelp"),
},
{
requires: "",
@ -146,11 +153,12 @@ export const rawUpgrades = [
giftable: false,
id: "smaller_puck",
max: 2,
name: t('upgrades.smaller_puck.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.smaller_puck.help'): t('upgrades.smaller_puck.help_plural'),
fullHelp: t('upgrades.smaller_puck.fullHelp'),
name: t("upgrades.smaller_puck.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.smaller_puck.help")
: t("upgrades.smaller_puck.help_plural"),
fullHelp: t("upgrades.smaller_puck.fullHelp"),
},
{
requires: "",
@ -158,9 +166,9 @@ export const rawUpgrades = [
id: "pierce",
giftable: true,
max: 3,
name: t('upgrades.pierce.name'),
help: (lvl: number) => t('upgrades.pierce.help',{count:3 * lvl}) ,
fullHelp: t('upgrades.pierce.fullHelp'),
name: t("upgrades.pierce.name"),
help: (lvl: number) => t("upgrades.pierce.help", { count: 3 * lvl }),
fullHelp: t("upgrades.pierce.fullHelp"),
},
{
requires: "",
@ -168,10 +176,9 @@ export const rawUpgrades = [
id: "picky_eater",
giftable: true,
max: 1,
name: t('upgrades.picky_eater.name'),
help: (lvl: number) => t('upgrades.picky_eater.help') ,
fullHelp: t('upgrades.picky_eater.fullHelp'),
name: t("upgrades.picky_eater.name"),
help: (lvl: number) => t("upgrades.picky_eater.help"),
fullHelp: t("upgrades.picky_eater.fullHelp"),
},
{
requires: "",
@ -179,11 +186,9 @@ export const rawUpgrades = [
giftable: false,
id: "metamorphosis",
max: 1,
name: t('upgrades.metamorphosis.name'),
help: (lvl: number) => t('upgrades.metamorphosis.help'),
fullHelp: t('upgrades.metamorphosis.fullHelp'),
name: t("upgrades.metamorphosis.name"),
help: (lvl: number) => t("upgrades.metamorphosis.help"),
fullHelp: t("upgrades.metamorphosis.fullHelp"),
},
{
requires: "",
@ -191,9 +196,9 @@ export const rawUpgrades = [
id: "compound_interest",
giftable: true,
max: 1,
name: t('upgrades.compound_interest.name'),
help: (lvl: number) => t('upgrades.compound_interest.help') ,
fullHelp: t('upgrades.compound_interest.fullHelp'),
name: t("upgrades.compound_interest.name"),
help: (lvl: number) => t("upgrades.compound_interest.help"),
fullHelp: t("upgrades.compound_interest.fullHelp"),
},
{
requires: "",
@ -201,13 +206,13 @@ export const rawUpgrades = [
id: "hot_start",
giftable: true,
max: 3,
name: t('upgrades.hot_start.name'),
help: (lvl: number) => t('upgrades.hot_start.help',{
start:lvl * 15 + 1,
lvl
}),
fullHelp: t('upgrades.hot_start.fullHelp'),
name: t("upgrades.hot_start.name"),
help: (lvl: number) =>
t("upgrades.hot_start.help", {
start: lvl * 15 + 1,
lvl,
}),
fullHelp: t("upgrades.hot_start.fullHelp"),
},
{
requires: "",
@ -215,9 +220,12 @@ export const rawUpgrades = [
id: "sapper",
giftable: true,
max: 7,
name: t('upgrades.sapper.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.sapper.help'): t('upgrades.sapper.help_plural',{lvl}),
fullHelp: t('upgrades.sapper.fullHelp'),
name: t("upgrades.sapper.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.sapper.help")
: t("upgrades.sapper.help_plural", { lvl }),
fullHelp: t("upgrades.sapper.fullHelp"),
},
{
requires: "",
@ -225,9 +233,9 @@ export const rawUpgrades = [
id: "bigger_explosions",
giftable: false,
max: 1,
name: t('upgrades.bigger_explosions.name'),
help: (lvl: number) => t('upgrades.bigger_explosions.help'),
fullHelp: t('upgrades.bigger_explosions.fullHelp'),
name: t("upgrades.bigger_explosions.name"),
help: (lvl: number) => t("upgrades.bigger_explosions.help"),
fullHelp: t("upgrades.bigger_explosions.fullHelp"),
},
{
requires: "",
@ -235,9 +243,9 @@ export const rawUpgrades = [
giftable: false,
id: "extra_levels",
max: 3,
name: t('upgrades.extra_levels.name'),
help: (lvl: number) => t('upgrades.extra_levels.help',{count:lvl + 7}) ,
fullHelp: t('upgrades.extra_levels.fullHelp'),
name: t("upgrades.extra_levels.name"),
help: (lvl: number) => t("upgrades.extra_levels.help", { count: lvl + 7 }),
fullHelp: t("upgrades.extra_levels.fullHelp"),
},
{
requires: "",
@ -245,10 +253,9 @@ export const rawUpgrades = [
giftable: false,
id: "pierce_color",
max: 1,
name: t('upgrades.pierce_color.name'),
help: (lvl: number) => t('upgrades.pierce_color.help') ,
fullHelp: t('upgrades.pierce_color.fullHelp'),
name: t("upgrades.pierce_color.name"),
help: (lvl: number) => t("upgrades.pierce_color.help"),
fullHelp: t("upgrades.pierce_color.fullHelp"),
},
{
requires: "",
@ -256,11 +263,9 @@ export const rawUpgrades = [
giftable: false,
id: "soft_reset",
max: 9,
name: t('upgrades.soft_reset.name'),
help: (lvl: number) => t('upgrades.soft_reset.help',{percent:10*lvl}),
fullHelp: t('upgrades.soft_reset.fullHelp'),
name: t("upgrades.soft_reset.name"),
help: (lvl: number) => t("upgrades.soft_reset.help", { percent: 10 * lvl }),
fullHelp: t("upgrades.soft_reset.fullHelp"),
},
{
requires: "multiball",
@ -268,11 +273,12 @@ export const rawUpgrades = [
giftable: false,
id: "ball_repulse_ball",
max: 3,
name: t('upgrades.ball_repulse_ball.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.ball_repulse_ball.help'): t('upgrades.ball_repulse_ball.help_plural'),
fullHelp: t('upgrades.ball_repulse_ball.fullHelp'),
name: t("upgrades.ball_repulse_ball.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.ball_repulse_ball.help")
: t("upgrades.ball_repulse_ball.help_plural"),
fullHelp: t("upgrades.ball_repulse_ball.fullHelp"),
},
{
requires: "multiball",
@ -280,9 +286,12 @@ export const rawUpgrades = [
giftable: false,
id: "ball_attract_ball",
max: 3,
name: t('upgrades.ball_attract_ball.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.ball_attract_ball.help'): t('upgrades.ball_attract_ball.help_plural'),
fullHelp: t('upgrades.ball_attract_ball.fullHelp'),
name: t("upgrades.ball_attract_ball.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.ball_attract_ball.help")
: t("upgrades.ball_attract_ball.help_plural"),
fullHelp: t("upgrades.ball_attract_ball.fullHelp"),
},
{
requires: "",
@ -290,10 +299,12 @@ export const rawUpgrades = [
giftable: false,
id: "puck_repulse_ball",
max: 2,
name: t('upgrades.puck_repulse_ball.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.puck_repulse_ball.help'): t('upgrades.puck_repulse_ball.help_plural'),
fullHelp: t('upgrades.puck_repulse_ball.fullHelp'),
name: t("upgrades.puck_repulse_ball.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.puck_repulse_ball.help")
: t("upgrades.puck_repulse_ball.help_plural"),
fullHelp: t("upgrades.puck_repulse_ball.fullHelp"),
},
{
requires: "",
@ -301,11 +312,10 @@ export const rawUpgrades = [
giftable: false,
id: "wind",
max: 3,
name: t('upgrades.wind.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.wind.help'): t('upgrades.wind.help_plural'),
fullHelp: t('upgrades.wind.fullHelp'),
name: t("upgrades.wind.name"),
help: (lvl: number) =>
lvl == 1 ? t("upgrades.wind.help") : t("upgrades.wind.help_plural"),
fullHelp: t("upgrades.wind.fullHelp"),
},
{
requires: "",
@ -313,10 +323,12 @@ export const rawUpgrades = [
giftable: false,
id: "sturdy_bricks",
max: 4,
name: t('upgrades.telekinesis.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.telekinesis.help'): t('upgrades.telekinesis.help_plural'),
fullHelp: t('upgrades.telekinesis.fullHelp'),
name: t("upgrades.telekinesis.name"),
help: (lvl: number) =>
lvl == 1
? t("upgrades.telekinesis.help")
: t("upgrades.telekinesis.help_plural"),
fullHelp: t("upgrades.telekinesis.fullHelp"),
},
{
requires: "",
@ -324,11 +336,10 @@ export const rawUpgrades = [
giftable: false,
id: "respawn",
max: 4,
name: t('upgrades.respawn.name'),
help: (lvl: number) => lvl == 1 ? t('upgrades.respawn.help'): t('upgrades.respawn.help_plural'),
fullHelp: t('upgrades.respawn.fullHelp'),
name: t("upgrades.respawn.name"),
help: (lvl: number) =>
lvl == 1 ? t("upgrades.respawn.help") : t("upgrades.respawn.help_plural"),
fullHelp: t("upgrades.respawn.fullHelp"),
},
{
requires: "",
@ -336,10 +347,9 @@ export const rawUpgrades = [
giftable: false,
id: "one_more_choice",
max: 3,
name: t('upgrades.one_more_choice.name'),
help: (lvl: number) => t('upgrades.one_more_choice.help'),
fullHelp: t('upgrades.one_more_choice.fullHelp'),
name: t("upgrades.one_more_choice.name"),
help: (lvl: number) => t("upgrades.one_more_choice.help"),
fullHelp: t("upgrades.one_more_choice.fullHelp"),
},
{
requires: "",
@ -347,9 +357,18 @@ export const rawUpgrades = [
giftable: false,
id: "instant_upgrade",
max: 2,
name: t('upgrades.instant_upgrade.name'),
help: (lvl: number) => t('upgrades.instant_upgrade.help') ,
fullHelp: t('upgrades.instant_upgrade.fullHelp'),
name: t("upgrades.instant_upgrade.name"),
help: (lvl: number) => t("upgrades.instant_upgrade.help"),
fullHelp: t("upgrades.instant_upgrade.fullHelp"),
},
{
requires: "",
threshold: 60000,
giftable: false,
id: "concave_puck",
max: 1,
name: t("upgrades.concave_puck.name"),
help: (lvl: number) => t("upgrades.concave_puck.help"),
fullHelp: t("upgrades.concave_puck.fullHelp"),
},
] as const;

View file

@ -1,168 +1,168 @@
import {gameCanvas} from "./render";
import {max_levels} from "./game_utils";
import {getAudioRecordingTrack} from "./sounds";
import {t} from "./i18n/i18n";
import {GameState} from "./types";
import {isOptionOn} from "./options";
import { gameCanvas } from "./render";
import { max_levels } from "./game_utils";
import { getAudioRecordingTrack } from "./sounds";
import { t } from "./i18n/i18n";
import { GameState } from "./types";
import { isOptionOn } from "./options";
let mediaRecorder: MediaRecorder | null,
captureStream: MediaStream,
captureTrack: CanvasCaptureMediaStreamTrack,
recordCanvas: HTMLCanvasElement,
recordCanvasCtx: CanvasRenderingContext2D;
captureStream: MediaStream,
captureTrack: CanvasCaptureMediaStreamTrack,
recordCanvas: HTMLCanvasElement,
recordCanvasCtx: CanvasRenderingContext2D;
export function recordOneFrame(gameState:GameState) {
if (!isOptionOn("record")) {
return;
}
if (!gameState.running) return;
if (!captureStream) return;
drawMainCanvasOnSmallCanvas(gameState);
if (captureTrack?.requestFrame) {
captureTrack?.requestFrame();
} else if (captureStream?.requestFrame) {
captureStream.requestFrame();
}
export function recordOneFrame(gameState: GameState) {
if (!isOptionOn("record")) {
return;
}
if (!gameState.running) return;
if (!captureStream) return;
drawMainCanvasOnSmallCanvas(gameState);
if (captureTrack?.requestFrame) {
captureTrack?.requestFrame();
} else if (captureStream?.requestFrame) {
captureStream.requestFrame();
}
}
export function drawMainCanvasOnSmallCanvas(gameState:GameState) {
if (!recordCanvasCtx) return;
recordCanvasCtx.drawImage(
gameCanvas,
gameState.offsetXRoundedDown,
0,
gameState.gameZoneWidthRoundedUp,
gameState.gameZoneHeight,
0,
0,
recordCanvas.width,
recordCanvas.height,
);
export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
if (!recordCanvasCtx) return;
recordCanvasCtx.drawImage(
gameCanvas,
gameState.offsetXRoundedDown,
0,
gameState.gameZoneWidthRoundedUp,
gameState.gameZoneHeight,
0,
0,
recordCanvas.width,
recordCanvas.height,
);
// 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.textBaseline = "top";
recordCanvasCtx.font = "12px monospace";
recordCanvasCtx.textAlign = "right";
recordCanvasCtx.fillText(
gameState.score.toString(),
recordCanvas.width - 12,
12,
);
// 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.textBaseline = "top";
recordCanvasCtx.font = "12px monospace";
recordCanvasCtx.textAlign = "right";
recordCanvasCtx.fillText(
gameState.score.toString(),
recordCanvas.width - 12,
12,
);
recordCanvasCtx.textAlign = "left";
recordCanvasCtx.fillText(
"Level " + (gameState.currentLevel + 1) + "/" + max_levels(gameState),
12,
12,
);
recordCanvasCtx.textAlign = "left";
recordCanvasCtx.fillText(
"Level " + (gameState.currentLevel + 1) + "/" + max_levels(gameState),
12,
12,
);
}
export function startRecordingGame(gameState:GameState) {
if (!isOptionOn("record")) {
return;
export function startRecordingGame(gameState: GameState) {
if (!isOptionOn("record")) {
return;
}
if (mediaRecorder) return;
if (!recordCanvas) {
// Smaller canvas with fewer details
recordCanvas = document.createElement("canvas");
recordCanvasCtx = recordCanvas.getContext("2d", {
antialias: false,
alpha: false,
}) as CanvasRenderingContext2D;
captureStream = recordCanvas.captureStream(0);
captureTrack =
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
const track = getAudioRecordingTrack();
if (track) {
captureStream.addTrack(track.stream.getAudioTracks()[0]);
}
if (mediaRecorder) return;
if (!recordCanvas) {
// Smaller canvas with fewer details
recordCanvas = document.createElement("canvas");
recordCanvasCtx = recordCanvas.getContext("2d", {
antialias: false,
alpha: false,
}) as CanvasRenderingContext2D;
}
captureStream = recordCanvas.captureStream(0);
captureTrack =
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
recordCanvas.width = gameState.gameZoneWidthRoundedUp;
recordCanvas.height = gameState.gameZoneHeight;
const track = getAudioRecordingTrack();
if (track) {
captureStream.addTrack(track.stream.getAudioTracks()[0]);
}
// drawMainCanvasOnSmallCanvas()
const recordedChunks: Blob[] = [];
const instance = new MediaRecorder(captureStream, {
videoBitsPerSecond: 3500000,
});
mediaRecorder = instance;
instance.start();
mediaRecorder.pause();
instance.ondataavailable = function (event) {
recordedChunks.push(event.data);
};
instance.onstop = async function () {
let targetDiv: HTMLElement | null;
let blob = new Blob(recordedChunks, { type: "video/webm" });
if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short
while (
!(targetDiv = document.getElementById("level-recording-container"))
) {
await new Promise((r) => setTimeout(r, 200));
}
const video = document.createElement("video");
video.autoplay = true;
video.controls = false;
video.disablePictureInPicture = true;
video.disableRemotePlayback = true;
video.width = recordCanvas.width;
video.height = recordCanvas.height;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.src = URL.createObjectURL(blob);
recordCanvas.width = gameState.gameZoneWidthRoundedUp;
recordCanvas.height = gameState.gameZoneHeight;
// drawMainCanvasOnSmallCanvas()
const recordedChunks: Blob[] = [];
const instance = new MediaRecorder(captureStream, {
videoBitsPerSecond: 3500000,
const a = document.createElement("a");
a.download = captureFileName("webm");
a.target = "_blank";
a.href = video.src;
a.textContent = t("main_menu.record_download", {
size: (blob.size / 1000000).toFixed(2),
});
mediaRecorder = instance;
instance.start();
mediaRecorder.pause();
instance.ondataavailable = function (event) {
recordedChunks.push(event.data);
};
instance.onstop = async function () {
let targetDiv: HTMLElement | null;
let blob = new Blob(recordedChunks, {type: "video/webm"});
if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short
while (
!(targetDiv = document.getElementById("level-recording-container"))
) {
await new Promise((r) => setTimeout(r, 200));
}
const video = document.createElement("video");
video.autoplay = true;
video.controls = false;
video.disablePictureInPicture = true;
video.disableRemotePlayback = true;
video.width = recordCanvas.width;
video.height = recordCanvas.height;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.src = URL.createObjectURL(blob);
const a = document.createElement("a");
a.download = captureFileName("webm");
a.target = "_blank";
a.href = video.src;
a.textContent = t('main_menu.record_download', {
size: (blob.size / 1000000).toFixed(2)
});
targetDiv.appendChild(video);
targetDiv.appendChild(a);
};
targetDiv.appendChild(video);
targetDiv.appendChild(a);
};
}
export function pauseRecording( ) {
if (!isOptionOn("record")) {
return;
}
if (mediaRecorder?.state === "recording") {
mediaRecorder?.pause();
}
export function pauseRecording() {
if (!isOptionOn("record")) {
return;
}
if (mediaRecorder?.state === "recording") {
mediaRecorder?.pause();
}
}
export function resumeRecording() {
if (!isOptionOn("record")) {
return;
}
if (mediaRecorder?.state === "paused") {
mediaRecorder.resume();
}
if (!isOptionOn("record")) {
return;
}
if (mediaRecorder?.state === "paused") {
mediaRecorder.resume();
}
}
export function stopRecording() {
if (!isOptionOn("record")) {
return;
}
if (!mediaRecorder) return;
mediaRecorder?.stop();
mediaRecorder = null;
if (!isOptionOn("record")) {
return;
}
if (!mediaRecorder) return;
mediaRecorder?.stop();
mediaRecorder = null;
}
export function captureFileName(ext = "webm") {
return (
"breakout-71-capture-" +
new Date().toISOString().replace(/[^0-9\-]+/gi, "-") +
"." +
ext
);
}
return (
"breakout-71-capture-" +
new Date().toISOString().replace(/[^0-9\-]+/gi, "-") +
"." +
ext
);
}

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,35 @@
// Settings
import {GameState} from "./types";
import { GameState } from "./types";
let cachedSettings: { [key: string]: unknown } = {};
export function getSettingValue<T>(key: string, defaultValue: T) {
if (typeof cachedSettings[key] == "undefined") {
try {
const ls = localStorage.getItem( key);
if (ls) cachedSettings[key] = JSON.parse(ls) as T;
} catch (e) {
console.warn(e);
}
if (typeof cachedSettings[key] == "undefined") {
try {
const ls = localStorage.getItem(key);
if (ls) cachedSettings[key] = JSON.parse(ls) as T;
} catch (e) {
console.warn(e);
}
return cachedSettings[key] as T ?? defaultValue;
}
return (cachedSettings[key] as T) ?? defaultValue;
}
export function setSettingValue<T>(key: string, value: T) {
cachedSettings[key] = value
try {
localStorage.setItem( key, JSON.stringify(value));
} catch (e) {
console.warn(e);
}
cachedSettings[key] = value;
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.warn(e);
}
}
export function getTotalScore() {
return getSettingValue('breakout_71_total_score', 0)
return getSettingValue("breakout_71_total_score", 0);
}
export function addToTotalScore(gameState: GameState, points: number) {
if (gameState.isCreativeModeRun) return;
setSettingValue('breakout_71_total_score', getTotalScore() + points)
if (gameState.isCreativeModeRun) return;
setSettingValue("breakout_71_total_score", getTotalScore() + points);
}

View file

@ -1,7 +1,6 @@
import { gameState } from "./game";
import {isOptionOn} from "./options";
import { isOptionOn } from "./options";
export const sounds = {
wallBeep: (pan: number) => {

9
src/types.d.ts vendored
View file

@ -1,5 +1,5 @@
import {rawUpgrades} from "./rawUpgrades";
import {options} from "./options";
import { rawUpgrades } from "./rawUpgrades";
import { options } from "./options";
export type colorString = string;
@ -98,7 +98,6 @@ export type Ball = {
piercedSinceBounce: number;
hitSinceBounce: number;
hitItem: { index: number; color: string }[];
bouncesList: { x: number; y: number }[];
sapperUses: number;
destroyed?: boolean;
};
@ -141,6 +140,7 @@ export type RunStats = {
misses: number;
balls_lost: number;
puck_bounces: number;
wall_bounces: number;
upgrades_picked: number;
max_combo: number;
max_level: number;
@ -233,6 +233,7 @@ export type GameState = {
runStatistics: RunStats;
lastOffered: Partial<{ [k in PerkId]: number }>;
levelTime: number;
levelWallBounces: number;
autoCleanUses: number;
};
@ -247,4 +248,4 @@ export type OptionDef = {
help: string;
disabled: () => boolean;
};
export type OptionId = keyof typeof options;
export type OptionId = keyof typeof options;

View file

@ -1 +0,0 @@
"29033878"