2025-03-16 17:45:29 +01:00
import { allLevels , appVersion , upgrades } from "./loadGameData" ;
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-03-20 22:50:50 +01:00
import { currentLevelInfo , findLast , pickedUpgradesHTMl } from "./game_utils" ;
2025-03-16 17:45:29 +01:00
import { getTotalScore } from "./settings" ;
import { stopRecording } from "./recording" ;
import { asyncAlert } from "./asyncAlert" ;
2025-03-16 14:29:14 +01:00
export function getUpgraderUnlockPoints() {
2025-03-16 17:45:29 +01:00
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" ) ,
} ) ;
}
} ) ;
2025-03-16 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
allLevels . forEach ( ( l ) = > {
list . push ( {
threshold : l.threshold ,
title : l.name + " " + t ( "level_up.unlocked_level" ) ,
2025-03-16 14:29:14 +01:00
} ) ;
2025-03-16 17:45:29 +01:00
} ) ;
2025-03-16 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
return list
. filter ( ( o ) = > o . threshold )
. sort ( ( a , b ) = > a . threshold - b . threshold ) ;
2025-03-16 14:29:14 +01:00
}
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 ) { }
2025-03-16 14:29:14 +01:00
}
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
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 += `
2025-03-16 14:29:14 +01:00
< p class = "progress" >
< span > $ { u . title } < / span >
< span class = "progress_bar_part" style = "${getDelay()}" > < / span >
< / p >
` ;
2025-03-16 17:45:29 +01:00
} ) ;
const previousUnlockAt =
findLast ( list , ( u ) = > u . threshold <= endTs ) ? . threshold || 0 ;
const nextUnlock = list . find ( ( u ) = > u . threshold > endTs ) ;
2025-03-16 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
if ( nextUnlock ) {
const total = nextUnlock ? . threshold - previousUnlockAt ;
const done = endTs - previousUnlockAt ;
2025-03-16 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
intro += t ( "gameOver.next_unlock" , {
points : nextUnlock.threshold - endTs ,
} ) ;
2025-03-16 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
const scaleX = ( done / total ) . toFixed ( 2 ) ;
unlocksInfo += `
2025-03-16 14:29:14 +01:00
< p class = "progress" >
< span > $ { nextUnlock . title } < / span >
< span style = "transform: scale(${scaleX},1);${getDelay()}" class = "progress_bar_part" > < / span >
< / p >
` ;
2025-03-16 17:45:29 +01:00
list
. slice ( list . indexOf ( nextUnlock ) + 1 )
. slice ( 0 , 3 )
. forEach ( ( u ) = > {
unlocksInfo += `
2025-03-16 14:29:14 +01:00
< p class = "progress" >
< span > $ { u . title } < / span >
< / p >
` ;
2025-03-16 17:45:29 +01:00
} ) ;
}
2025-03-16 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
let unlockedItems = list . filter (
( u ) = > u . threshold > startTs && u . threshold < endTs ,
) ;
if ( unlockedItems . length ) {
2025-04-01 13:49:25 +02:00
unlocksInfo += ` <p> ${ t ( "gameOver.unlocked_count" , { count : unlockedItems.length } )} ${ unlockedItems . map ( ( u ) = > u . title ) . join ( ", " ) } </p> ` ;
2025-03-16 17:45:29 +01:00
}
2025-03-16 14:29:14 +01:00
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 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
asyncAlert ( {
allowClose : true ,
title ,
2025-03-27 10:52:31 +01:00
content : [
`
2025-03-16 14:29:14 +01:00
< p > $ { intro } < / p >
2025-03-16 17:45:29 +01:00
< p > $ { t ( "gameOver.cumulative_total" , { startTs , endTs } ) } < / p >
2025-03-16 14:29:14 +01:00
$ { unlocksInfo }
` ,
2025-03-16 17:45:29 +01:00
{
value : null ,
text : t ( "gameOver.restart" ) ,
help : "" ,
} ,
2025-03-27 10:52:31 +01:00
` <div id="level-recording-container"></div>
$ { pickedUpgradesHTMl ( gameState ) }
2025-04-01 13:35:33 +02:00
$ { getHistograms ( gameState ) }
2025-03-16 14:29:14 +01:00
` ,
2025-03-27 10:52:31 +01:00
] ,
} ) . then ( ( ) = >
restart ( {
2025-03-28 19:40:59 +01:00
levelToAvoid : currentLevelInfo ( gameState ) . name ,
2025-03-27 10:52:31 +01:00
} ) ,
) ;
2025-03-16 14:29:14 +01:00
}
2025-04-01 13:35:33 +02:00
export function getHistograms ( gameState : GameState ) {
2025-03-16 17:45:29 +01:00
let runStats = "" ;
try {
// Stores only top 100 runs
let runsHistory = JSON . parse (
localStorage . getItem ( "breakout_71_runs_history" ) || "[]" ,
) as RunHistoryItem [ ] ;
2025-03-28 11:58:58 +01:00
2025-03-16 17:45:29 +01:00
runsHistory . sort ( ( a , b ) = > a . score - b . score ) . reverse ( ) ;
runsHistory = runsHistory . slice ( 0 , 100 ) ;
runsHistory . push ( {
. . . gameState . runStatistics ,
perks : gameState.perks ,
2025-04-01 13:35:33 +02:00
mode : gameState.mode ,
2025-03-16 17:45:29 +01:00
appVersion ,
} ) ;
// 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-01 13:35:33 +02:00
let values = runsHistory
. filter ( ( h ) = > ( h . mode || "short" ) === gameState . mode )
. 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 } "
2025-03-16 14:29:14 +01:00
> < 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 14:29:14 +01:00
2025-03-16 17:45:29 +01:00
return ` <h2 class="histogram-title"> ${ title } : <strong> ${ lastValue } ${ unit } </strong></h2>
2025-03-16 14:29:14 +01:00
< 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 14:29:14 +01:00
}
2025-03-16 17:45:29 +01:00
} catch ( e ) {
console . warn ( e ) ;
}
return runStats ;
}