Build 29035725
Before Width: | Height: | Size: 715 B After Width: | Height: | Size: 715 B |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
8
src/PWA/icon.svg
Normal 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 |
|
@ -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}`;
|
|
@ -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--;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
"29035725"
|
|
@ -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>
|
1340
src/game.ts
423
src/gameOver.ts
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
sample,
|
||||
sumOfKeys,
|
||||
} from "./game_utils";
|
||||
import { Upgrade } from "./types";
|
||||
|
||||
describe("getMajorityValue", () => {
|
||||
it("returns the most common string", () => {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 ",
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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()}"/>`;
|
||||
}
|
|
@ -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",
|
|
@ -1,4 +1,4 @@
|
|||
import { RawLevel } from "./types";
|
||||
import { RawLevel } from "../types";
|
||||
|
||||
export function resizeLevel(level: RawLevel, sizeDelta: number) {
|
||||
const { size, bricks } = level;
|
|
@ -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", () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
284
src/recording.ts
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
1270
src/render.ts
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
"29033878"
|