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" ;
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
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-03-16 14:29:14 +01:00
2025-04-06 15:38:30 +02:00
let unlocksInfo = unlockedPerks . length
? `
2025-03-16 14:29:14 +01:00
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 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-04-06 15:38:30 +02:00
getCreativeModeWarning ( gameState ) ,
2025-03-27 10:52:31 +01:00
`
2025-03-16 14:29:14 +01:00
< p > $ { intro } < / p >
2025-04-06 10:13:10 +02:00
< p > $ { t ( "gameOver.cumulative_total" , { startTs , endTs } ) } < / p >
2025-03-16 14:29:14 +01:00
` ,
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-03-16 14:29:14 +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 } "
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 ) ;
}
2025-04-06 15:38:30 +02:00
return runStats + unlockedLevels ;
2025-03-16 17:45:29 +01:00
}