import { allLevels, appVersion, icons, upgrades } from "./loadGameData"; import { t } from "./i18n/i18n"; import { GameState, RunHistoryItem } from "./types"; import { gameState, pause, restart } from "./game"; import { currentLevelInfo, describeLevel, findLast, pickedUpgradesHTMl, reasonLevelIsLocked, } from "./game_utils"; import { getSettingValue, getTotalScore, setSettingValue } from "./settings"; import { stopRecording } from "./recording"; import { asyncAlert } from "./asyncAlert"; import { rawUpgrades } from "./upgrades"; import { run } from "jest"; import { editRawLevelList } from "./levelEditor"; import { openCreativeModePerksPicker } from "./creative"; export function addToTotalPlayTime(ms: number) { setSettingValue( "breakout_71_total_play_time", getSettingValue("breakout_71_total_play_time", 0) + ms, ); } export function gameOver(title: string, intro: string) { if (!gameState.running) return; if (gameState.isGameOver) return; gameState.isGameOver = true; pause(true); stopRecording(); addToTotalPlayTime(gameState.runStatistics.runTime); if (typeof gameState.startParams.isEditorTrialRun === "number") { editRawLevelList(gameState.startParams.isEditorTrialRun); restart({}); return; } if (typeof gameState.startParams.isCreativeRun) { openCreativeModePerksPicker(); restart({}); return; } // unlocks const endTs = getTotalScore(); const startTs = endTs - gameState.score; const unlockedPerks = rawUpgrades.filter( (o) => o.threshold > startTs && o.threshold < endTs, ); let unlocksInfo = unlockedPerks.length ? `

${unlockedPerks.length === 1 ? t("gameOver.unlocked_perk") : t("gameOver.unlocked_perk_plural", { count: unlockedPerks.length })}

${unlockedPerks .map( (u) => `
${icons["icon:" + u.id]}

${u.name} ${u.help(1)}

`, ) .join("\n")} ` : ""; // Avoid the sad sound right as we restart a new games gameState.combo = 1; asyncAlert({ allowClose: true, title, content: [ getCreativeModeWarning(gameState) || `

${intro}

${t("gameOver.cumulative_total", { startTs, endTs })}

`, { icon: icons["icon:new_run"], value: null, text: t("confirmRestart.yes"), help: "", }, `
`, unlocksInfo, getHistograms(gameState), ], }).then(() => restart({ levelToAvoid: currentLevelInfo(gameState).name, }), ); } export function getCreativeModeWarning(gameState: GameState) { if (gameState.creative) { return "

" + t("gameOver.creative") + "

"; } return ""; } let runsHistory = []; try { runsHistory = JSON.parse( localStorage.getItem("breakout_71_runs_history") || "[]", ) .sort((a, b) => b.score - a.score) .slice(0, 100) as RunHistoryItem[]; } catch (e) {} export function getHistory() { return runsHistory; } export function getHistograms(gameState: GameState) { if (gameState.creative) return ""; let unlockedLevels = ""; let runStats = ""; try { const locked = allLevels .map((l, li) => ({ li, l, r: reasonLevelIsLocked(li, runsHistory, false)?.text, })) .filter((l) => l.r); gameState.runStatistics.runTime = Math.round( gameState.runStatistics.runTime, ); const perks = { ...gameState.perks }; for (let id in perks) { if (!perks[id]) { delete perks[id]; } } runsHistory.push({ ...gameState.runStatistics, perks, appVersion, }); const unlocked = locked.filter( ({ li }) => !reasonLevelIsLocked(li, runsHistory, true), ); if (unlocked.length) { unlockedLevels = `

${unlocked.length === 1 ? t("unlocks.just_unlocked") : t("unlocks.just_unlocked_plural", { count: unlocked.length })}

${unlocked .map( ({ l, r }) => `
${icons[l.name]}

${l.name} ${describeLevel(l)}

`, ) .join("\n")} `; } // Generate some histogram 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 `${(!v && " ") || (vi == activeBin && lastValue + unit) || Math.round(binsTotal[vi] / v) + unit}`; }) .join(""); return `

${title} : ${lastValue}${unit}

${bars}
`; }; 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 = `

${t("gameOver.stats_intro", { count: runsHistory.length - 1 })}

` + runStats; } } catch (e) { console.warn(e); } return unlockedLevels + runStats; }