breakout71/src/gameOver.ts

295 lines
7.8 KiB
TypeScript
Raw Normal View History

2025-04-06 15:38:30 +02:00
import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
2025-03-16 17:45:29 +01:00
import { t } from "./i18n/i18n";
2025-04-01 13:35:33 +02:00
import { GameState, RunHistoryItem } from "./types";
2025-03-16 17:45:29 +01:00
import { gameState, pause, restart } from "./game";
2025-04-06 15:38:30 +02:00
import {
currentLevelInfo,
describeLevel,
findLast,
pickedUpgradesHTMl,
reasonLevelIsLocked,
} from "./game_utils";
2025-03-16 17:45:29 +01:00
import { getTotalScore } from "./settings";
import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert";
2025-04-06 15:38:30 +02:00
import { rawUpgrades } from "./upgrades";
export function addToTotalPlayTime(ms: number) {
2025-03-16 17:45:29 +01:00
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) {
2025-03-16 17:45:29 +01:00
if (!gameState.running) return;
2025-03-22 16:04:25 +01:00
if (gameState.isGameOver) return;
gameState.isGameOver = true;
2025-03-16 17:45:29 +01:00
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
2025-04-06 15:38:30 +02:00
2025-03-16 17:45:29 +01:00
const endTs = getTotalScore();
const startTs = endTs - gameState.score;
2025-04-06 15:38:30 +02:00
const unlockedPerks = rawUpgrades.filter(
(o) => o.threshold > startTs && o.threshold < endTs,
);
2025-04-06 15:38:30 +02:00
let unlocksInfo = unlockedPerks.length
? `
2025-04-06 15:38:30 +02:00
<h2>${unlockedPerks.length === 1 ? t("gameOver.unlocked_perk") : t("gameOver.unlocked_perk_plural", { count: unlockedPerks.length })}</h2>
${unlockedPerks
.map(
(u) => `
<div class="upgrade used">
${icons["icon:" + u.id]}
<p>
<strong>${u.name}</strong>
${u.help(1)}
</p>
</div>
`,
)
.join("\n")}
`
: "";
2025-03-16 17:45:29 +01:00
// Avoid the sad sound right as we restart a new games
gameState.combo = 1;
2025-03-16 17:45:29 +01:00
asyncAlert({
allowClose: true,
title,
2025-03-27 10:52:31 +01:00
content: [
2025-04-06 15:38:30 +02:00
getCreativeModeWarning(gameState),
2025-03-27 10:52:31 +01:00
`
<p>${intro}</p>
2025-04-06 10:13:10 +02:00
<p>${t("gameOver.cumulative_total", { startTs, endTs })}</p>
`,
2025-03-16 17:45:29 +01:00
{
2025-04-06 15:38:30 +02:00
icon: icons["icon:new_run"],
2025-03-16 17:45:29 +01:00
value: null,
text: t("gameOver.restart"),
help: "",
},
2025-04-06 15:38:30 +02:00
`<div id="level-recording-container"></div>`,
unlocksInfo,
getHistograms(gameState),
2025-03-27 10:52:31 +01:00
],
}).then(() =>
restart({
2025-04-06 15:38:30 +02:00
levelToAvoid: currentLevelInfo(gameState).name,
2025-03-27 10:52:31 +01:00
}),
);
}
2025-04-06 15:38:30 +02:00
export function getCreativeModeWarning(gameState: GameState) {
if (gameState.creative) {
return "<p>" + t("gameOver.creative") + "</p>";
2025-04-06 10:13:10 +02:00
}
2025-04-06 15:38:30 +02:00
return "";
}
let runsHistory = [];
try {
runsHistory = JSON.parse(
localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[];
} catch (e) {}
export function getHistory() {
return runsHistory;
2025-04-06 10:13:10 +02:00
}
2025-04-06 15:38:30 +02:00
2025-04-01 13:35:33 +02:00
export function getHistograms(gameState: GameState) {
2025-04-06 15:38:30 +02:00
if (gameState.creative) return "";
let unlockedLevels = "";
2025-03-16 17:45:29 +01:00
let runStats = "";
try {
2025-04-06 15:38:30 +02:00
const locked = allLevels
.map((l, li) => ({
li,
l,
r: reasonLevelIsLocked(li, runsHistory),
}))
.filter((l) => l.r);
2025-03-16 17:45:29 +01:00
2025-04-06 18:21:53 +02:00
gameState.runStatistics.runTime=Math.round(gameState.runStatistics.runTime)
const perks={...gameState.perks}
for(let id in perks){
if(!perks[id]){
delete perks[id]
}
}
2025-03-16 17:45:29 +01:00
runsHistory.push({
...gameState.runStatistics,
2025-04-06 18:21:53 +02:00
perks,
2025-03-16 17:45:29 +01:00
appVersion,
});
2025-04-06 15:38:30 +02:00
const unlocked = locked.filter(
({ li }) => !reasonLevelIsLocked(li, runsHistory),
);
if (unlocked.length) {
unlockedLevels = `
<h2>${unlocked.length === 1 ? t("unlocks.just_unlocked") : t("unlocks.just_unlocked_plural", { count: unlocked.length })}</h2>
${unlocked
.map(
({ l, r }) => `
<div class="upgrade used">
${icons[l.name]}
<p>
<strong>${l.name}</strong>
${describeLevel(l)}
</p>
</div>
`,
)
.join("\n")}
`;
}
2025-03-16 17:45:29 +01:00
// Generate some histogram
2025-04-01 13:49:10 +02:00
localStorage.setItem(
"breakout_71_runs_history",
JSON.stringify(runsHistory, null, 2),
);
2025-03-16 17:45:29 +01:00
const makeHistogram = (
title: string,
getter: (hi: RunHistoryItem) => number,
unit: string,
) => {
2025-04-06 15:38:30 +02:00
let values = runsHistory.map((h) => getter(h) || 0);
2025-03-16 17:45:29 +01:00
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}"
><span>${(!v && " ") || (vi == activeBin && lastValue + unit) || Math.round(binsTotal[vi] / v) + unit}</span></span></span>`;
2025-03-16 17:45:29 +01:00
})
.join("");
2025-03-16 17:45:29 +01:00
return `<h2 class="histogram-title">${title} : <strong>${lastValue}${unit}</strong></h2>
<div class="histogram">${bars}</div>
`;
2025-03-16 17:45:29 +01:00
};
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,
"",
);
2025-03-28 19:40:59 +01:00
runStats += makeHistogram(t("gameOver.stats.loops"), (r) => r.loops, "");
2025-03-16 17:45:29 +01:00
if (runStats) {
runStats =
`<p>${t("gameOver.stats.intro", { count: runsHistory.length - 1 })}</p>` +
runStats;
}
2025-03-16 17:45:29 +01:00
} catch (e) {
console.warn(e);
}
2025-04-06 15:38:30 +02:00
return runStats + unlockedLevels;
2025-03-16 17:45:29 +01:00
}