This commit is contained in:
Renan LE CARO 2025-04-07 08:24:17 +02:00
parent 46228a2128
commit 9624c5b351
16 changed files with 518 additions and 52 deletions

View file

@ -17,9 +17,13 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
## To do ## To do
- avoid showing a +1 and -1 at the same time when a combo increase is reset - avoid showing a +1 and -1 at the same time when a combo increase is reset
- display runs history
- display closest unlock with current perks in score and gameover screens
- progress of unlock
## Done ## Done
- in the runs history, only save perks that were chosen by the user
- migration to save past content to localStorage.recovery_data right before starting a new version - migration to save past content to localStorage.recovery_data right before starting a new version
- mention unlock conditions in help - mention unlock conditions in help
- show unlock condition in unlocks menu for perks as tooltip - show unlock condition in unlocks menu for perks as tooltip

159
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -43,7 +43,7 @@ export async function asyncAlert<t>({
content: (string | AsyncAlertAction<t>)[]; content: (string | AsyncAlertAction<t>)[];
allowClose?: boolean; allowClose?: boolean;
className?: string; className?: string;
}): Promise<t | void> { }): Promise<t | void|string> {
updateAlertsOpen(+1); updateAlertsOpen(+1);
return new Promise((resolve) => { return new Promise((resolve) => {
popupWrap.className = className; popupWrap.className = className;
@ -139,6 +139,11 @@ ${icon}
addto.appendChild(button); addto.appendChild(button);
}); });
popup.addEventListener('click', e=>{
if(e.target.getAttribute('data-resolve-to')){
closeWithResult(e.target.getAttribute('data-resolve-to'))
}
},true)
popupWrap.appendChild(popup); popupWrap.appendChild(popup);
( (
popupWrap.querySelector( popupWrap.querySelector(

View file

@ -1280,5 +1280,12 @@
"bricks": "_________________________bbb____ttb_bbbbb__tttbbbb_bbbttt_bbbb__bbbt__bbbb_ttbbb__bbttttttbbbbbb_ttt___bbbb_____________________________________", "bricks": "_________________________bbb____ttb_bbbbb__tttbbbb_bbbttt_bbbb__bbbt__bbbb_ttbbb__bbttttttbbbbbb_ttt___bbbb_____________________________________",
"svg": null, "svg": null,
"color": "" "color": ""
},
{
"name": "icon:history",
"size": 8,
"bricks": "__gggg___ggbggg_gggbgggggggbggggggggbbgggggggggg_gggggg___gggg__",
"svg": null,
"color": ""
} }
] ]

View file

@ -1,6 +1,5 @@
* { * {
font-family: font-family: Courier New,
Courier New,
Courier, Courier,
Lucida Sans Typewriter, Lucida Sans Typewriter,
Lucida Typewriter, Lucida Typewriter,
@ -465,3 +464,32 @@ h2.histogram-title strong {
border: 1px solid white; border: 1px solid white;
max-width: 300px; max-width: 300px;
} }
#popup.history > div {
max-width: none;
table {
th:hover{
cursor: pointer;
background: black;
}
td, th {
padding: 0 5px;
line-height: 20px;
text-align: right;
}
th:first-child, td:first-child {
text-align: left
}
img{
width: 20px;
height: auto;
pointer-events: none;
}
tr:nth-child(2n) {
background: rgba(0, 0, 0, 0.58);;
}
}
}

View file

@ -44,6 +44,7 @@ import {startingPerkMenuButton} from "./startingPerks";
import "./migrations"; import "./migrations";
import {getCreativeModeWarning, getHistory} from "./gameOver"; import {getCreativeModeWarning, getHistory} from "./gameOver";
import {generateSaveFileContent} from "./generateSaveFileContent"; import {generateSaveFileContent} from "./generateSaveFileContent";
import {runHistoryViewerMenuEntry} from "./runHistoryViewer";
export async function play() { export async function play() {
if (await applyFullScreenChoice()) return; if (await applyFullScreenChoice()) return;
@ -460,6 +461,7 @@ export async function openMainMenu() {
}, },
}, },
creativeMode(gameState), creativeMode(gameState),
runHistoryViewerMenuEntry(),
{ {
icon: icons["icon:unlocks"], icon: icons["icon:unlocks"],
text: t("main_menu.unlocks"), text: t("main_menu.unlocks"),

View file

@ -33,7 +33,6 @@ export function gameOver(title: string, intro: string) {
pause(true); pause(true);
stopRecording(); stopRecording();
addToTotalPlayTime(gameState.runStatistics.runTime); addToTotalPlayTime(gameState.runStatistics.runTime);
gameState.runStatistics.max_level = gameState.currentLevel + 1;
let animationDelay = -300; let animationDelay = -300;
const getDelay = () => { const getDelay = () => {
@ -112,6 +111,7 @@ try {
localStorage.getItem("breakout_71_runs_history") || "[]", localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[]; ) as RunHistoryItem[];
} catch (e) {} } catch (e) {}
export function getHistory() { export function getHistory() {
return runsHistory; return runsHistory;
} }

View file

@ -596,23 +596,6 @@ export function addToScore(gameState: GameState, coin: Coin) {
} }
} }
function recordBestWorstLevelScore(gameState: GameState) {
const levelScore = gameState.score - gameState.levelStartScore;
const { runStatistics } = gameState;
if (
runStatistics.best_level_score === -1 ||
runStatistics.best_level_score < levelScore
) {
runStatistics.best_level_score = levelScore;
}
if (
runStatistics.worst_level_score === -1 ||
runStatistics.worst_level_score > levelScore
) {
runStatistics.worst_level_score = levelScore;
}
}
export async function setLevel(gameState: GameState, l: number) { export async function setLevel(gameState: GameState, l: number) {
// Here to alleviate double upgrades issues // Here to alleviate double upgrades issues
if (gameState.upgradesOfferedFor >= l) { if (gameState.upgradesOfferedFor >= l) {
@ -622,7 +605,6 @@ export async function setLevel(gameState: GameState, l: number) {
gameState.upgradesOfferedFor = l; gameState.upgradesOfferedFor = l;
pause(false); pause(false);
stopRecording(); stopRecording();
recordBestWorstLevelScore(gameState);
if (l > 0) { if (l > 0) {
await openUpgradesPicker(gameState); await openUpgradesPicker(gameState);

View file

@ -419,6 +419,196 @@
</folder_node> </folder_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>history</name>
<children>
<folder_node>
<name>columns</name>
<children>
<concept_node>
<name>max_combo</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>max_level</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>puck_bounces</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>puck_bounces_tooltip</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>runTime</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>runTime_tooltip</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>score</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>started</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>upgrades_picked</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>
<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>locked</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>title</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> <folder_node>
<name>lab</name> <name>lab</name>
<children> <children>

View file

@ -24,6 +24,18 @@
"gameOver.unlocked_perk_plural": "You just unlocked {{count}} perks", "gameOver.unlocked_perk_plural": "You just unlocked {{count}} perks",
"gameOver.win.summary": "This game is over. You stashed {{score}} coins. ", "gameOver.win.summary": "This game is over. You stashed {{score}} coins. ",
"gameOver.win.title": "You completed this game", "gameOver.win.title": "You completed this game",
"history.columns.max_combo": "Max combo",
"history.columns.max_level": "Levels",
"history.columns.puck_bounces": "PB",
"history.columns.puck_bounces_tooltip": "Puck bounces : number of time the ball bounced on the puck",
"history.columns.runTime": "Dur.",
"history.columns.runTime_tooltip": "Duration of the run, in seconds, only counting time where the game is running and the ball is in motion",
"history.columns.score": "Score",
"history.columns.started": "Date",
"history.columns.upgrades_picked": "Upgrades",
"history.help": "See the list of your {{count}} game",
"history.locked": "Play at least ten games to unlock",
"history.title": "Runs history",
"lab.help": "Try any build you want", "lab.help": "Try any build you want",
"lab.instructions": "Select upgrades below, then pick the level to play. Creative mode runs are ignored in unlocks, high score, total score and statistics, and only last one level.", "lab.instructions": "Select upgrades below, then pick the level to play. Creative mode runs are ignored in unlocks, high score, total score and statistics, and only last one level.",
"lab.menu_entry": "Creative mode", "lab.menu_entry": "Creative mode",

View file

@ -24,6 +24,18 @@
"gameOver.unlocked_perk_plural": "Vous avez débloqué {{count}} améliorations", "gameOver.unlocked_perk_plural": "Vous avez débloqué {{count}} améliorations",
"gameOver.win.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces. ", "gameOver.win.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces. ",
"gameOver.win.title": "Vous avez terminé cette partie", "gameOver.win.title": "Vous avez terminé cette partie",
"history.columns.max_combo": "",
"history.columns.max_level": "",
"history.columns.puck_bounces": "",
"history.columns.puck_bounces_tooltip": "",
"history.columns.runTime": "Dur.",
"history.columns.runTime_tooltip": "",
"history.columns.score": "",
"history.columns.started": "",
"history.columns.upgrades_picked": "",
"history.help": "",
"history.locked": "",
"history.title": "",
"lab.help": "Essayez n'importe quel build", "lab.help": "Essayez n'importe quel build",
"lab.instructions": "Sélectionnez les améliorations ci-dessous, puis choisissez le niveau à jouer. Les parties en mode créatif sont ignorées dans les déblocages, le meilleur score, le score total et les statistiques, et ne durent qu'un seul niveau.", "lab.instructions": "Sélectionnez les améliorations ci-dessous, puis choisissez le niveau à jouer. Les parties en mode créatif sont ignorées dans les déblocages, le meilleur score, le score total et les statistiques, et ne durent qu'un seul niveau.",
"lab.menu_entry": "Mode créatif", "lab.menu_entry": "Mode créatif",

View file

@ -78,7 +78,15 @@ migrate("compact_runs_data", () => {
delete r.perks[key] delete r.perks[key]
} }
} }
if('best_level_score' in r) {
delete r.best_level_score
}
if('worst_level_score' in r) {
delete r.worst_level_score
}
}); });
localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory)); localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory));
}); });

