mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-20 12:15:06 -04:00
251 lines
8.5 KiB
TypeScript
251 lines
8.5 KiB
TypeScript
![]() |
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 }[];
|
||
|
|
||
|
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'),
|
||
|
});
|
||
|
});
|
||
|
|
||
|
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) {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
|
||
|
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);
|
||
|
|
||
|
if (nextUnlock) {
|
||
|
const total = nextUnlock?.threshold - previousUnlockAt;
|
||
|
const done = endTs - previousUnlockAt;
|
||
|
|
||
|
intro += t('gameOver.next_unlock', {points: nextUnlock.threshold - endTs});
|
||
|
|
||
|
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 += `
|
||
|
<p class="progress" >
|
||
|
<span>${u.title}</span>
|
||
|
</p>
|
||
|
`;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
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>`;
|
||
|
}
|
||
|
|
||
|
// 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> ` : ""}
|
||
|
<p>${intro}</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>
|
||
|
${getHistograms()}
|
||
|
`,
|
||
|
}).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);
|
||
|
|
||
|
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),
|
||
|
);
|
||
|
|
||
|
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}"
|
||
|
><span>${(!v && " ") || (vi == activeBin && lastValue + unit) || Math.round(binsTotal[vi] / v) + unit}</span></span></span>`;
|
||
|
})
|
||
|
.join("");
|
||
|
|
||
|
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, "");
|
||
|
|
||
|
if (runStats) {
|
||
|
runStats =
|
||
|
`<p>${t('gameOver.stats.intro', {count: runsHistory.length - 1})}</p>` +
|
||
|
runStats;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
console.warn(e);
|
||
|
}
|
||
|
return runStats;
|
||
|
}
|