View file

@ -103,8 +103,6 @@ export function newGameState(params: RunParams): GameState {
runTime: 0, runTime: 0,
coins_spawned: 0, coins_spawned: 0,
score: 0, score: 0,
best_level_score: -1,
worst_level_score: -1,
bricks_broken: 0, bricks_broken: 0,
misses: 0, misses: 0,
balls_lost: 0, balls_lost: 0,
@ -112,7 +110,6 @@ export function newGameState(params: RunParams): GameState {
wall_bounces: 0, wall_bounces: 0,
upgrades_picked: 1, upgrades_picked: 1,
max_combo: 1, max_combo: 1,
max_level: 0,
}, },
lastOffered: {}, lastOffered: {},
levelTime: 0, levelTime: 0,

99
src/runHistoryViewer.ts Normal file
View file

@ -0,0 +1,99 @@
import {getHistory} from "./gameOver";
import {icons} from "./loadGameData";
import {t} from "./i18n/i18n";
import {asyncAlert} from "./asyncAlert";
import {rawUpgrades} from "./upgrades";
export function runHistoryViewerMenuEntry(){
const history = getHistory()
return {
icon:icons['icon:history'],
text:t('history.title'),
disabled : history.length<10,
help: history.length<10 ? t('history.locked'):t('history.help',{count:history.length}),
async value(){
let sort = 0
let sortDir = -1
let columns = [
{
label:t('history.columns.started'),
field: r=>r.started,
render(v){
return new Date(v).toISOString().slice(0,10)
}
},
{
label:t('history.columns.score'),
field: r=>r.score
},
{
label:t('history.columns.runTime'),
tooltip:t('history.columns.runTime_tooltip'),
field: r=>r.runTime,
render(v){
return Math.floor(v/1000)+'s'
}
},
{
label:t('history.columns.puck_bounces'),
tooltip:t('history.columns.puck_bounces_tooltip'),
field: r=>r.puck_bounces,
},
{
label:t('history.columns.max_combo'),
field: r=>r.max_combo,
},
{
label:t('history.columns.upgrades_picked'),
field: r=>r.upgrades_picked,
},
...rawUpgrades.map(u=>({
label: icons['icon:'+u.id],
tooltip:u.name,
field: r=>r.perks[u.id]||0,
render(v){
if(!v) return '-'
return v
}
}))
]
while(true){
const header = columns.map((c, ci) => `<th data-tooltip="${c.tooltip || ''}" data-resolve-to="sort:${ci}">${c.label}</th>`).join('')
const toString = v => v.toString()
const tbody = history.sort((a, b) => sortDir * (columns[sort].field(a) - columns[sort].field(b))).map(h => '<tr>' + columns.map(c => {
const value = c.field(h) ?? 0
const render = c.render || toString
return '<td>' + render(value) + '</td>'
}).join('') + '</tr>').join('')
const result = await asyncAlert({
title: t('history.title'),
className: 'history',
content: [
`
<table>
<thead><tr>${header}</tr></thead>
<tbody>${tbody}</tbody>
</table>
`
]
})
if(!result) return
if(result.startsWith('sort:')){
const newSort = parseInt(result.split(':')[1])
if(newSort==sort){
sortDir*=-1
}else{
sortDir=-1
sort=newSort
}
}
}
}
}
}

View file

@ -25,7 +25,7 @@ export function setupTooltips() {
while (parent && !parent.hasAttribute("data-tooltip")) { while (parent && !parent.hasAttribute("data-tooltip")) {
parent = parent.parentElement; parent = parent.parentElement;
} }
if (parent?.hasAttribute("data-tooltip")) { if (parent?.getAttribute("data-tooltip")?.trim()) {
hovering = parent as HTMLElement; hovering = parent as HTMLElement;
tooltip.innerHTML = hovering.getAttribute("data-tooltip") || ""; tooltip.innerHTML = hovering.getAttribute("data-tooltip") || "";
tooltip.style.display = ""; tooltip.style.display = "";

3
src/types.d.ts vendored
View file

@ -147,9 +147,6 @@ export type RunStats = {
wall_bounces: number; wall_bounces: number;
upgrades_picked: number; upgrades_picked: number;
max_combo: number; max_combo: number;
max_level: number;
best_level_score: number;
worst_level_score: number;
}; };
export type PerksMap = { export type PerksMap = {