diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 22699c1..e0ac0f7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
- versionCode = 29104759
- versionName = "29104759"
+ versionCode = 29104940
+ versionName = "29104940"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html
index b86eae0..77788e9 100644
--- a/app/src/main/assets/index.html
+++ b/app/src/main/assets/index.html
@@ -1 +1 @@
-
Breakout 71
\ No newline at end of file
+Breakout 71
\ No newline at end of file
diff --git a/dist/index.html b/dist/index.html
index 6275054..451e945 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -1690,7 +1690,7 @@ module.exports = JSON.parse("{\"_\":\"\",\"B\":\"black\",\"W\":\"#FFFFFF\",\"g\"
module.exports = JSON.parse('[{"name":"icon:addiction","size":9,"bricks":"__________________________t__WWWWW_tWWWrrttttr_WWWWW_tr_______t__________________","credit":""},{"name":"icon:asceticism","size":8,"bricks":"_tttttt__tt__tt_____W_______r______________r_________r_____WWW__","credit":""},{"name":"icon:ball_attract_ball","size":8,"bricks":"__b__b____b__b__bbW__Wbb________________bbW__Wbb__b__b____b__b__","credit":""},{"name":"icon:ball_attracts_coins","size":8,"bricks":"WWW_____WWW_y___WWW____y__y_y____y____y_____y_____y____y___y_y__","credit":""},{"name":"icon:ball_repulse_ball","size":8,"bricks":"Wbb__bbWb______bb______b________________b______bb______bWbb__bbW","credit":""},{"name":"icon:base_combo","size":7,"bricks":"________ttttt__tytyt__ttttt__tytyt__ttttt________","credit":""},{"name":"icon:bigger_explosions","size":8,"bricks":"__O__Oy___Oyy_____OyOy__OyyyByOO_OOBBBy___yyByO__yOOy_OO_OO_____","credit":""},{"name":"icon:bigger_puck","size":8,"bricks":"_________GGGGGG__GGGGGG______________________W___________WWWWWW_","credit":""},{"name":"icon:bricks_attract_ball","size":8,"bricks":"ttW_____tt_y________y________ytt____y_tt___y____tty_____tt_y____","credit":""},{"name":"icon:bricks_attract_coins","size":9,"bricks":"______________________________bbbybbbbybbb______bbbybb___y_y______b_b______b_b___","credit":""},{"name":"icon:buoy","size":7,"bricks":"___y______y_____yyy__tyyyyytttOOOtttttOtttttttttt","credit":""},{"name":"icon:checkmark_checked","size":6,"bricks":"_ggggbgBBBbbbbBbbggbbbBggBbBBg_gggg_","credit":""},{"name":"icon:checkmark_unchecked","size":6,"bricks":"_gggg_gBBBBggBBBBggBBBBggBBBBg_gggg_","credit":""},{"name":"icon:clairvoyant","size":9,"bricks":"__y___y__y__y_y__y_y__t__y____ttt_____tWWWt___tWWgWWt_tttWWWttt__________________","credit":""},{"name":"icon:coin_magnet","size":8,"bricks":"__y__y_yy_________y_y_y_y________y_y______________y______WWW____","credit":""},{"name":"icon:coins","size":8,"bricks":"__bbbb___bbggbb_bbggggbbbggggggbbggggggbbbggggbb_bbggbb___bbbb__","credit":""},{"name":"icon:compound_interest","size":8,"bricks":"_________tttttt__ttt__t_____W________________r___________WWW__r_","credit":""},{"name":"icon:concave_puck","size":7,"bricks":"___W_____________W__________G__W__GGG___GGGGGGGGG","credit":""},{"name":"icon:corner_shot","size":9,"bricks":"___W________W________W__WW____W__WW____W________W______W_W_WWW_WW_W_WWWWWW_W_WWWW","credit":""},{"name":"icon:creative","size":7,"bricks":"bbg_bgg_______bbb_bgg_______bgg_bbg_______bbg_bbb","credit":""},{"name":"icon:double_or_nothing","size":10,"bricks":"_______________________yyyy____yyyyyyyy__yyyyyyyb_yyyyyyybbbyyyyyybbbb______________________________","credit":""},{"name":"icon:download","size":8,"bricks":"___bb______bb______bb______bb______bb____bbbbbb___bbbb__gggbbggg","credit":""},{"name":"icon:editor","size":10,"bricks":"_______ggg______gggg_____ggggg____ggggg____ggggg____ggggg____ggggg____bgggg_____bbgg______bbb_______","credit":""},{"name":"icon:etherealcoins","size":11,"bricks":"_____y_________yyy________bbb________bbb_______ybbby_____yybbbyy____yybbbyy____yybbbyy____y__y__y________________________","credit":""},{"name":"icon:extra_levels","size":6,"bricks":"__________t__W_tt_WWW_t__W_ttt______","credit":""},{"name":"icon:extra_life","size":8,"bricks":"_________WW__WW_WGGWWGGWWGGGGGGWWGGGGGGW_WGGGGW___WGGW_____WW___","credit":""},{"name":"icon:forgiving","size":8,"bricks":"____y______y_y____y___y__y_____yy_____y__y___y____y_y____WWWWW__","credit":""},{"name":"icon:fountain_toss","size":12,"bricks":"_____________________y_________y______________y______y__y_____WWWWWWWW___WttttttttW_WtytttytyttWWtttyttttttWWWtyttttytWW_WWWWWWWWWW___WWWWWWWWW_","credit":""},{"name":"icon:ghost_coins","size":7,"bricks":"__bbb___bbbbb_bbybybbbbbbbbbbbyyybbbbbbbbbbb_b_bb","credit":""},{"name":"icon:gold_medal","size":10,"bricks":"tttttttttttttttttttttt______ttttt____ttt_tttggttt___tgyygt____gyyyyg____gyyyyg_____gyyg_______gg____","credit":""},{"name":"icon:golden_goose","size":8,"bricks":"_bbby____bbb_y___bbby_y__y_y_y_y__y_y_y____y_y_y____y_y______y_y","credit":""},{"name":"icon:happy_family","size":9,"bricks":"__tt_tt____tt_tt____tt_tt____________________W_______W__W_W_W___________rrrWWWrrr","credit":""},{"name":"icon:helium","size":8,"bricks":"_y____y_yb____bybb___ybbb____b_b_____b____________________WWW___","credit":""},{"name":"icon:help","size":8,"bricks":"___bb_____bbbb___bb__bb__bb__bb_____bb_____bb______________bb___","credit":""},{"name":"icon:history","size":8,"bricks":"__gggg___ggbggg_gggbgggggggbggggggggbbgggggggggg_gggggg___gggg__","credit":""},{"name":"icon:hot_start","size":7,"bricks":"tt__ttt__t_trt_t__tttt_____ttttWttt________WWW___","credit":""},{"name":"icon:implosions","size":8,"bricks":"y______b___yb_b__y_Bbb_____Bbbby_bbbB_____bbB___yb_by___b_____y_","credit":""},{"name":"icon:left_is_lava","size":8,"bricks":"r_______rtttttt_rtttttt_r_______r_______r____W__r_______r_WWW___","credit":""},{"name":"icon:limitless","size":12,"bricks":"_________________________bbb____yyb_bbbbb__yyybbbb_bbbyyy_bbbb__bbby__bbbb_yybbb__bbyyyyyybbbbbb_yyy___bbbb_____________________________________","credit":""},{"name":"icon:metamorphosis","size":8,"bricks":"yyyyyy__yyyy__________W___________bbyybb__bbbbbb_________WWW____","credit":""},{"name":"icon:minefield","size":7,"bricks":"tB___Bttt___tt__ByB____yyy__tB___Bttt___tt_______","credit":""},{"name":"icon:multiball","size":8,"bricks":"_________tttttt__tttttt___________W__W____________________WWW___","credit":""},{"name":"icon:nbricks","size":7,"bricks":"________tttrt__ttr_r____________W__________WWW___","credit":""},{"name":"icon:new_run","size":7,"bricks":"_ggg____gbgg___gbbgg__gbbbg__gbbgg__gbgg___ggg___","credit":""},{"name":"icon:no_medal","size":10,"bricks":"gggggggggggggggggggggg______ggggg____ggg_g_gggg_g___gg__gg____g____g____g____g_____g__g_______gg____","credit":""},{"name":"icon:one_more_choice","size":7,"bricks":"WWW____WGGG___WGWWW__WGWGGG__GWGGG___WGGG____GGG_","credit":""},{"name":"icon:ottawa_treaty","size":8,"bricks":"BBbyybBBBbbyybbBbyybbbybbbyyyybbbbbyybbbbbyyybbbByybybbBBBbbbyBB","credit":""},{"name":"icon:passive_income","size":8,"bricks":"_ttttt___ttt_t________________W_____________________WWW_______r_","credit":""},{"name":"icon:picky_eater","size":8,"bricks":"_rrr_______rt_____rtt_____r_t______ttt_______W____________WWWW__","credit":""},{"name":"icon:pierce","size":6,"bricks":"ttttttttttWtttt__ttt__ttt__ttt__tttt","credit":""},{"name":"icon:pierce_color","size":8,"bricks":"tt___tttt__t_ttt_____ttt____ttttt____ttttt____ttttt____ttttt____","credit":""},{"name":"icon:premium","size":11,"bricks":"__g____g___g____g____g_g__gbg__g______g______gg_gbg_gg_gbbgbbbgbbggbbgbbbgbbg_gbgbbbgbg___ggggggg____ggggggg_____________","credit":""},{"name":"icon:puck_repulse_ball","size":8,"bricks":"__________________W_______b___W___b__b______b____________WWW____","credit":""},{"name":"icon:rainbow","size":10,"bricks":"yyyyybbb__yyyybbb___yyybbbr___yybbbOrr__ybbbyOOrr_bbbCyyOOrrbbtCCyyOOrb_ttCCyyOO___ttCCyyO____ttCCyy","credit":""},{"name":"icon:reach","size":8,"bricks":"tttttttttttttttttt____ttrr____rr___________W_____________WWW____","credit":""},{"name":"icon:reroll","size":8,"bricks":"___WWWWe__WgWWee_WWWWegellllleeelglglegellgllee_lglgle__lllll___","credit":""},{"name":"icon:reset","size":8,"bricks":"bb____bbbbb__bbb_bbbbbb___bbbb____bbbb___bbbbbb_bbb__bbbbb____bb","credit":""},{"name":"icon:respawn","size":9,"bricks":"tttttytttttttyyytttttttyttt_____________________________W_________________WWW____","credit":""},{"name":"icon:right_is_lava","size":8,"bricks":"_______r_ttttttr_ttttttr_______r_______r_____W_r_______r__WWW__r","credit":""},{"name":"icon:sacrifice","size":9,"bricks":"__b___b___bbb_bbb_bbyyyyybbbbybybybbbbyybyybb_bbyyybb___bybyb_____bbb_______b____","credit":""},{"name":"icon:sapper","size":9,"bricks":"_____WW______W__W_tttWttt_yttgggtt__tgggggt__tgggggt__tgggggt__ttgggtt__ttttttt__","credit":""},{"name":"icon:settings","size":9,"bricks":"___g_g____g_ggg_g___ggbgg__gggbbbggg_gbb_bbg_gggbbbggg__ggbgg___g_ggg_g____g_g___","credit":""},{"name":"icon:shocks","size":8,"bricks":"____y_Oy_bbbO_y__bbbOO_O_bbby_yyyyOyyOO_OO_ybbb__yO_bbb_y__ybbb_","credit":""},{"name":"icon:shunt","size":8,"bricks":"_______y______yy______yy__yttyyy__y__yyy_yy__yyy_yy__yyyyyy__yyy","credit":""},{"name":"icon:side_flip","size":7,"bricks":"________rtttt__rtttt____________W__________WWW___","credit":""},{"name":"icon:side_kick","size":7,"bricks":"________ttttr__ttttr__________W______________WWW_","credit":""},{"name":"icon:silver_medal","size":10,"bricks":"bbbbbbbbbbbbbbbbbbbbbb______bbbbb____bbb_bbbggbbb___bgllgb____gllllg____gllllg_____gllg_______gg____","credit":""},{"name":"icon:skip_last","size":5,"bricks":"_GGG_G_G_GGG_GGG_G_G_GGG_","credit":""},{"name":"icon:slow_down","size":10,"bricks":"_____________kk_______kkkk_____kkkkkkGG__kkkkkkGBG_kkkkkkGGGGkkkkkkGG__GGGGGG____GG__GG_____________","credit":""},{"name":"icon:smaller_puck","size":8,"bricks":"_________tttttt__tttttt_____________W_____________________yy____","credit":""},{"name":"icon:soft_reset","size":9,"bricks":"__yy______yyy_tt__yyyy_ttt_yyyy_tttt_____tttt_tttttttt_tttttttt__tttttt____tttt__","credit":""},{"name":"icon:starting_perks","size":8,"bricks":"_________b_b_b___________g_g_g_g_________g_g_g_g_________g_g_g_g","credit":""},{"name":"icon:sticky_coins","size":8,"bricks":"__________yy_yy___bbbby__ybbbb___ybbbb____bbbby______yy_________","credit":""},{"name":"icon:streak_shots","size":8,"bricks":"_tttttt__ttWttt___W_W____W___W__W_____W__W___W____W_W_____rrr___","credit":""},{"name":"icon:sturdy_bricks","size":7,"bricks":"tttttttttttttt____y_____y_y___y___y_______WWW____","credit":""},{"name":"icon:superhot","size":11,"bricks":"____________________________________________W_W_WWW_WWWWWW_W_W__W_W_W_WWW__W_____________________________________________","credit":""},{"name":"icon:telekinesis","size":8,"bricks":"______WW____GGWW___G______G_______G_______G_______G_____WWWWW___","credit":""},{"name":"icon:three_cushion","size":7,"bricks":"tttttttttttttt____r______r______r______r_____WWW_","credit":""},{"name":"icon:top_is_lava","size":8,"bricks":"rrrrrrrr_tttttt__tttttt____________________W_______________WWW__","credit":""},{"name":"icon:trampoline","size":8,"bricks":"_r_r_r_rrtttttt__ttttttrr___________W__rr______________r__WWW___","credit":""},{"name":"icon:transparency","size":9,"bricks":"__t_y_t___________t_y_t_y_t_________y_t_y_t_y_________t_y_t_y_t___________t_y_t__","credit":""},{"name":"icon:trickledown","size":8,"bricks":"_ybbbbbb_________y_y_y__bbbbbb____________y_y_y___bbbbbb_y______","credit":""},{"name":"icon:unbounded","size":8,"bricks":"bbyyyybbbbyyyybbbb____bbbb____bbbb____bbbb__y_bbbb____bbbbyyy_bb","credit":""},{"name":"icon:unlocked_levels","size":9,"bricks":"ggggggggggbbbgbbbggbgggggbggbgbgbgbgggggggggggbgbgbgbggbgggggbggbbbgbbbgggggggggg","credit":""},{"name":"icon:unlocked_upgrades","size":9,"bricks":"___ggg_____ggbgg___ggbbbgg_ggbbgbbgggbbbgbbbggggbgbggg__gbgbg____gbgbg____ggggg__","credit":""},{"name":"icon:upload","size":8,"bricks":"gggbbggg__bbbb___bbbbbb____bb______bb______bb______bb______bb___","credit":""},{"name":"icon:viscosity","size":8,"bricks":"________bb______ttbb__bbttttbbtttbttbtttttbttbtttttyttyttttttttt","credit":""},{"name":"icon:wind","size":9,"bricks":"_bb______b___yyyy_b_________bbbbbbb___________bbbbbbb_b________b___yyyy__bb______","credit":""},{"name":"icon:wrap_left","size":7,"bricks":"__W_______b_______b_______b_y_____y_b______WWW___","credit":""},{"name":"icon:wrap_right","size":7,"bricks":"___W_____b_____b_____y_____y_____b_____b____WWW__","credit":""},{"name":"icon:yoyo","size":8,"bricks":"____W____GGWGGG_GGWGGGGGGWGGGGGG_WWWWWW_GGGGGGGGGGGGGGGG_GGGGGG_","credit":""},{"name":"icon:zen","size":12,"bricks":"________________tttt_______tttttt_______tttt________BrrB_______tttttt_____tttttttt_____tttttt______BrrrrB_____tttttttt___tttttttttt___tttttttt__","credit":""},{"name":"71 mini","size":5,"bricks":"bbb____bt__btt__b_t___ttt","credit":""},{"name":"Butterfly","size":9,"bricks":"_________bb_t_t_bbbbb_t_bbbbbbbtbbbb_bbbtbbb____btb____bbbtbbb__bb_t_bb__________","credit":""},{"name":"Castle","size":7,"bricks":"s_s_s_ssssssssssBBBssssBBBssttbbbttttbbbtttbtbtbt","credit":""},{"name":"Eyes","size":9,"bricks":"ttttttt__tWWWWWWW_tWrrWttW_tWWWWWWW_ttttttt_____t______ttttt____ttttt_____t_t____","credit":"My favorite character in https://nuclearthrone.com/"},{"name":"Creeper","size":10,"bricks":"___________ccGGccGG__cGccGcGc__GBBccBBc__cBBGcBBc__GccBBGGc__ccBBBBcG__GGBBBBcG__cGBccBGc___________","credit":"https://en.wikipedia.org/wiki/Creeper_(Minecraft)"},{"name":"Stairs","size":8,"bricks":"tt______tt______bbtt____bbtt____vvbbtt__vvbbtt__ppvvbbttppvvbbtt","credit":""},{"name":"Dots","size":9,"bricks":"b_t_a_c_c__________b_t_a_c__________P_b_t_a_c__________P_b_t_a__________P_P_b_t_a","credit":""},{"name":"Lines","size":9,"bricks":"aaaaaaaa___________tttttttt_________aaaaaaaa___________tttttttt_________aaaaaaaa_","credit":""},{"name":"Heart","size":15,"bricks":"__________________RRR___RRR_____RSSSR_RSSSR___RSWWSSRSSSSSR__RSWSSSSSSSSSR__RSSSSSSSSSSSR__RSWSSSSSSSSSR___RSSSSSSSSSR_____RSSSSSSSR_______RSSSSSR_________RSSSR___________RSR_____________R_____________________________________","credit":"https://www.youtube.com/watch?v=gdWiTfzXb1g"},{"name":"Swiss","size":7,"bricks":"________RRRRR__RRWRR__RWWWR__RRWRR__RRRRR________","credit":""},{"name":"Germany","size":4,"bricks":"____ggggrrrryyyy","credit":""},{"name":"France","size":6,"bricks":"______ttWWrrttWWrrttWWrrttWWrrttWWrr","credit":""},{"name":"Smiley","size":8,"bricks":"_________yy__yy__yy__yy__________________yyyyyy___yyyy__________","credit":""},{"name":"Labyrinthe","size":11,"bricks":"_______tttS_Stttt_S________t___S__Stt_ttttt____t_____S__ttt_S_S____t___t_tttt_t_S_t____tSt_t_t_Sttt___t_t_____Sttt_tttttS","credit":""},{"name":"Temple","size":11,"bricks":"_______________WWW______WWWWWWW___WWWWWWWWW___b_b_b_b____b_b_b_b____v_v_v_v____P_P_P_P____P_P_P_P____WWWWWWW___WWWWWWWWW_","credit":""},{"name":"Pacman","size":12,"bricks":"____yyyy______yyyyyyyy___yyyyByyyyy__yyyyyyyyy__yyyyyyyy____yyyyyy______yyyyyy___S_Syyyyyyyy_____yyyyyyyyy___yyyyyyyyyy___yyyyyyyy______yyyy____","credit":"https://en.wikipedia.org/wiki/Pacman"},{"name":"Ship","size":11,"bricks":"____sWW________sWWW_______sWWW_______s___OOOOOOOOOOOOOO_OBOBOBOBOO__OOOOOOOO_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb___________","credit":""},{"name":"We come in peace","size":13,"bricks":"________________a_____a_______a___a_______aaaaaaa_____aaBaaaBaa___aaaaaaaaaaa__aaaaaaaaaaa__a_aaaaaaa_a__a_a_____a_a_____aa_aa___________________________________________","credit":"https://en.wikipedia.org/wiki/Space_invaders"},{"name":"Space mushroom","size":10,"bricks":"______________WW_______WWWW_____WWWWWW___WWBWWBWW__WWWWWWWW____W__W_____W_WW_W___W_W__W_W___________","credit":"https://en.wikipedia.org/wiki/Space_invaders"},{"name":"Wololo","size":9,"bricks":"____WW_OOW___WW__OWW__W___OWWWbbbW_WWW_WbW_WOW__WWb__OW__bbb__O___W_W__O___W_W__O","credit":"https://aoe.heavengames.com/theacademy/unitsboatsandbuildings/priest/"},{"name":"Small heart","size":15,"bricks":"________________________________RRRR___RRRR___RrWWrR_RWWrrR__RWWrrrRWWrrrR__RrrrrrrrrrrrR__RrrrrrrrrrrrR___RrrrrrrrrrR_____RrrrrrrrR_______RrrrrrR_________RrrrR___________RrR_____________R_____________________________________","credit":""},{"name":"Eye","size":9,"bricks":"____________ggg_____gWWWg___gWbbbWg_gWWbBbWWg_gWbbbWg___gWWWg_____ggg____________","credit":""},{"name":"Enderman","size":10,"bricks":"___________gggggggg__gggggggg__gggggggg__gggggggg__vvvggvvv__gggggggg__gggggggg__gggggggg___________","credit":"https://minecraft.wiki/w/Enderman"},{"name":"Mushroom","size":16,"bricks":"_____________________rrrrWW________WWrrrrWWWW_____WWrrrrrrWWWW____WrrWWWWrrWWW___rrrWWWWWWrrrrr__rrrWWWWWWrrWWr__WrrWWWWWWrWWWW__WWrrWWWWrrWWWW__WWrrrrrrrrrWWr__WrrWWWWWWWWrrr_____WWBWWBWW_______WWWBWWBWWW______WWWWWWWWWW_______WWWWWWWW____________________","credit":"https://pixelartmaker.com/art/cce4295a92035ea"},{"name":"Tulip","size":11,"bricks":"______________R_R_R______RRRRR______RRRRR______RRRRR_______RRR_________k________k_k_k______k_k_k_______kkk_________k_____","credit":""},{"name":"Chain","size":7,"bricks":"yyy____yBy____yyyyy____yBy____yyyyy____yBy____yyy","credit":""},{"name":"Marion","size":9,"bricks":"rr_____rr_rr___rr__rrr_rrr__rrrrrrr__rr_r_rr__rr___rr__rr___rr__rr___rr_rrr___rrr","credit":""},{"name":"Renan","size":9,"bricks":"yyyyyyy___yyyyyyy__yy___yy__yy___yy__yyyyyy___yy_yy____yy__yy___yy___yy_yyy___yyy","credit":""},{"name":"Violet Pairs","size":8,"bricks":"b_b_b_b_b_b_b_b__________t_t_t_t_t_t_t_t________b_b_b_b_b_b_b_b_","credit":""},{"name":"Red Cups","size":11,"bricks":"___________rBr_rBr_rBrrrr_rrr_rrr___________r_rBr_rBr_rr_rrr_rrr_r___________rBr_rBr_rBrrrr_rrr_rrr______________________","credit":""},{"name":"Cactus","size":10,"bricks":"____G______rG_Gk______G_Gk______kkkk_r_____kkk_G______GkGk_____rGkk_______Gk________kk________kk____","credit":""},{"name":"Sunny Face","size":11,"bricks":"____yyy______yyyyyyy___yyyyyyyyy__yyyyyyyyy_yyyWWyWWyyyyyyyyyyyyyyyyyyyyyyyyy_yyWWWWWyy__yyyWWWyyy___yyyyyyy______yyy____","credit":""},{"name":"Mountain","size":9,"bricks":"_______________W_______WWW______GGWW__W_GGGGG_kkkGGGGG_kkkkGGGGkkkkkGGGGkkkkkkGGG","credit":""},{"name":"Dollar","size":17,"bricks":"________________________G_G______________G_G____________GGGGGGG_________GGGGGGGGG_______GG__G_G__GG______GG__G_G__GG______GG__G_G___________GGGGGGGG__________GGGGGGGG___________G_G__GG______GG__G_G__GG______GG__G_G__GG_______GGGGGGGGG_________GGGGGGG____________G_G______________G_G_______","credit":""},{"name":"Waves","size":8,"bricks":"___bbb____bbb____bbttbbbbbttbbbbttttaatttttaattttaaaaaaa________","credit":""},{"name":"Box","size":8,"bricks":"yyyyyyyyy______yy_bbbb_yy_b__b_yy_b__b_yy_bbbb_yy______yyyyyyyyy","credit":""},{"name":"Rose","size":9,"bricks":"__SS______SSSS_____SSSS_____SSSS______SS_k______k_kk_____kk_k______kk________k___","credit":""},{"name":"Time","size":9,"bricks":"__________WWWWWWW___WWWWW_____yyy_______y________y_______WyW_____WyyyW___yyyyyyy_","credit":""},{"name":"Watermelon","size":8,"bricks":"_____Sk_____SSBk___SBSSk__SSSSSk_SSBSSk_SBSSSSk_kSSSkk___kkk____","credit":""},{"name":"Worms","size":13,"bricks":"___sssss_______sssssss______WWsWWsss_____WBsBWsss_____WBsBWsss_____WWsWWsss_____sssssss_______ssssss_____WWWWWWss_______WssWs__s_____ssss__sss___sssssssssss__sssssssss_s","credit":"https://en.wikipedia.org/wiki/Worms_(series)"},{"name":"Ocean Sunrise","size":8,"bricks":"SSSSSSSSSSSyySSSSSyyyySSSyyyyyySbttttttbbbttttbbbbbttbbbbbbbbbbb","credit":""},{"name":"Crosses","size":13,"bricks":"b___b___b___b__v___v___v___vvv_vvv_vvv___v___v___v__p___p___p___ppp_ppp_ppp_ppp___p___p___p__P___P___P___PPP_PPP_PPP___P___P___P__p___p___p___ppp_ppp_ppp_ppp___p___p___p","credit":""},{"name":"Negative space","size":9,"bricks":"tttttttttt_t_t_t_t_________b_b_b_b_bbbbbbbbbb_b_b_b_b___________t_t_t_t_ttttttttt","credit":""},{"name":"UK","size":11,"bricks":"brbbWrWbbrbbbrbWrWbrbbbbbrWrWrbbbWWWWWrWWWWWrrrrrrrrrrrWWWWWrWWWWWbbbrWrWrbbbbbrbWrWbrbbbrbbWrWbbrb______________________","credit":""},{"name":"Greece","size":11,"bricks":"ttWttttttttttWttWWWWWWWWWWWttttttttWttWWWWWWttWttttttttWWWWWWWWWWWtttttttttttWWWWWWWWWWWttttttttttt______________________","credit":""},{"name":"Russia","size":8,"bricks":"________WWWWWWWWWWWWWWWWttttttttttttttttrrrrrrrrrrrrrrrr________","credit":""},{"name":"Ukraine","size":8,"bricks":"________ttttttttttttttttttttttttyyyyyyyyyyyyyyyyyyyyyyyy________","credit":""},{"name":"Poland","size":7,"bricks":"________WWWWW__WWWWW__rrrrr__rrrrr_______________","credit":""},{"name":"Yellow 71","size":9,"bricks":"_________yyyyy__yyyyyyy_yyy___yy__yy__yyy__yy_yyy___yy_yy____yy_yy____yy_________","credit":""},{"name":"71 on white","size":6,"bricks":"WWWWWWrrrWWrWWrWrrWrWWWrWrWWWrWWWWWW","credit":""},{"name":"Blue 71","size":8,"bricks":"ttttt__bttttt_bb___ttbbb__tt__bb__tt__bb_tt___bb_tt___bb_tt___bb","credit":""},{"name":"Seventy one","size":21,"bricks":"rr_yy_rrry_yrrry_yrrrr_ry_yr__y_yr_ry_y_r_rr_yy_rr_yy_r_ry_y_r_r_ry_yr__y_yr_ry_y_r_rr_y_yrrry_yrrryyy_r_yyy__________________y______________r_____yyyrrry_yrrryyyrr_y_y__yrr_y_yrr_y_yr__y_yyyyrrr_y_rrry_yrrryyy____________________yrrryyyrrr_________yy_r_ry_yrr_____________rrry_yrrryyyyyyyyyyyy___________________________________________________________________________________________________________________________________________________","credit":""},{"name":"B71","size":10,"bricks":"__________bbbtttt_b_b__b__tbb_b__b__t_b_bbb__t__b_b__b_t__b_b__bt___b_bbb_t__bbb____________________","credit":""},{"name":"Pig","size":9,"bricks":"__________PP___PP__PPP_PPP__WWPPPWW__WBPPPBW__PPsssPP__PsBsBsP__PPsssPP__________","credit":""},{"name":"Big Pig","size":15,"bricks":"________________sss_______sss__ss__sssss__ss____sssssssss_____sWBsssssBWs___ssBBsssssBBss__ssss_____ssss__sss_sssss_sss__sss_sBsBs_sss__sss_sssss_sss___sss_____sss____sssssssssss__GGGsssssssssGGGGGGsGsssssGsGGGGGGssGGGGGssGGG","credit":""},{"name":"Donkey Kong","size":9,"bricks":"OOr__a___OOr__a___ppppppp_O______a________a____pppppppr_a______b_a___O__ppppppp__","credit":""},{"name":"Banana","size":12,"bricks":"_________________e__________eee_________eee_________eee_________eeeyy_____yyeeyyyy___yyyyey_yC___yy_yyy___C_____yyyy_________yyyy_________yyyy__","credit":""},{"name":"Fox","size":8,"bricks":"e______eee_OO_eeeeOOOOeeeOBOOBOeOOOOOOOO_WWBBWW___WWWW_____WW___","credit":""},{"name":"Wiki","size":10,"bricks":"_______________________GGGG_____GGkkGG___GkggggkG__GgWWWWgG__GkggggkG___GGkkGG_____GGGG_____________","credit":""},{"name":"Baby Dog","size":8,"bricks":"_______W__eeeeWWWWeeWeWWWeBWeBeeeeWWWWee_eWBBWe__eWWWWe____WW___","credit":""},{"name":"dog 21","size":9,"bricks":"__________O_____O_OOOWWWOOOOOWWWWWOOOOeWWWWOO_eBeWWBW__eBeWWBW___eWBWW_____WRW___","credit":"https://prohama.com/dog-21-pattern/"},{"name":"A","size":7,"bricks":"__ttt___ttttt_ttt_ttttt___ttttttttttt___tttt___tt","credit":""},{"name":"B","size":9,"bricks":"_bbbbb_____bb_bb____bb_bb____bb_bb____bbbb_____bb_bb____bb_bb____bb_bb___bbbbb___","credit":""},{"name":"C","size":8,"bricks":"__rrrr___rrrrrr_rrr___rrrr______rr______rrr___rr_rrrrrr___rrrr__","credit":""},{"name":"D","size":8,"bricks":"_GGGGG____GG__G___GG__GG__GG__GG__GG__GG__GG__GG__GG__G__GGGGG__","credit":""},{"name":"e","size":8,"bricks":"__tttt___tttttt_tt____tttt____tttttttttttt_______tt__tt___tttt__","credit":""},{"name":"Elephant","size":18,"bricks":"_________________________llll_________lll_llllll_lll___lsssllllllllsssl__lsssllllllllsssl__lsssllBllBllsssl__lssllWllllWllssl___ll__llllll__ll_________llll_______________ll______________llll______________ll______________________________________________________________________________________________________________________","credit":"https://prohama.com/elephant-5-pattern/"},{"name":"Orca","size":20,"bricks":"____________________________________________________________________________________________ggggg____ggg_ggg___ggggggg____ggggg___ggggggggg____ggg___ggggWggWWW_____gggggggggggWWWW_____ggggggggggWWWWW_____gggggggggWWWWW_______gggggggWWWWW___________WWggWWW______________ggg_gg______________gg__g__________________________________________________________________________________________________________","credit":"https://prohama.com/whale-2-pattern/"},{"name":"Shark","size":17,"bricks":"__________________________________________g_______________ggg____________ggggggg_________ggggggggg_______ggggggggggg_____gggggWWWggggg____gBgWWWWWWWgBg___ggWWWWrWrWWWWgg__ggWWWrrrrrWWWgg_ggWWWrrrrrrrWWWggggWWrrrrrrrrrWWgggWWWrWrWrWrWrWWWggWWrrWWWWWWWrrWWggWWWWWWWWWWWWWWWg_________________","credit":"https://prohama.com/shark-2-pattern/"},{"name":"Bird","size":13,"bricks":"_______RRR____R____RSSSR___RR__RSSWWWR__RSR_RSWWBWR__RSSRRSWWWWyy_RSSSRSWWWR___RSSSSSSRR_____RRSSyyyy______RSyyyyy___RRRRSyyyy____RSSSRyyy_____RRRR______________________","credit":"https://prohama.com/bird-1-size-13x12/"},{"name":"Tux","size":14,"bricks":"_____gggg________gggggggg_____gggggggggg____gggggggggg___gggggggggggg__gggWBggWBggg__gggBBggBBggg__ggggyyyygggg_ggggggyyggggggggggWWWWWWggggg_gWWWWWWWWg_g__WWWWWWWWWW____WWWWWWWWWW____yyy____yyy__","credit":"https://prohama.com/pingwin-4-pattern/"},{"name":"Armenia","size":6,"bricks":"_______rrrr__bbbb__yyyy_____________","credit":""},{"name":"Austria","size":6,"bricks":"_______rrrr__WWWW__rrrr_____________","credit":""},{"name":"Benin","size":8,"bricks":"_________kkyyyy__kkyyyy__kkrrrr__kkrrrr_________________________","credit":""},{"name":"Botswana","size":10,"bricks":"___________tttttttt__tttttttt__tttttttt__WWWWWWWW__BBBBBBBB__WWWWWWWW__tttttttt__tttttttt__tttttttt_","credit":""},{"name":"Bulgaria","size":6,"bricks":"_______WWWW__cccc__rrrr_____________","credit":""},{"name":"Canada","size":7,"bricks":"________rWWWr__rWrWr__rWWWr______________________","credit":""},{"name":"Chad","size":8,"bricks":"_________bbyyRR__bbyyRR__bbyyRR_________________________________","credit":""},{"name":"China","size":6,"bricks":"______RRyRRRRyRyRRRRyRRRRRRRRR______","credit":""},{"name":"Colombia","size":7,"bricks":"________yyyyy__yyyyy__bbbbb__RRRRR_______________","credit":""},{"name":"Republic of the Congo","size":7,"bricks":"________kkkyy__kkyyr__kyyrr__yyrrr_______________","credit":""},{"name":"C\xf4te d\'Ivoire","size":8,"bricks":"_________OOWWGG__OOWWGG__OOWWGG_________________________________","credit":""},{"name":"Denmark","size":8,"bricks":"_________rrWrrr__rrWrrr__WWWWWW__rrWrrr__rrWrrr_________________","credit":""},{"name":"El Salvador","size":8,"bricks":"_________bbbbbb__bbbbbb__WWWkWW__WWkWWW__bbbbbb__bbbbbb_________","credit":""},{"name":"Egypt","size":8,"bricks":"_________RRRRRR__RRRRRR__WWWyWW__WWyWWW__gggggg__gggggg_________","credit":""},{"name":"Estonia","size":8,"bricks":"_________tttttt__tttttt__gggggg__gggggg__WWWWWW__WWWWWW_________","credit":""},{"name":"Finland","size":6,"bricks":"_______WtWW__tttt__WtWW_____________","credit":""},{"name":"Gabon","size":5,"bricks":"______CCC__yyy__ttt______","credit":""},{"name":"Georgia","size":9,"bricks":"__________WrWrWrW__WWWrWWW__rrrrrrr__WWWrWWW__WrWrWrW____________________________","credit":""},{"name":"Guinea","size":8,"bricks":"_________rryycc__rryycc__rryycc_________________________________","credit":""},{"name":"Indonesia","size":6,"bricks":"_______rrrr__rrrr__WWWW__WWWW_______","credit":""},{"name":"Pingwin","size":13,"bricks":"______gggg________ggWWgg_______gWWgWgy______ggWWWg_______ggggg_______gggWWW______gggggWWW___gggggggWWW____ggggggWWW_____ggggWWWW____gggWWWWW______ggWWWW________gWWyyy___","credit":"https://prohama.com/pingwin-2-pattern/"},{"name":"Dog 8","size":17,"bricks":"_____________________________________gg_ggggg_gg____ggWWgWWWWWgWWgg__gWWgWWWWWWWgWWg__gWWgWWWWWWWgWWg__gggWWWWWWWWWggg___gWggWWWWWggWg____gWggWWWWWggWg____gWWWWgggWWWWg_____gWgWWgWWgWg______gWWggsggWWg_______gWgsssgWg_________ggsssgg____________ggg_________________________________________","credit":"https://prohama.com/dog-8-pattern/"},{"name":"Sunglasses","size":24,"bricks":"____________________________________________________ggggg______ggggg_______gg___g______g___gg_____gg________________gg___gg__________________gg_gggggggggg____gggggggggggggtttttggggggggbbbbbgggggtWWWttttggggbbbbWWWbgg_gtWttttttggggbbbbWbbbg__gtttttttgg__ggbbbbbbbg__gtttttttg____gbbbbbbbg__ggtttttgg____ggbbbbbgg___ggtttgg______ggbbbgg_____ggggg________ggggg___________________________________________________________________________________________________________________________________________________________________________________________________________________________","credit":"https://prohama.com/sunglasses-pattern-1/"},{"name":"Balloon","size":21,"bricks":"_____bbbWbbbWbbb_________PWbWPWbWPWbWP_______bWbbbWbbbWbbbWb_____WbbbWbbbWbbbWbbbW___WPWbWPWbWPWbWPWbWPW__bWbbbWbbbWbbbWbbbWb__bbbPbbbPbbbPbbbPbbb__bbPPPbPPPbPPPbPPPbb___PPWPPPWPPPWPPPWPP____PWbWPWbWPWbWPWbWP_____PWPPPWPPPWPPPWP_______PPWPPPWPPPWPP_________WbWPWbWPWbW___________bbbbbbbbb_____________b_____b______________b_____b______________b_____b______________WWWWWWW_______________PPPPP________________PPPPP________________PPPPP________","credit":"https://prohama.com/balloon-1/"},{"name":"Opening","size":14,"bricks":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbyyyyyyyyyyyybbyB___BB___Bybby__________ybbyyy______yyybbbbyyB__Byybbbbbbbyy__yybbbbbbbbby__ybbbbbyyyyby__ybyyyy___yby__yby______yby__yby______yBy__yBy______yyy__yyy___","credit":""},{"name":"Stripes","size":17,"bricks":"bbb______tttttt________tttttt________tttttt______bbtttttt______bbbbbttt______bbbbbb________bbbbbb________bbbbbb______ttbbbbbb______tttttbbb______tttttt________tttttt________tttttt______bbtttttt______bbbbbttt______bbbbbb________bbbbbb________bbbbbb________bbbbbb___________bbb______________","credit":""},{"name":"You are here","size":13,"bricks":"_____rrr_________rrrrr_______rrr_rrr______rr___rr______rr___rr_______rr_rr________rrrrr_________rrr__________rrr_________WWrWW_______WWWrWWW______WWWWWWW_______WWWWW____","credit":""},{"name":"Gear","size":13,"bricks":"_________________l_l_l______l_lllll_l_____lllllll____lllll_lllll___lll___lll___lll_____lll___lll___lll___lllll_lllll____lllllll_____l_lllll_l______l_l_l_________________","credit":""},{"name":"Play","size":15,"bricks":"_________________rrrrrrrrrrr___rrrrWWrrrrrrr__rrrrWWWrrrrrr__rrrrWWWWrrrrr__rrrrWWWWWrrrr__rrrrWWWWWWrrr__rrrrWWWWWrrrr__rrrrWWWWrrrrr__rrrrWWWrrrrrr__rrrrWWrrrrrrr___rrrrrrrrrrr_______________________________________________","credit":""},{"name":"City","size":18,"bricks":"_______yyy___bbbbb________yyy__ybyyb________yyy__ybyyb__tt___yyy_b_ybbbb_tttt______bbbbbbbtttttt_______ybyybbbbbbb_______ybyybbyybyb_____b_ybbbbbyybyb_____bbbbbbbbbbbbb__bb___bbbbbbybyyb_bbbb__ybyybbybyybbbbbbb_ybyybbbbbbbtttttt_ybbbbbyybybtyyyyt_bbbbbbyybybtyyyyt_bbbbbbbbbbbtttttt_byybybybyybtytyyt_byybybybyybtttttt_byybb","credit":""},{"name":"Wiggle","size":17,"bricks":"__________________cccccc_ccc_cccc__c____c_c_c_c__c__ccc_cc_c_ccc_cc____c_c__c_____c___ccc_cccc_ccc_cc__c________c_c__c__ccc_ccc_cc_cccc____c_c_c_c________ccc_c_c_ccccccc__c___c_________c__ccc_ccccccccccc____c______________ccc_ccc_ccc_ccc__c___c_c_c_c_c_c__ccccc_ccc_ccc_c__________________","credit":""},{"name":"Graph","size":18,"bricks":"_______________________yy________________yyt__yytttt______tt_tttyy___t____yyt____t_____t____yy____tt_____t____t_____t______yy___t_____t______yy__tt_____yytttttt___tt___ttyy_____t___t____t__t_____t___yytttt__t_____t___yy______tt____t____t_______yy___t____ttt_____yyttyy______tyy___t___yy_______yytttt_________________________","credit":""},{"name":"Lightbulb","size":14,"bricks":"_______________y__yyyyy___y____yyyyyyy______yyyyyyyyy_____yyyyyyyyy___y_yyyyyyyyy__y__yyyyyyyyy_____yyyyyyyyy_____yyyBBByyy___y__yyByByy___y____yByBy_________lllll______y___lll___y_______lll______","credit":""},{"name":"Note","size":16,"bricks":"_____________WWW__________WWWWWW_______WWWWWW__W____WWWWWW_____W____WWW________W____W__________W____W__________W____W__________W____W__________W____W________WWW____W_______WWWW____W_______WWW___WWW____________WWWW____________WWW____________________________","credit":""},{"name":"Rocket","size":13,"bricks":"______b___________bbb_________bbBbb________btttb________ttBtt________ttttt________ttBtt________ttttt________ttBtt_______bbtttbb_____bbbyyybbb____bbbyyybbb____bb_ByB_bb__","credit":""},{"name":"Abstract","size":16,"bricks":"________________aaaaa_cccc_aaaaaaaaaa_cccc_aaaaa________________aaaa_cccc_aaaaaaaaaa_cccc_aaaaaa________________aaa_cccc_aaaaaaaaaa_cccc_aaaaaaa________________aa_cccc_aaaaaaaaaa_cccc_aaaaaaaa________________a_cccc_aaaaaaaaaa_cccc_aaaaaaaaa________________","credit":""},{"name":"Fingerprint","size":15,"bricks":"___SSSSSSSS______S_______SS____S__SSSSS__SS__S__S____SS__S____S__SS__SS_S___S__S_SS__S__S_S__S___SS_SS__SS_S_____S___S__S_S__SS__S__SS_S_S_SS_S__S__S_S_S_S___S_S__S_S_S_S___S_S__S___S_S___S_S__S__S__S___S_S__S__S__S__S___S_S_","credit":""},{"name":"Leaf","size":14,"bricks":"____________________________________________________________GGkGG________GGkGGkGG_____GGkGGkGGkkG_kkkkkkkkkkkGGG__GGkGGkGGkkG____GGkGGkGG_______GGkGG_______________________________________________","credit":""},{"name":"Abstract 2","size":14,"bricks":"______________yyyy______yyyy______________bbb_bbbbbb_bbbbb___bbbb___bbb__y__bb__y__b______________bbb_bbbbbb_bbbbb___bbbb___bbb__y__bb__y__b______________bbb_bbbbbb_bbbbb___bbbb___bbb__y__bb__y__b","credit":""},{"name":"Abstract 3","size":13,"bricks":"______________p_aaa_ppp_a__p___a_p___a__ppp_a_p_aaa_______________aaa_p_a_ppp__a___p_a___p__a_ppp_aaa_p_______________p_aaa_ppp_a__p___a_p___a__ppp_a_p_aaa______________","credit":""},{"name":"Abstract 4","size":13,"bricks":"______________y_y_y_y_y_y__y_y_y_y_y_y__y_y_y_y_y_y_______________bbb_bbb_bbb_______________bbb_bbb_bbb_______________y_y_y_y_y_y__y_y_y_y_y_y__y_y_y_y_y_y______________","credit":""},{"name":"Abstract 5","size":13,"bricks":"______________ccc_ccc_ccc__c_a_c_c_a_c__caa_aaa_aac_______________cca_aaa_acc__c_a_a_a_a_c__cca_aaa_acc_______________caa_aaa_aac__c_a_c_c_a_c__ccc_ccc_ccc______________","credit":""},{"name":"Abstract 6","size":13,"bricks":"_vvvvv_vvvvv__v___v_v___v__v_bbbbbbb_v__v_b_v_v_b_v__v_b_v_v_b_v__v_b_v_v_b_v__v_b_v_v_b_v__v_b_v_v_b_v__v_b_v_v_b_v__v_b_vvv_b_v__v_b_____b_v__vvvvvvvvvvv_bbbb_____bbbb","credit":""},{"name":"Hemiola","size":11,"bricks":"___gggg_____gggrrgg_____ggrrg_______gggg_____gggyygg_____ggyyg_______gggg_____gggCCgg_____ggCCg_______gggg________gg_____","credit":"Left a wonderful review on the play store."},{"name":"Obigre","size":13,"bricks":"_______________________________________OOOORgRgRgOOOOWOORgRgRgOOOOOWORgRgRgOWOOWOORgRgRgOOWOOWORgRgRgOWOOWOORgRgRgOOOOOOORgRgRgOOO_______________________________________","credit":"Colin helped a lot with the game design https://colin-crapahute.bearblog.dev/"},{"name":"Noodlemire","size":15,"bricks":"_________________________________ggggggggg_____g_________g___g___________g_g_____________gg_____________gg_____yyy_____ggg__yyyyyyy__ggggtyyyyyyyyytggggtttttttttttgggg_ttttttttt_gg_____ttttt___________________________________","credit":"Early adopter of the game"},{"name":"Bearded axe","size":12,"bricks":"______________WyyyOOy_____WyyyOOy_____Wyy_OO______Wyy_OO______Wyy_OO__________OO__________OO__________OO__________OO__________OO__________OO____","credit":"Did some nice bug reports"},{"name":"Lebanon","size":9,"bricks":"_________rrrrrrrrrWWWWkWWWWWWWkkkWWWWWkkkkkWWWWWWkWWWWrrrrrrrrr__________________","credit":""},{"name":"Spain","size":9,"bricks":"_________rrrrrrrrryyyyyyyyyyWrWyyyyyyrWryyyyyyWrWyyyyyyyyyyyyyyrrrrrrrrr_________","credit":""},{"name":"Uzbekistan","size":8,"bricks":"tWtttWttWtttWttttWtWtWttWWWWWWWWWWWWWWWWGGGGGGGGGGGGGGGGGGGGGGGG","credit":""},{"name":"Pakistan","size":8,"bricks":"________WWkkkkkkWWkkWkWkWWkWkkkkWWkWkkWkWWkkWWkkWWkkkkkk________","credit":""},{"name":"Korea","size":10,"bricks":"__________WWWWWWWWWWWgWWWWWWgWWgWrrrrWgWWWWrrbbWWWWWWrrbbWWWWgWbbbbWgWWgWWWWWWgWWWWWWWWWWW__________","credit":""},{"name":"Chile","size":9,"bricks":"_________tttWWWWWWtWtWWWWWWtttWWWWWWrrrrrrrrrrrrrrrrrrrrrrrrrrr__________________","credit":""},{"name":"T\xfcrkiye","size":12,"bricks":"____________rrrrrrrrrrrrrrrWWWrrrrrrrrWWrrrrrrrrrWWrrWrWrrrrrWWrrrWrrrrrrWWrrWrWrrrrrrWWrrrrrrrrrrrWWWrrrrrrrrrrrrrrrrrr________________________","credit":""},{"name":"Taj Mahal","size":11,"bricks":"_____e________WWWWW_____WWWWWWW____WWWWWWW____WWWWWWW__W__lllll__WWWeeeeeeeWWeeeeeWeeeeeeleeWWWeeleeeeWWWWWeeeeleWWlWWele","credit":"An approximative reproduction "},{"name":"Abstract 7","size":9,"bricks":"__________SS_t_SS__S_____S____t_t____t_____t____t_t____S_____S__SS_t_SS__________","credit":""},{"name":"Abstract 9","size":8,"bricks":"PP_vv_PP_P__v__P________vv_PP_vvv__P__v_________PP_vv_PP_P__v__P","credit":""},{"name":"Crosshair","size":9,"bricks":"____W_____WWWWWWW__WB_W_BW__W_____W_WWW_B_WWW_W_____W__WB_W_BW__WWWWWWW_____W____","credit":""},{"name":"Abstract 10","size":15,"bricks":"bbbB_ttttt_BbbbbBbb_ttBtt_bbBbb____tt_tt____bbbbb_tt_tt_bbbb_______________ttttt_b_b_tttttt_____b_b_____tt_ttt_b_b_ttt_ttBtBt_bBb_tBtBtttt_t_bbb_t_ttt________________bb_ttttttt_bb__Bb_tB___Bt_bB__Bb_ttt_ttt_bB_bbb_________bbb","credit":""},{"name":"Face","size":6,"bricks":"SSSSSSSOOOOSSBOOBSSOOOOSSOOOOS_OSSO_","credit":""},{"name":"Eiffel tower","size":11,"bricks":"_____O__________O__________O__________O_________OOO________OOO____k___O_O___kkk_OO_OO_kkkkkOOOOOkkkkkOOO_OOOkkkOOO___OOOk","credit":""},{"name":"Abstract 11","size":9,"bricks":"P_t_s_t_PP_t___t_PP_ttttt_PP_______PPPPPPPPPPP_______PP_sssss_PP_s___s_PP_s_t_s_P","credit":""},{"name":"Abstract 12","size":8,"bricks":"BbBb____bbbb____BbBb____bbbb________tBtB____tttt____tBtB____tttt","credit":""},{"name":"Abstract 13","size":9,"bricks":"SSSSbSSSSSbbSbSbbSSbbS_SbbSSSSS_SSSSbb_____bbSSSS_SSSSSbbS_SbbSSbbSbSbbSSSSSbSSSS","credit":""},{"name":"Abstract 14","size":11,"bricks":"aa_tt_aa_ttaa_tt_aa_tt__B__B__B__bb_aa_bb_aabb_aa_bb_aa__B__B__B__aa_bb_tt_bbaa_bb_tt_bb__B__B__B__tt_aa_bb_aatt_aa_bb_aa","credit":""},{"name":"S","size":10,"bricks":"___________Oyyyyyyy__Oyyyyyyy__Oyy__Oyy__Oyy_______Oyyyyyyy_______Oyy__Oyy__Oyy__Oyyyyyyy__Oyyyyyyy_","credit":""},{"name":"Abstract 15","size":11,"bricks":"____________S_vvv_SSS__S___v___S__SSS_vvv_S__________S__S_vvv_SSS__S___v______SSS_vvv_S____S_____S__v_SSS_SSS____________","credit":"Just random strokes"},{"name":"Mario!","size":11,"bricks":"________________________RRRRR_____RRRRRRRRR__kkkOOkO___kOkOOOkOOO_kOkkOOOkOOOkkOOOOkkkk___OOOOOOO________________________","credit":"Suggested by Nicolas03. A Mario level ! Sprite taken from https://art.pixilart.com/sr2d5c0683c82aws3.png . The sprite belongs to Nintendo"},{"name":"Minesweeper","size":16,"bricks":"___llltCCttBC______lllCBBttCB______lttbBbtltt______ltBrBClttt______lttCCCttBt______llttCBtttt______ltCBCttlll______ltBCCtCtCt______lttCCBCBrB______llltBCCtrB______ttttttlltt______CBrttlllll______CBrBCttttl______ttCCBttBtl______tttCCCtCCt______tBttBtltBt___","credit":"Suggested by Noodlemire. For once, you\'ll want to trigger as many mines as possible."},{"name":"Target","size":19,"bricks":"__________________________________________________________________________________________________________________________WWW_______________WrrrW_____________WrWWWrW____________WrWBWrW____________WrWWWrW_____________WrrrW_______________WWW__________________________________________________________________________________________________________________________","credit":"Suggested by Noodlemire. Unusually small level, with lots of room to miss your shots. Acts as decent aim practice."},{"name":"The Boys","size":10,"bricks":"__________rrrrr_____WWrWWrrrrrWWrWWWWrWWWWrWWWWrWWrWrWWWWrWWWrWWWrWrWW_____WrWWW____________________","credit":"Suggested by Bearded-Axe. My boys initals"},{"name":"A Very Dangerous High-Five","size":21,"bricks":"__________________________________________________yy_______________yy__yy__yy___________yy__yy__yy____________yy__yy_yy_________y__yy__yy_yy________yyy_yyy_yy_yy_________yy__yy_yyyyy__________yy_yyyyyyyy___yyy____yyyyygggyyy__yyy______yyygBBBgyy_yyy________ygBBBBBgyyyy__W______ygBBBBBgyyy__________yygBBBgyyyy___________yygBgyyyy____________yyyByyyy_____________yyyyByy_______________yyByy_________________r_________________________________","credit":"Suggested by Noodlemire. A unique shape, fun to bounce the ball between fingers. The palm was initially boring on its own, so I gave it a big bomb. It adds a distinct feeling between the top and bottom halves."},{"name":"Blinky","size":20,"bricks":"____________________________gggg______________ggrrrrgg___________grrrrrrrrg_________grrrrrrrrrrg_______grrrWWrrrrWWrg______grrWWWWrrWWWWg______grrWWbbrrWWbbg_____grrrWWbbrrWWbbrg____grrrrWWrrrrWWrrg____grrrrrrrrrrrrrrg____grrrrrrrrrrrrrrg____grrrrrrrrrrrrrrg____grrrrrrrrrrrrrrg____grrgrrrggrrrgrrg____grg_grg__grg_grg_____g___g____g___g_______________________________________________________________","credit":"Suggested by Big Goober. The red ghost, Blinky, from the arcade game \\"Pac Man\\""},{"name":"Fish","size":11,"bricks":"______________________________________________bbbb______tttttt___btgttbttt_bbtttttbtttb___ttbttt_bb_tttttt___b___________","credit":"A fish based on the fish discord emoji. Suggested by Big Goober. "},{"name":"Spider","size":7,"bricks":"_l_____Sgg____ggSgBB_gSgBBBBSgggggg_gg___g_g_g_g_","credit":"Suggested by obigre."},{"name":"Gliders","size":8,"bricks":"g_g______gg___l__g__l_l______ll__c________cc__W__cc____W_____WWW","credit":"iSuggested by obigre. Inspired by Conway\'s gliders"},{"name":"Lone island","size":8,"bricks":"C__________kkk____kkOkk___kkkO_k_k_k_O_______O______CC__tttCCCCt","credit":"Suggested by obigre. Which game would you take there ?"},{"name":"Spacewyrm Jon","size":8,"bricks":"___PPP____PPPP____SSSP____WPWP_P__PPP_PP___PP_____yPPy__bWWyyWWb","credit":"Suggested by obigre. The invertebrate hero with a gun"},{"name":"Taijitu","size":7,"bricks":"_WWWWW_W__WWWWgg__WBWggg_WWWgBg__WWgggg__g_ggggg_","credit":"Suggested by obigre. Yin and yang fishes"},{"name":"Egg pan","size":5,"bricks":"WWWWgWWyWggWWWggggg____g_","credit":"Suggested by obigre. Fried and tasty"},{"name":"Inception","size":20,"bricks":"____llllllllllll________lbbbbbb____l________lbbbbbb____l________l____bbtt__l________l____bbtt__l________l__bbtttt__l________l__bbtttt__l________l______tt__l________l____y_tt__l________l______ttttl________l_____yttttl________l__W_______l________l_____y____l________l__y_y_____l________l_y___y_y__l________l__________l________l___WWW____l________llllllllllll____________________________________________","credit":"Breakout 71 within Breakout 71. By Noodlemire"},{"name":"Chess","size":21,"bricks":"_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_W_","credit":"White n black by Topenvy"},{"name":"italy","size":8,"bricks":"_________GGWWrr__GGWWrr__GGWWrr__GGWWrr_________________________","credit":"italia by Topenvy"},{"name":"icon:steering","size":9,"bricks":"_bb__bb_____b___b_____b___bWWW_b___bWWW_WWWWWWWW_b___b____b___b___b___b__bb__bb__","svg":null,"color":""}]');
},{}],"iyP6E":[function(require,module,exports,__globalThis) {
-module.exports = JSON.parse("\"29104759\"");
+module.exports = JSON.parse("\"29104940\"");
},{}],"1u3Dx":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
@@ -3006,8 +3006,6 @@ parcelHelpers.export(exports, "ballTransparency", ()=>ballTransparency);
parcelHelpers.export(exports, "coinsBoostedCombo", ()=>coinsBoostedCombo);
parcelHelpers.export(exports, "miniMarkDown", ()=>miniMarkDown);
parcelHelpers.export(exports, "firstWhere", ()=>firstWhere);
-parcelHelpers.export(exports, "wallBouncedBest", ()=>wallBouncedBest);
-parcelHelpers.export(exports, "wallBouncedGood", ()=>wallBouncedGood);
parcelHelpers.export(exports, "levelTimeBest", ()=>levelTimeBest);
parcelHelpers.export(exports, "levelTimeGood", ()=>levelTimeGood);
parcelHelpers.export(exports, "catchRateBest", ()=>catchRateBest);
@@ -3082,7 +3080,7 @@ function firstWhere(arr, mapper) {
if (typeof result !== "undefined") return result;
}
}
-const wallBouncedBest = 2, wallBouncedGood = 7, levelTimeBest = 25, levelTimeGood = 45, catchRateBest = 98, catchRateGood = 90, missesBest = 1, missesGood = 6, choicePerSilver = 1, choicePerGold = 2, upPerSilver = 1, upPerGold = 1;
+const levelTimeBest = 25, levelTimeGood = 45, catchRateBest = 98, catchRateGood = 90, missesBest = 1, missesGood = 6, choicePerSilver = 1, choicePerGold = 2, upPerSilver = 1, upPerGold = 1;
const MAX_LEVEL_SIZE = 21;
const MIN_LEVEL_SIZE = 2;
function automaticBackgroundColor(bricks) {
@@ -3524,7 +3522,6 @@ var _i18N = require("./i18n/i18n");
var _pureFunctions = require("./pure_functions");
var _settings = require("./settings");
var _options = require("./options");
-var _render = require("./render");
function describeLevel(level) {
let bricks = 0, colors = new Set(), bombs = 0;
level.bricks.forEach((color)=>{
@@ -3770,649 +3767,57 @@ function zoneLeftBorderX(gameState) {
return gameState.offsetXRoundedDown - 1;
}
function zoneRightBorderX(gameState) {
- return (0, _render.gameCanvas).width - gameState.offsetXRoundedDown + 1;
+ return gameState.canvasWidth - gameState.offsetXRoundedDown + 1;
}
-},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./pure_functions":"6pQh7","./settings":"5blfu","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./render":"9AS2t"}],"9AS2t":[function(require,module,exports,__globalThis) {
+},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./pure_functions":"6pQh7","./settings":"5blfu","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
-parcelHelpers.export(exports, "gameCanvas", ()=>gameCanvas);
-parcelHelpers.export(exports, "ctx", ()=>ctx);
-parcelHelpers.export(exports, "bombSVG", ()=>bombSVG);
-parcelHelpers.export(exports, "background", ()=>background);
-parcelHelpers.export(exports, "backgroundCanvas", ()=>backgroundCanvas);
-parcelHelpers.export(exports, "haloCanvas", ()=>haloCanvas);
-parcelHelpers.export(exports, "getHaloScale", ()=>getHaloScale);
-parcelHelpers.export(exports, "render", ()=>render);
-parcelHelpers.export(exports, "renderAllBricks", ()=>renderAllBricks);
-parcelHelpers.export(exports, "drawPuck", ()=>drawPuck);
-parcelHelpers.export(exports, "drawBall", ()=>drawBall);
-parcelHelpers.export(exports, "drawCoin", ()=>drawCoin);
-parcelHelpers.export(exports, "drawFuzzyBall", ()=>drawFuzzyBall);
-parcelHelpers.export(exports, "drawBrick", ()=>drawBrick);
-parcelHelpers.export(exports, "roundRect", ()=>roundRect);
-parcelHelpers.export(exports, "drawIMG", ()=>drawIMG);
-parcelHelpers.export(exports, "drawText", ()=>drawText);
-parcelHelpers.export(exports, "scoreDisplay", ()=>scoreDisplay);
-parcelHelpers.export(exports, "getDashOffset", ()=>getDashOffset);
-var _gameStateMutators = require("./gameStateMutators");
-var _gameUtils = require("./game_utils");
-var _i18N = require("./i18n/i18n");
-var _game = require("./game");
-var _options = require("./options");
-var _pureFunctions = require("./pure_functions");
-const gameCanvas = document.getElementById("game");
-const ctx = gameCanvas.getContext("2d", {
- alpha: false
-});
-const bombSVG = document.createElement("img");
-bombSVG.src = "data:image/svg+xml;base64," + btoa(``);
-bombSVG.onload = ()=>(0, _game.gameState).needsRender = true;
-const background = document.createElement("img");
-background.onload = ()=>(0, _game.gameState).needsRender = true;
-const backgroundCanvas = document.createElement("canvas");
-const haloCanvas = document.createElement("canvas");
-const haloCanvasCtx = haloCanvas.getContext("2d", {
- alpha: false
-});
-function getHaloScale() {
- return 16 * ((0, _options.isOptionOn)("precise_lighting") ? 1 : 2);
-}
-let framesCounter = 0;
-function render(gameState) {
- framesCounter++;
- (0, _game.startWork)("render:init");
- const level = (0, _gameUtils.currentLevelInfo)(gameState);
- const hasCombo = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState);
- const { width, height } = gameCanvas;
- if (!width || !height) return;
- if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = (0, _i18N.t)("play.current_lvl", {
- level: gameState.currentLevel + 1,
- max: (0, _gameUtils.max_levels)(gameState)
- });
- else menuLabel.innerText = (0, _i18N.t)("play.menu_label");
- const catchRate = gameState.levelSpawnedCoins ? gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1) : // gameState.levelSpawnedCoins
- 1;
- (0, _game.startWork)("render:scoreDisplay");
- scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") || gameState.startParams.computer_controlled ? `
-
- ${0, _game.lastMeasuredFPS} FPS
- /
- ` : "") + ((0, _options.isOptionOn)("show_stats") ? `
- (0, _pureFunctions.catchRateGood) / 100 && "good" || ""}" data-tooltip="${(0, _i18N.t)("play.stats.coins_catch_rate")}">
- ${Math.floor(catchRate * 100)}%
- /
-
- ${Math.ceil(gameState.levelTime / 1000)}s
- /
-
- ${gameState.levelMisses} M
- /
- ` : "") + `$${gameState.score}`;
- scoreDisplay.classList[gameState.startParams.computer_controlled ? "add" : "remove"]("computer_controlled");
- scoreDisplay.classList[gameState.lastScoreIncrease > gameState.levelTime - 500 ? "add" : "remove"]("active");
- // Clear
- if (!(0, _options.isOptionOn)("basic") && level.svg && level.color === "#000000") {
- const skipN = (0, _options.isOptionOn)("probabilistic_lighting") && (0, _gameStateMutators.liveCount)(gameState.coins) > 150 ? 3 : 0;
- const shouldSkip = (index)=>skipN ? (framesCounter + index) % (skipN + 1) !== 0 : false;
- const haloScale = getHaloScale();
- (0, _game.startWork)("render:halo:clear");
- haloCanvasCtx.globalCompositeOperation = "source-over";
- haloCanvasCtx.globalAlpha = skipN ? 0.1 : 0.99;
- haloCanvasCtx.fillStyle = level.color;
- haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale);
- const brightness = (0, _options.isOptionOn)("extra_bright") ? 3 : 1;
- haloCanvasCtx.globalCompositeOperation = "lighten";
- haloCanvasCtx.globalAlpha = 0.1 + 5 / ((0, _gameStateMutators.liveCount)(gameState.coins) + 10);
- (0, _game.startWork)("render:halo:coins");
- (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin, index)=>{
- if (shouldSkip(index)) return;
- const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin);
- drawFuzzyBall(haloCanvasCtx, color, gameState.coinSize * 2 * brightness / haloScale, coin.x / haloScale, coin.y / haloScale);
- });
- (0, _game.startWork)("render:halo:balls");
- gameState.balls.forEach((ball, index)=>{
- if (shouldSkip(index)) return;
- haloCanvasCtx.globalAlpha = 0.3 * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState));
- drawFuzzyBall(haloCanvasCtx, gameState.ballsColor, gameState.ballSize * 2 * brightness / haloScale, ball.x / haloScale, ball.y / haloScale);
- });
- (0, _game.startWork)("render:halo:bricks");
- haloCanvasCtx.globalAlpha = 0.05;
- gameState.bricks.forEach((color, index)=>{
- if (!color) return;
- if (shouldSkip(index)) return;
- const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
- drawFuzzyBall(haloCanvasCtx, color == "black" ? "#666666" : color, // Perf could really go down there because of the size of the halo
- Math.min(200, gameState.brickWidth * 1.5 * brightness) / haloScale, x / haloScale, y / haloScale);
- });
- (0, _game.startWork)("render:halo:particles");
- haloCanvasCtx.globalCompositeOperation = "screen";
- (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash, index)=>{
- if (shouldSkip(index)) return;
- const { x, y, time, color, size, duration } = flash;
- const elapsed = gameState.levelTime - time;
- haloCanvasCtx.globalAlpha = 0.1 * Math.min(1, 2 - elapsed / duration * 2);
- drawFuzzyBall(haloCanvasCtx, color, size * 3 * brightness / haloScale, x / haloScale, y / haloScale);
- });
- (0, _game.startWork)("render:halo:scale_up");
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "source-over";
- ctx.imageSmoothingQuality = "high";
- ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false;
- ctx.drawImage(haloCanvas, 0, 0, width, height);
- ctx.imageSmoothingEnabled = false;
- (0, _game.startWork)("render:halo:pattern");
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "multiply";
- if (level.svg && background.width && background.complete) {
- if (backgroundCanvas.title !== level.name) {
- backgroundCanvas.title = level.name;
- backgroundCanvas.width = gameState.canvasWidth;
- backgroundCanvas.height = gameState.canvasHeight;
- const bgctx = backgroundCanvas.getContext("2d");
- bgctx.globalCompositeOperation = "source-over";
- bgctx.fillStyle = level.color || "#000";
- bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
- if (gameState.perks.clairvoyant >= 3) {
- const pageSource = document.body.innerHTML.replace(/\s+/gi, "");
- const lineWidth = Math.ceil(gameState.canvasWidth / 15);
- const lines = Math.ceil(gameState.canvasHeight / 20);
- const chars = lineWidth * lines;
- let start = Math.ceil(Math.random() * (pageSource.length - chars));
- for(let i = 0; i < lines; i++){
- bgctx.fillStyle = "#FFFFFF";
- bgctx.font = "20px Courier";
- bgctx.fillText(pageSource.slice(start + i * lineWidth, start + (i + 1) * lineWidth), 0, i * 20, gameState.canvasWidth);
- }
- } else {
- const pattern = ctx.createPattern(background, "repeat");
- if (pattern) {
- bgctx.globalCompositeOperation = "screen";
- bgctx.fillStyle = pattern;
- bgctx.fillRect(0, 0, width, height);
- }
- }
- }
- ctx.globalCompositeOperation = "darken";
- ctx.drawImage(backgroundCanvas, 0, 0);
- } else {
- // Background not loaded yes
- ctx.fillStyle = "#000";
- ctx.fillRect(0, 0, width, height);
- }
- } else {
- (0, _game.startWork)("render:halo-basic");
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "source-over";
- ctx.fillStyle = level.color || "#000";
- ctx.fillRect(0, 0, width, height);
- (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash)=>{
- const { x, y, time, color, size, duration } = flash;
- const elapsed = gameState.levelTime - time;
- ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
- drawBall(ctx, color, size, x, y);
- });
- }
- (0, _game.startWork)("render:explosionshake");
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "source-over";
- const lastExplosionDelay = gameState.levelTime - gameState.lastExplosion + 5;
- const shaked = lastExplosionDelay < 200 && !(0, _options.isOptionOn)("basic") && // Otherwise, if you pause after an explosion, moving the mouses shakes the picture
- gameState.running;
- if (shaked) {
- const amplitude = (gameState.perks.bigger_explosions + 1) * 50 / lastExplosionDelay;
- ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude);
- }
- (0, _game.startWork)("render:coins");
- // Coins
- ctx.globalAlpha = 1;
- (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin)=>{
- const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin);
- const hollow = gameState.perks.metamorphosis && !coin.metamorphosisPoints;
- ctx.globalCompositeOperation = "source-over";
- drawCoin(ctx, hollow ? "transparent" : color, coin.size, coin.x, coin.y, // Red border around coins with asceticism
- hasCombo && gameState.perks.asceticism && "#FF0000" || // Gold coins
- // (color === "#ffd300" && "#ffd300") ||
- hollow && color || gameState.level.color, coin.a);
- });
- (0, _game.startWork)("render:ball shade");
- // Black shadow around balls
- ctx.globalCompositeOperation = "source-over";
- gameState.balls.forEach((ball)=>{
- ctx.globalAlpha = Math.min(0.8, (0, _gameStateMutators.liveCount)(gameState.coins) / 20) * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState));
- drawBall(ctx, level.color || "#000", gameState.ballSize * 6, ball.x, ball.y);
- });
- (0, _game.startWork)("render:bricks");
- ctx.globalCompositeOperation = "source-over";
- renderAllBricks();
- (0, _game.startWork)("render:lights");
- ctx.globalCompositeOperation = "screen";
- (0, _gameStateMutators.forEachLiveOne)(gameState.lights, (flash)=>{
- const { x, y, time, color, size, duration } = flash;
- const elapsed = gameState.levelTime - time;
- ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2) * 0.5;
- drawBrick(gameState, ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
- });
- (0, _game.startWork)("render:texts");
- ctx.globalCompositeOperation = "screen";
- (0, _gameStateMutators.forEachLiveOne)(gameState.texts, (flash)=>{
- const { x, y, time, color, size, duration } = flash;
- const elapsed = gameState.levelTime - time;
- ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
- ctx.globalCompositeOperation = "source-over";
- drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
- });
- (0, _game.startWork)("render:particles");
- (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (particle)=>{
- const { x, y, time, color, size, duration } = particle;
- const elapsed = gameState.levelTime - time;
- ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
- ctx.globalCompositeOperation = "screen";
- drawBall(ctx, color, size, x, y);
- });
- //
- (0, _game.startWork)("render:extra_life");
- if (gameState.perks.extra_life) {
- ctx.globalAlpha = gameState.balls.length > 1 ? 0.2 : 1;
- ctx.globalCompositeOperation = "source-over";
- ctx.fillStyle = gameState.puckColor;
- for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneWidthRoundedUp, 1);
- }
- (0, _game.startWork)("render:balls");
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "source-over";
- gameState.balls.forEach((ball)=>{
- const drawingColor = gameState.ballsColor;
- const ballAlpha = 1 - (0, _pureFunctions.ballTransparency)(ball, gameState);
- ctx.globalAlpha = ballAlpha;
- // The white border around is to distinguish colored balls from coins/bg
- drawBall(ctx, drawingColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor);
- if ((0, _gameUtils.telekinesisEffectRate)(gameState, ball) || (0, _gameUtils.yoyoEffectRate)(gameState, ball)) {
- ctx.beginPath();
- ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
- ctx.globalAlpha = (0, _pureFunctions.clamp)(Math.max((0, _gameUtils.telekinesisEffectRate)(gameState, ball), (0, _gameUtils.yoyoEffectRate)(gameState, ball)) * ballAlpha, 0, 1);
- ctx.strokeStyle = gameState.puckColor;
- ctx.bezierCurveTo(gameState.puckPosition, gameState.gameZoneHeight, gameState.puckPosition, ball.y, ball.x, ball.y);
- ctx.stroke();
- ctx.lineWidth = 2;
- ctx.setLineDash(emptyArray);
- }
- ctx.globalAlpha = 1;
- if (gameState.perks.clairvoyant && gameState.ballStickToPuck || gameState.perks.steering > 1 && !gameState.ballStickToPuck) {
- ctx.strokeStyle = gameState.ballsColor;
- ctx.beginPath();
- ctx.moveTo(ball.x, ball.y);
- ctx.lineTo(ball.x + ball.vx * 10, ball.y + ball.vy * 10);
- ctx.stroke();
- }
- });
- (0, _game.startWork)("render:puck");
- ctx.globalAlpha = (0, _gameUtils.isMovingWhilePassiveIncome)(gameState) ? 0.2 : 1;
- ctx.globalCompositeOperation = "source-over";
- drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight, 0, gameState.perks.concave_puck, gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1);
- (0, _game.startWork)("render:combotext");
- const spawns = (0, _pureFunctions.coinsBoostedCombo)(gameState);
- if (spawns > 1 && !(0, _gameUtils.isMovingWhilePassiveIncome)(gameState)) {
- ctx.globalCompositeOperation = "source-over";
- ctx.globalAlpha = 1;
- const comboText = spawns.toString();
- const comboTextWidth = comboText.length * gameState.puckHeight / 1.8;
- const totalWidth = comboTextWidth + gameState.coinSize * 2;
- const left = gameState.puckPosition - totalWidth / 2;
- ctx.globalAlpha = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState) ? 1 : 0.3;
- if (totalWidth < gameState.puckWidth) {
- drawText(ctx, comboText, "#000", gameState.puckHeight, left + gameState.coinSize * 1.5, gameState.gameZoneHeight - gameState.puckHeight / 2, true);
- ctx.globalAlpha = 1;
- drawCoin(ctx, "#ffd300", gameState.coinSize, left + gameState.coinSize / 2, gameState.gameZoneHeight - gameState.puckHeight / 2, "#ffd300", 0);
- } else drawText(ctx, comboTextWidth > gameState.puckWidth ? gameState.combo.toString() : comboText, "#000", comboTextWidth > gameState.puckWidth ? 12 : 20, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false);
- }
- (0, _game.startWork)("render:borders");
- // Borders
- ctx.globalCompositeOperation = "source-over";
- ctx.globalAlpha = 1;
- let redLeftSide = hasCombo && (gameState.perks.left_is_lava || gameState.perks.trampoline);
- let redRightSide = hasCombo && (gameState.perks.right_is_lava || gameState.perks.trampoline);
- let redTop = hasCombo && (gameState.perks.top_is_lava || gameState.perks.trampoline);
- if (gameState.offsetXRoundedDown) {
- // draw outside of gaming area to avoid capturing borders in recordings
- if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "#FFFFFF", (0, _gameUtils.zoneLeftBorderX)(gameState), 0, (0, _gameUtils.zoneLeftBorderX)(gameState), height, 1);
- if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "#FFFFFF", (0, _gameUtils.zoneRightBorderX)(gameState), 0, (0, _gameUtils.zoneRightBorderX)(gameState), height, 1);
- } else {
- if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "", 0, 0, 0, height, 1);
- if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "", width - 1, 0, width - 1, height, 1);
- }
- if (redTop && gameState.perks.top_is_lava < 2) drawStraightLine(ctx, gameState, "#FF0000", (0, _gameUtils.zoneLeftBorderX)(gameState), 1, (0, _gameUtils.zoneRightBorderX)(gameState), 1, 1);
- (0, _game.startWork)("render:bottom_line");
- ctx.globalAlpha = 1;
- const corner = (0, _gameUtils.getCornerOffset)(gameState);
- const bottomLineIsRed = hasCombo && gameState.perks.compound_interest;
- drawStraightLine(ctx, gameState, bottomLineIsRed && "#FF0000" || (0, _options.isOptionOn)("mobile-mode") && "#FFFFFF" || corner && "#FFFFFF" || "", gameState.offsetXRoundedDown - corner, gameState.gameZoneHeight - 1, width - gameState.offsetXRoundedDown + corner, gameState.gameZoneHeight - 1, bottomLineIsRed ? 1 : 0.5);
- (0, _game.startWork)("render:contrast");
- if (!(0, _options.isOptionOn)("basic") && (0, _options.isOptionOn)("contrast") && level.svg && level.color === "#000000") {
- ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false;
- if ((0, _options.isOptionOn)("probabilistic_lighting")) {
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "soft-light";
- } else {
- haloCanvasCtx.fillStyle = "#FFFFFF";
- haloCanvasCtx.globalAlpha = 0.25;
- haloCanvasCtx.globalCompositeOperation = "screen";
- haloCanvasCtx.fillRect(0, 0, haloCanvas.width, haloCanvas.height);
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "overlay";
- }
- ctx.drawImage(haloCanvas, 0, 0, width, height);
- ctx.imageSmoothingEnabled = false;
- }
- (0, _game.startWork)("render:text_under_puck");
- ctx.globalCompositeOperation = "source-over";
- ctx.globalAlpha = 1;
- if ((0, _options.isOptionOn)("mobile-mode") && gameState.startParams.computer_controlled) drawText(ctx, "breakout.lecaro.me?autoplay", gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2);
- if ((0, _options.isOptionOn)("mobile-mode") && !gameState.running) drawText(ctx, (0, _i18N.t)("play.mobile_press_to_play"), gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2);
- (0, _game.startWork)("render:timeout");
- if (gameState.winAt || gameState.startCountDown) {
- const remaining = gameState.startCountDown || Math.ceil((gameState.winAt - gameState.levelTime) / 1000);
- if (remaining > 0 && remaining < 5) {
- ctx.globalAlpha = 1;
- ctx.globalCompositeOperation = "destination-out";
- drawText(ctx, remaining.toString(), 'white', 65, gameState.canvasWidth / 2, gameState.canvasHeight / 2);
- ctx.globalCompositeOperation = "screen";
- ctx.globalAlpha = 1 / remaining;
- drawText(ctx, remaining.toString(), 'white', 60, gameState.canvasWidth / 2, gameState.canvasHeight / 2);
- }
- }
- ctx.globalAlpha = 1;
- (0, _game.startWork)("render:askForWakeLock");
- askForWakeLock(gameState);
- (0, _game.startWork)("render:resetTransform");
- if (shaked) ctx.resetTransform();
-}
-function drawStraightLine(ctx, gameState, mode, x1, y1, x2, y2, alpha = 1) {
- ctx.globalAlpha = alpha;
- if (!mode) return;
- x1 = Math.round(x1);
- y1 = Math.round(y1);
- x2 = Math.round(x2);
- y2 = Math.round(y2);
- if (mode == "#FF0000") {
- ctx.strokeStyle = "red";
- ctx.lineDashOffset = getDashOffset(gameState);
- ctx.lineWidth = 2;
- ctx.setLineDash(redBorderDash);
- ctx.beginPath();
- ctx.moveTo(x1, y1);
- ctx.lineTo(x2, y2);
- ctx.stroke();
- ctx.setLineDash(emptyArray);
- ctx.lineWidth = 1;
- } else {
- ctx.fillStyle = mode;
- ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.max(1, Math.abs(x1 - x2)), Math.max(1, Math.abs(y1 - y2)));
- }
- mode;
- ctx.globalAlpha = 1;
-}
-let cachedBricksRender = document.createElement("canvas");
-let cachedBricksRenderKey = "";
-function renderAllBricks() {
- ctx.globalAlpha = 1;
- const hasCombo = (0, _game.gameState).combo > (0, _gameStateMutators.baseCombo)((0, _game.gameState));
- const redBorderOnBricksWithWrongColor = hasCombo && (0, _game.gameState).perks.picky_eater && (0, _gameUtils.isPickyEatingPossible)((0, _game.gameState));
- const redRowReach = (0, _gameUtils.reachRedRowIndex)((0, _game.gameState));
- const { clairvoyant } = (0, _game.gameState).perks;
- let offset = getDashOffset((0, _game.gameState));
- if (!(redBorderOnBricksWithWrongColor || redRowReach !== -1 || (0, _game.gameState).perks.zen)) offset = 0;
- const clairVoyance = clairvoyant && (0, _game.gameState).brickHP.reduce((a, b)=>a + b, 0);
- const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redRowReach + "_" + redBorderOnBricksWithWrongColor + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color + "_" + clairVoyance + "_" + offset;
- if (newKey !== cachedBricksRenderKey) {
- cachedBricksRenderKey = newKey;
- cachedBricksRender.width = (0, _game.gameState).gameZoneWidth;
- cachedBricksRender.height = (0, _game.gameState).gameZoneWidth + 1;
- const canctx = cachedBricksRender.getContext("2d");
- canctx.clearRect(0, 0, (0, _game.gameState).gameZoneWidth, (0, _game.gameState).gameZoneWidth);
- canctx.resetTransform();
- canctx.translate(-(0, _game.gameState).offsetX, 0);
- // Bricks
- (0, _game.gameState).bricks.forEach((color, index)=>{
- const x = (0, _gameUtils.brickCenterX)((0, _game.gameState), index), y = (0, _gameUtils.brickCenterY)((0, _game.gameState), index);
- if (!color) return;
- let redBecauseOfReach = redRowReach === Math.floor(index / (0, _game.gameState).level.size);
- let redBorder = (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || hasCombo && (0, _game.gameState).perks.zen && color === "black" || redBecauseOfReach;
- canctx.globalCompositeOperation = "source-over";
- drawBrick((0, _game.gameState), canctx, color, x, y, redBorder ? offset : -1, clairvoyant >= 2);
- if ((0, _game.gameState).brickHP[index] > 1 && clairvoyant) {
- canctx.globalCompositeOperation = "source-over";
- drawText(canctx, (0, _game.gameState).brickHP[index].toString(), clairvoyant >= 2 ? color : (0, _game.gameState).level.color, (0, _game.gameState).puckHeight, x, y);
- }
- if (color === "black") {
- canctx.globalCompositeOperation = "source-over";
- drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y);
- }
- });
- }
- ctx.drawImage(cachedBricksRender, (0, _game.gameState).offsetX, 0);
-}
-let cachedGraphics = {};
-function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0, concave_puck, redBorderOffset) {
- const key = "puck" + color + "_" + puckWidth + "_" + puckHeight + "_" + concave_puck + "_" + redBorderOffset;
- if (!cachedGraphics[key]) {
- const can = document.createElement("canvas");
- can.width = puckWidth;
- can.height = puckHeight * 2;
- const canctx = can.getContext("2d");
- canctx.fillStyle = color;
- canctx.beginPath();
- canctx.moveTo(0, puckHeight * 2);
- if (concave_puck) {
- canctx.lineTo(0, puckHeight * 0.75);
- canctx.bezierCurveTo(puckWidth / 2, puckHeight * (2 + concave_puck) / 3, puckWidth / 2, puckHeight * (2 + concave_puck) / 3, puckWidth, puckHeight * 0.75);
- canctx.lineTo(puckWidth, puckHeight * 2);
- } else {
- canctx.lineTo(0, puckHeight * 1.25);
- canctx.bezierCurveTo(0, puckHeight * 0.75, puckWidth, puckHeight * 0.75, puckWidth, puckHeight * 1.25);
- canctx.lineTo(puckWidth, puckHeight * 2);
- }
- canctx.fill();
- if (redBorderOffset !== -1) {
- canctx.strokeStyle = "#FF0000";
- canctx.lineWidth = 4;
- canctx.setLineDash(redBorderDash);
- canctx.lineDashOffset = redBorderOffset;
- canctx.stroke();
- }
- cachedGraphics[key] = can;
- }
- ctx.drawImage(cachedGraphics[key], Math.round((0, _game.gameState).puckPosition - puckWidth / 2), (0, _game.gameState).gameZoneHeight - puckHeight * 2 + yOffset);
-}
-function drawBall(ctx, color, width, x, y, borderColor = "") {
- const key = "ball" + color + "_" + width + "_" + borderColor;
- const size = Math.round(width);
- if (!cachedGraphics[key]) {
- const can = document.createElement("canvas");
- can.width = size;
- can.height = size;
- const canctx = can.getContext("2d");
- canctx.beginPath();
- canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI);
- canctx.fillStyle = color;
- canctx.fill();
- if (borderColor) {
- canctx.lineWidth = 2;
- canctx.strokeStyle = borderColor;
- canctx.stroke();
- }
- cachedGraphics[key] = can;
- }
- ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
-}
-const angles = 32;
-function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) {
- const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles;
- const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "#ffd300" ? angle : "whatever");
- if (!cachedGraphics[key]) {
- const can = document.createElement("canvas");
- can.width = size;
- can.height = size;
- const canctx = can.getContext("2d");
- // coin
- canctx.beginPath();
- canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI);
- canctx.fillStyle = color;
- canctx.fill();
- canctx.strokeStyle = borderColor;
- if (borderColor == "#FF0000") {
- canctx.lineWidth = 2;
- canctx.setLineDash(redBorderDash);
- }
- if (color === "transparent") canctx.lineWidth = 2;
- canctx.stroke();
- if (color === "#ffd300") {
- // Fill in
- canctx.beginPath();
- canctx.arc(size / 2, size / 2, size / 2 * 0.6, 0, 2 * Math.PI);
- canctx.fillStyle = "rgba(255,255,255,0.5)";
- canctx.fill();
- canctx.translate(size / 2, size / 2);
- canctx.rotate(angle / 16);
- canctx.translate(-size / 2, -size / 2);
- canctx.globalCompositeOperation = "multiply";
- drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
- drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
- }
- cachedGraphics[key] = can;
- }
- ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
-}
-function drawFuzzyBall(ctx, color, width, x, y) {
- width = Math.max(width, 2);
- const key = "fuzzy-circle" + color + "_" + width;
- if (!color?.startsWith("#")) debugger;
- const size = Math.round(width * 3);
- if (!size || isNaN(size)) {
- debugger;
- return;
- }
- if (!cachedGraphics[key]) {
- const can = document.createElement("canvas");
- can.width = size;
- can.height = size;
- const canctx = can.getContext("2d");
- const gradient = canctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
- gradient.addColorStop(0, color);
- gradient.addColorStop(0.3, color + "88");
- gradient.addColorStop(0.6, color + "22");
- gradient.addColorStop(1, "transparent");
- canctx.fillStyle = gradient;
- canctx.fillRect(0, 0, size, size);
- cachedGraphics[key] = can;
- }
- ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
-}
-function drawBrick(gameState, ctx, color, x, y, offset = 0, borderOnly) {
- const tlx = Math.ceil(x - gameState.brickWidth / 2);
- const tly = Math.ceil(y - gameState.brickWidth / 2);
- const brx = Math.ceil(x + gameState.brickWidth / 2) - 1;
- const bry = Math.ceil(y + gameState.brickWidth / 2) - 1;
- const width = brx - tlx, height = bry - tly;
- const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + "_" + borderOnly + "_";
- if (!cachedGraphics[key]) {
- const can = document.createElement("canvas");
- can.width = width;
- can.height = height;
- const bord = 4;
- const cornerRadius = 2;
- const canctx = can.getContext("2d");
- canctx.fillStyle = color;
- canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray);
- canctx.lineDashOffset = offset;
- canctx.strokeStyle = offset !== -1 && "#FF000033" || color;
- canctx.lineJoin = "round";
- canctx.lineWidth = bord;
- roundRect(canctx, bord / 2, bord / 2, width - bord, height - bord, cornerRadius);
- if (!borderOnly) canctx.fill();
- canctx.stroke();
- cachedGraphics[key] = can;
- }
- ctx.drawImage(cachedGraphics[key], tlx, tly, width, height);
-// It's not easy to have a 1px gap between bricks without antialiasing
-}
-function roundRect(ctx, x, y, width, height, radius) {
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
-}
-function drawIMG(ctx, img, size, x, y) {
- const key = "svg" + img + "_" + size + "_" + img.complete;
- if (!cachedGraphics[key]) {
- const can = document.createElement("canvas");
- can.width = size;
- can.height = size;
- const canctx = can.getContext("2d");
- const ratio = size / Math.max(img.width, img.height);
- const w = img.width * ratio;
- const h = img.height * ratio;
- canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
- cachedGraphics[key] = can;
- }
- ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
-}
-function drawText(ctx, text, color, fontSize, x, y, left = false) {
- const key = "text" + text + "_" + color + "_" + fontSize + "_" + left;
- if (!cachedGraphics[key]) {
- const can = document.createElement("canvas");
- can.width = fontSize * text.length;
- can.height = fontSize;
- const canctx = can.getContext("2d");
- canctx.fillStyle = color;
- canctx.textAlign = left ? "left" : "center";
- canctx.textBaseline = "middle";
- canctx.font = fontSize + "px monospace";
- canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width);
- cachedGraphics[key] = can;
- }
- ctx.drawImage(cachedGraphics[key], left ? x : Math.round(x - cachedGraphics[key].width / 2), Math.round(y - cachedGraphics[key].height / 2));
-}
-const scoreDisplay = document.getElementById("score");
-const menuLabel = document.getElementById("menuLabel");
-const emptyArray = [];
-const redBorderDash = [
- 5,
- 5
-];
-function getDashOffset(gameState) {
- if ((0, _options.isOptionOn)("basic")) return 0;
- return Math.floor(gameState.levelTime % 500 / 500 * 10) % 10;
-}
-let wakeLock = null, wakeLockPending = false;
-function askForWakeLock(gameState) {
- if (gameState.startParams.computer_controlled && !wakeLock && !wakeLockPending) {
- wakeLockPending = true;
- try {
- navigator.wakeLock.request("screen").then((lock)=>{
- wakeLock = lock;
- wakeLockPending = false;
- lock.addEventListener("release", ()=>{
- // the wake lock has been released
- wakeLock = null;
- });
- });
- } catch (e) {
- console.warn("askForWakeLock error", e);
- }
- }
+if ("serviceWorker" in navigator && window.location.href.endsWith("/index.html?isPWA=true")) {
+ // @ts-ignore
+ const url = new URL(require("b04459cc43e56e8c"));
+ navigator.serviceWorker.register(url);
}
-},{"./gameStateMutators":"9ZeQl","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./game":"edeGs","./options":"d5NoS","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9ZeQl":[function(require,module,exports,__globalThis) {
+},{"b04459cc43e56e8c":"17ciJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"17ciJ":[function(require,module,exports,__globalThis) {
+module.exports = require("9c7c7951fd7c4db6").getBundleURL('arAGi') + "sw-b71.41cdff1b.js";
+
+},{"9c7c7951fd7c4db6":"lgJ39"}],"lgJ39":[function(require,module,exports,__globalThis) {
+"use strict";
+var bundleURL = {};
+function getBundleURLCached(id) {
+ var value = bundleURL[id];
+ if (!value) {
+ value = getBundleURL();
+ bundleURL[id] = value;
+ }
+ return value;
+}
+function getBundleURL() {
+ try {
+ throw new Error();
+ } catch (err) {
+ var matches = ('' + err.stack).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g);
+ if (matches) // The first two stack frames will be this function and getBundleURLCached.
+ // Use the 3rd one, which will be a runtime in the original bundle.
+ return getBaseURL(matches[2]);
+ }
+ return '/';
+}
+function getBaseURL(url) {
+ return ('' + url).replace(/^((?:https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/.+)\/[^/]+$/, '$1') + '/';
+}
+// TODO: Replace uses with `new URL(url).origin` when ie11 is no longer supported.
+function getOrigin(url) {
+ var matches = ('' + url).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^/]+/);
+ if (!matches) throw new Error('Origin not found');
+ return matches[0];
+}
+exports.getBundleURL = getBundleURLCached;
+exports.getBaseURL = getBaseURL;
+exports.getOrigin = getOrigin;
+
+},{}],"9ZeQl":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "setMousePos", ()=>setMousePos);
@@ -5187,7 +4592,7 @@ function ballTick(gameState, ball, frames) {
const d = Math.sqrt(ball.vy * ball.vy + ball.vx * ball.vx);
ball.vy = Math.sin(angle) * d;
ball.vx = Math.cos(angle) * d;
- if (Math.random() < frames && !(0, _options.isOptionOn)('basic')) makeParticle(gameState, ball.x, ball.y, -ball.vx / 10, -ball.vy / 10, '#6262EA', true, 8, 500);
+ if (Math.random() < frames && !(0, _options.isOptionOn)("basic")) makeParticle(gameState, ball.x, ball.y, -ball.vx / 10, -ball.vy / 10, "#6262EA", true, 8, 500);
}
}
// Bounces
@@ -5492,7 +4897,646 @@ function zenTick(gameState) {
}
}
-},{"./game_utils":"cEeac","./i18n/i18n":"eNPRm","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","./pure_functions":"6pQh7","./addToTotalScore":"ka4dG","./getLevelBackground":"7OIPf","./openUpgradesPicker":"2fQt0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"caCAf":[function(require,module,exports,__globalThis) {
+},{"./game_utils":"cEeac","./i18n/i18n":"eNPRm","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","./pure_functions":"6pQh7","./addToTotalScore":"ka4dG","./getLevelBackground":"7OIPf","./openUpgradesPicker":"2fQt0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9AS2t":[function(require,module,exports,__globalThis) {
+var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
+parcelHelpers.defineInteropFlag(exports);
+parcelHelpers.export(exports, "gameCanvas", ()=>gameCanvas);
+parcelHelpers.export(exports, "ctx", ()=>ctx);
+parcelHelpers.export(exports, "bombSVG", ()=>bombSVG);
+parcelHelpers.export(exports, "background", ()=>background);
+parcelHelpers.export(exports, "backgroundCanvas", ()=>backgroundCanvas);
+parcelHelpers.export(exports, "haloCanvas", ()=>haloCanvas);
+parcelHelpers.export(exports, "getHaloScale", ()=>getHaloScale);
+parcelHelpers.export(exports, "render", ()=>render);
+parcelHelpers.export(exports, "renderAllBricks", ()=>renderAllBricks);
+parcelHelpers.export(exports, "drawPuck", ()=>drawPuck);
+parcelHelpers.export(exports, "drawBall", ()=>drawBall);
+parcelHelpers.export(exports, "drawCoin", ()=>drawCoin);
+parcelHelpers.export(exports, "drawFuzzyBall", ()=>drawFuzzyBall);
+parcelHelpers.export(exports, "drawBrick", ()=>drawBrick);
+parcelHelpers.export(exports, "roundRect", ()=>roundRect);
+parcelHelpers.export(exports, "drawIMG", ()=>drawIMG);
+parcelHelpers.export(exports, "drawText", ()=>drawText);
+parcelHelpers.export(exports, "scoreDisplay", ()=>scoreDisplay);
+parcelHelpers.export(exports, "getDashOffset", ()=>getDashOffset);
+var _gameStateMutators = require("./gameStateMutators");
+var _gameUtils = require("./game_utils");
+var _i18N = require("./i18n/i18n");
+var _game = require("./game");
+var _options = require("./options");
+var _pureFunctions = require("./pure_functions");
+const gameCanvas = document.getElementById("game");
+const ctx = gameCanvas.getContext("2d", {
+ alpha: false
+});
+const bombSVG = document.createElement("img");
+bombSVG.src = "data:image/svg+xml;base64," + btoa(``);
+bombSVG.onload = ()=>(0, _game.gameState).needsRender = true;
+const background = document.createElement("img");
+background.onload = ()=>(0, _game.gameState).needsRender = true;
+const backgroundCanvas = document.createElement("canvas");
+const haloCanvas = document.createElement("canvas");
+const haloCanvasCtx = haloCanvas.getContext("2d", {
+ alpha: false
+});
+function getHaloScale() {
+ return 16 * ((0, _options.isOptionOn)("precise_lighting") ? 1 : 2);
+}
+let framesCounter = 0;
+function render(gameState) {
+ framesCounter++;
+ (0, _game.startWork)("render:init");
+ const level = (0, _gameUtils.currentLevelInfo)(gameState);
+ const hasCombo = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState);
+ const { width, height } = gameCanvas;
+ if (!width || !height) return;
+ if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = (0, _i18N.t)("play.current_lvl", {
+ level: gameState.currentLevel + 1,
+ max: (0, _gameUtils.max_levels)(gameState)
+ });
+ else menuLabel.innerText = (0, _i18N.t)("play.menu_label");
+ const catchRate = gameState.levelSpawnedCoins ? gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1) : // gameState.levelSpawnedCoins
+ 1;
+ (0, _game.startWork)("render:scoreDisplay");
+ scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") || gameState.startParams.computer_controlled ? `
+
+ ${0, _game.lastMeasuredFPS} FPS
+ /
+ ` : "") + ((0, _options.isOptionOn)("show_stats") ? `
+ (0, _pureFunctions.catchRateGood) / 100 && "good" || ""}" data-tooltip="${(0, _i18N.t)("play.stats.coins_catch_rate")}">
+ ${Math.floor(catchRate * 100)}%
+ /
+
+ ${Math.ceil(gameState.levelTime / 1000)}s
+ /
+
+ ${gameState.levelMisses} M
+ /
+ ` : "") + `$${gameState.score}`;
+ scoreDisplay.classList[gameState.startParams.computer_controlled ? "add" : "remove"]("computer_controlled");
+ scoreDisplay.classList[gameState.lastScoreIncrease > gameState.levelTime - 500 ? "add" : "remove"]("active");
+ // Clear
+ if (!(0, _options.isOptionOn)("basic") && level.svg && level.color === "#000000") {
+ const skipN = (0, _options.isOptionOn)("probabilistic_lighting") && (0, _gameStateMutators.liveCount)(gameState.coins) > 150 ? 3 : 0;
+ const shouldSkip = (index)=>skipN ? (framesCounter + index) % (skipN + 1) !== 0 : false;
+ const haloScale = getHaloScale();
+ (0, _game.startWork)("render:halo:clear");
+ haloCanvasCtx.globalCompositeOperation = "source-over";
+ haloCanvasCtx.globalAlpha = skipN ? 0.1 : 0.99;
+ haloCanvasCtx.fillStyle = level.color;
+ haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale);
+ const brightness = (0, _options.isOptionOn)("extra_bright") ? 3 : 1;
+ haloCanvasCtx.globalCompositeOperation = "lighten";
+ haloCanvasCtx.globalAlpha = 0.1 + 5 / ((0, _gameStateMutators.liveCount)(gameState.coins) + 10);
+ (0, _game.startWork)("render:halo:coins");
+ (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin, index)=>{
+ if (shouldSkip(index)) return;
+ const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin);
+ drawFuzzyBall(haloCanvasCtx, color, gameState.coinSize * 2 * brightness / haloScale, coin.x / haloScale, coin.y / haloScale);
+ });
+ (0, _game.startWork)("render:halo:balls");
+ gameState.balls.forEach((ball, index)=>{
+ if (shouldSkip(index)) return;
+ haloCanvasCtx.globalAlpha = 0.3 * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState));
+ drawFuzzyBall(haloCanvasCtx, gameState.ballsColor, gameState.ballSize * 2 * brightness / haloScale, ball.x / haloScale, ball.y / haloScale);
+ });
+ (0, _game.startWork)("render:halo:bricks");
+ haloCanvasCtx.globalAlpha = 0.05;
+ gameState.bricks.forEach((color, index)=>{
+ if (!color) return;
+ if (shouldSkip(index)) return;
+ const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
+ drawFuzzyBall(haloCanvasCtx, color == "black" ? "#666666" : color, // Perf could really go down there because of the size of the halo
+ Math.min(200, gameState.brickWidth * 1.5 * brightness) / haloScale, x / haloScale, y / haloScale);
+ });
+ (0, _game.startWork)("render:halo:particles");
+ haloCanvasCtx.globalCompositeOperation = "screen";
+ (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash, index)=>{
+ if (shouldSkip(index)) return;
+ const { x, y, time, color, size, duration } = flash;
+ const elapsed = gameState.levelTime - time;
+ haloCanvasCtx.globalAlpha = 0.1 * Math.min(1, 2 - elapsed / duration * 2);
+ drawFuzzyBall(haloCanvasCtx, color, size * 3 * brightness / haloScale, x / haloScale, y / haloScale);
+ });
+ (0, _game.startWork)("render:halo:scale_up");
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "source-over";
+ ctx.imageSmoothingQuality = "high";
+ ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false;
+ ctx.drawImage(haloCanvas, 0, 0, width, height);
+ ctx.imageSmoothingEnabled = false;
+ (0, _game.startWork)("render:halo:pattern");
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "multiply";
+ if (level.svg && background.width && background.complete) {
+ if (backgroundCanvas.title !== level.name) {
+ backgroundCanvas.title = level.name;
+ backgroundCanvas.width = gameState.canvasWidth;
+ backgroundCanvas.height = gameState.canvasHeight;
+ const bgctx = backgroundCanvas.getContext("2d");
+ bgctx.globalCompositeOperation = "source-over";
+ bgctx.fillStyle = level.color || "#000";
+ bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
+ if (gameState.perks.clairvoyant >= 3) {
+ const pageSource = document.body.innerHTML.replace(/\s+/gi, "");
+ const lineWidth = Math.ceil(gameState.canvasWidth / 15);
+ const lines = Math.ceil(gameState.canvasHeight / 20);
+ const chars = lineWidth * lines;
+ let start = Math.ceil(Math.random() * (pageSource.length - chars));
+ for(let i = 0; i < lines; i++){
+ bgctx.fillStyle = "#FFFFFF";
+ bgctx.font = "20px Courier";
+ bgctx.fillText(pageSource.slice(start + i * lineWidth, start + (i + 1) * lineWidth), 0, i * 20, gameState.canvasWidth);
+ }
+ } else {
+ const pattern = ctx.createPattern(background, "repeat");
+ if (pattern) {
+ bgctx.globalCompositeOperation = "screen";
+ bgctx.fillStyle = pattern;
+ bgctx.fillRect(0, 0, width, height);
+ }
+ }
+ }
+ ctx.globalCompositeOperation = "darken";
+ ctx.drawImage(backgroundCanvas, 0, 0);
+ } else {
+ // Background not loaded yes
+ ctx.fillStyle = "#000";
+ ctx.fillRect(0, 0, width, height);
+ }
+ } else {
+ (0, _game.startWork)("render:halo-basic");
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "source-over";
+ ctx.fillStyle = level.color || "#000";
+ ctx.fillRect(0, 0, width, height);
+ (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash)=>{
+ const { x, y, time, color, size, duration } = flash;
+ const elapsed = gameState.levelTime - time;
+ ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
+ drawBall(ctx, color, size, x, y);
+ });
+ }
+ (0, _game.startWork)("render:explosionshake");
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "source-over";
+ const lastExplosionDelay = gameState.levelTime - gameState.lastExplosion + 5;
+ const shaked = lastExplosionDelay < 200 && !(0, _options.isOptionOn)("basic") && // Otherwise, if you pause after an explosion, moving the mouses shakes the picture
+ gameState.running;
+ if (shaked) {
+ const amplitude = (gameState.perks.bigger_explosions + 1) * 50 / lastExplosionDelay;
+ ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude);
+ }
+ (0, _game.startWork)("render:coins");
+ // Coins
+ ctx.globalAlpha = 1;
+ (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin)=>{
+ const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin);
+ const hollow = gameState.perks.metamorphosis && !coin.metamorphosisPoints;
+ ctx.globalCompositeOperation = "source-over";
+ drawCoin(ctx, hollow ? "transparent" : color, coin.size, coin.x, coin.y, // Red border around coins with asceticism
+ hasCombo && gameState.perks.asceticism && "#FF0000" || // Gold coins
+ // (color === "#ffd300" && "#ffd300") ||
+ hollow && color || gameState.level.color, coin.a);
+ });
+ (0, _game.startWork)("render:ball shade");
+ // Black shadow around balls
+ ctx.globalCompositeOperation = "source-over";
+ gameState.balls.forEach((ball)=>{
+ ctx.globalAlpha = Math.min(0.8, (0, _gameStateMutators.liveCount)(gameState.coins) / 20) * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState));
+ drawBall(ctx, level.color || "#000", gameState.ballSize * 6, ball.x, ball.y);
+ });
+ (0, _game.startWork)("render:bricks");
+ ctx.globalCompositeOperation = "source-over";
+ renderAllBricks();
+ (0, _game.startWork)("render:lights");
+ ctx.globalCompositeOperation = "screen";
+ (0, _gameStateMutators.forEachLiveOne)(gameState.lights, (flash)=>{
+ const { x, y, time, color, size, duration } = flash;
+ const elapsed = gameState.levelTime - time;
+ ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2) * 0.5;
+ drawBrick(gameState, ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
+ });
+ (0, _game.startWork)("render:texts");
+ ctx.globalCompositeOperation = "screen";
+ (0, _gameStateMutators.forEachLiveOne)(gameState.texts, (flash)=>{
+ const { x, y, time, color, size, duration } = flash;
+ const elapsed = gameState.levelTime - time;
+ ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
+ ctx.globalCompositeOperation = "source-over";
+ drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
+ });
+ (0, _game.startWork)("render:particles");
+ (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (particle)=>{
+ const { x, y, time, color, size, duration } = particle;
+ const elapsed = gameState.levelTime - time;
+ ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
+ ctx.globalCompositeOperation = "screen";
+ drawBall(ctx, color, size, x, y);
+ });
+ //
+ (0, _game.startWork)("render:extra_life");
+ if (gameState.perks.extra_life) {
+ ctx.globalAlpha = gameState.balls.length > 1 ? 0.2 : 1;
+ ctx.globalCompositeOperation = "source-over";
+ ctx.fillStyle = gameState.puckColor;
+ for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneWidthRoundedUp, 1);
+ }
+ (0, _game.startWork)("render:balls");
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "source-over";
+ gameState.balls.forEach((ball)=>{
+ const drawingColor = gameState.ballsColor;
+ const ballAlpha = 1 - (0, _pureFunctions.ballTransparency)(ball, gameState);
+ ctx.globalAlpha = ballAlpha;
+ // The white border around is to distinguish colored balls from coins/bg
+ drawBall(ctx, drawingColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor);
+ if ((0, _gameUtils.telekinesisEffectRate)(gameState, ball) || (0, _gameUtils.yoyoEffectRate)(gameState, ball)) {
+ ctx.beginPath();
+ ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
+ ctx.globalAlpha = (0, _pureFunctions.clamp)(Math.max((0, _gameUtils.telekinesisEffectRate)(gameState, ball), (0, _gameUtils.yoyoEffectRate)(gameState, ball)) * ballAlpha, 0, 1);
+ ctx.strokeStyle = gameState.puckColor;
+ ctx.bezierCurveTo(gameState.puckPosition, gameState.gameZoneHeight, gameState.puckPosition, ball.y, ball.x, ball.y);
+ ctx.stroke();
+ ctx.lineWidth = 2;
+ ctx.setLineDash(emptyArray);
+ }
+ ctx.globalAlpha = 1;
+ if (gameState.perks.clairvoyant && gameState.ballStickToPuck || gameState.perks.steering > 1 && !gameState.ballStickToPuck) {
+ ctx.strokeStyle = gameState.ballsColor;
+ ctx.beginPath();
+ ctx.moveTo(ball.x, ball.y);
+ ctx.lineTo(ball.x + ball.vx * 10, ball.y + ball.vy * 10);
+ ctx.stroke();
+ }
+ });
+ (0, _game.startWork)("render:puck");
+ ctx.globalAlpha = (0, _gameUtils.isMovingWhilePassiveIncome)(gameState) ? 0.2 : 1;
+ ctx.globalCompositeOperation = "source-over";
+ drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight, 0, gameState.perks.concave_puck, gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1);
+ (0, _game.startWork)("render:combotext");
+ const spawns = (0, _pureFunctions.coinsBoostedCombo)(gameState);
+ if (spawns > 1 && !(0, _gameUtils.isMovingWhilePassiveIncome)(gameState)) {
+ ctx.globalCompositeOperation = "source-over";
+ ctx.globalAlpha = 1;
+ const comboText = spawns.toString();
+ const comboTextWidth = comboText.length * gameState.puckHeight / 1.8;
+ const totalWidth = comboTextWidth + gameState.coinSize * 2;
+ const left = gameState.puckPosition - totalWidth / 2;
+ ctx.globalAlpha = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState) ? 1 : 0.3;
+ if (totalWidth < gameState.puckWidth) {
+ drawText(ctx, comboText, "#000", gameState.puckHeight, left + gameState.coinSize * 1.5, gameState.gameZoneHeight - gameState.puckHeight / 2, true);
+ ctx.globalAlpha = 1;
+ drawCoin(ctx, "#ffd300", gameState.coinSize, left + gameState.coinSize / 2, gameState.gameZoneHeight - gameState.puckHeight / 2, "#ffd300", 0);
+ } else drawText(ctx, comboTextWidth > gameState.puckWidth ? gameState.combo.toString() : comboText, "#000", comboTextWidth > gameState.puckWidth ? 12 : 20, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false);
+ }
+ (0, _game.startWork)("render:borders");
+ // Borders
+ ctx.globalCompositeOperation = "source-over";
+ ctx.globalAlpha = 1;
+ let redLeftSide = hasCombo && (gameState.perks.left_is_lava || gameState.perks.trampoline);
+ let redRightSide = hasCombo && (gameState.perks.right_is_lava || gameState.perks.trampoline);
+ let redTop = hasCombo && (gameState.perks.top_is_lava || gameState.perks.trampoline);
+ if (gameState.offsetXRoundedDown) {
+ // draw outside of gaming area to avoid capturing borders in recordings
+ if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "#FFFFFF", (0, _gameUtils.zoneLeftBorderX)(gameState), 0, (0, _gameUtils.zoneLeftBorderX)(gameState), height, 1);
+ if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "#FFFFFF", (0, _gameUtils.zoneRightBorderX)(gameState), 0, (0, _gameUtils.zoneRightBorderX)(gameState), height, 1);
+ } else {
+ if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "", 0, 0, 0, height, 1);
+ if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "", width - 1, 0, width - 1, height, 1);
+ }
+ if (redTop && gameState.perks.top_is_lava < 2) drawStraightLine(ctx, gameState, "#FF0000", (0, _gameUtils.zoneLeftBorderX)(gameState), 1, (0, _gameUtils.zoneRightBorderX)(gameState), 1, 1);
+ (0, _game.startWork)("render:bottom_line");
+ ctx.globalAlpha = 1;
+ const corner = (0, _gameUtils.getCornerOffset)(gameState);
+ const bottomLineIsRed = hasCombo && gameState.perks.compound_interest;
+ drawStraightLine(ctx, gameState, bottomLineIsRed && "#FF0000" || (0, _options.isOptionOn)("mobile-mode") && "#FFFFFF" || corner && "#FFFFFF" || "", gameState.offsetXRoundedDown - corner, gameState.gameZoneHeight - 1, width - gameState.offsetXRoundedDown + corner, gameState.gameZoneHeight - 1, bottomLineIsRed ? 1 : 0.5);
+ (0, _game.startWork)("render:contrast");
+ if (!(0, _options.isOptionOn)("basic") && (0, _options.isOptionOn)("contrast") && level.svg && level.color === "#000000") {
+ ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false;
+ if ((0, _options.isOptionOn)("probabilistic_lighting")) {
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "soft-light";
+ } else {
+ haloCanvasCtx.fillStyle = "#FFFFFF";
+ haloCanvasCtx.globalAlpha = 0.25;
+ haloCanvasCtx.globalCompositeOperation = "screen";
+ haloCanvasCtx.fillRect(0, 0, haloCanvas.width, haloCanvas.height);
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "overlay";
+ }
+ ctx.drawImage(haloCanvas, 0, 0, width, height);
+ ctx.imageSmoothingEnabled = false;
+ }
+ (0, _game.startWork)("render:text_under_puck");
+ ctx.globalCompositeOperation = "source-over";
+ ctx.globalAlpha = 1;
+ if ((0, _options.isOptionOn)("mobile-mode") && gameState.startParams.computer_controlled) drawText(ctx, "breakout.lecaro.me?autoplay", gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2);
+ if ((0, _options.isOptionOn)("mobile-mode") && !gameState.running) drawText(ctx, (0, _i18N.t)("play.mobile_press_to_play"), gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2);
+ (0, _game.startWork)("render:timeout");
+ if (gameState.winAt || gameState.startCountDown) {
+ const remaining = gameState.startCountDown || Math.ceil((gameState.winAt - gameState.levelTime) / 1000);
+ if (remaining > 0 && remaining < 5) {
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "destination-out";
+ drawText(ctx, remaining.toString(), "white", 65, gameState.canvasWidth / 2, gameState.canvasHeight / 2);
+ ctx.globalCompositeOperation = "screen";
+ ctx.globalAlpha = 1 / remaining;
+ drawText(ctx, remaining.toString(), "white", 60, gameState.canvasWidth / 2, gameState.canvasHeight / 2);
+ }
+ }
+ ctx.globalAlpha = 1;
+ (0, _game.startWork)("render:askForWakeLock");
+ askForWakeLock(gameState);
+ (0, _game.startWork)("render:resetTransform");
+ if (shaked) ctx.resetTransform();
+}
+function drawStraightLine(ctx, gameState, mode, x1, y1, x2, y2, alpha = 1) {
+ ctx.globalAlpha = alpha;
+ if (!mode) return;
+ x1 = Math.round(x1);
+ y1 = Math.round(y1);
+ x2 = Math.round(x2);
+ y2 = Math.round(y2);
+ if (mode == "#FF0000") {
+ ctx.strokeStyle = "red";
+ ctx.lineDashOffset = getDashOffset(gameState);
+ ctx.lineWidth = 2;
+ ctx.setLineDash(redBorderDash);
+ ctx.beginPath();
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.stroke();
+ ctx.setLineDash(emptyArray);
+ ctx.lineWidth = 1;
+ } else {
+ ctx.fillStyle = mode;
+ ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.max(1, Math.abs(x1 - x2)), Math.max(1, Math.abs(y1 - y2)));
+ }
+ mode;
+ ctx.globalAlpha = 1;
+}
+let cachedBricksRender = document.createElement("canvas");
+let cachedBricksRenderKey = "";
+function renderAllBricks() {
+ ctx.globalAlpha = 1;
+ const hasCombo = (0, _game.gameState).combo > (0, _gameStateMutators.baseCombo)((0, _game.gameState));
+ const redBorderOnBricksWithWrongColor = hasCombo && (0, _game.gameState).perks.picky_eater && (0, _gameUtils.isPickyEatingPossible)((0, _game.gameState));
+ const redRowReach = (0, _gameUtils.reachRedRowIndex)((0, _game.gameState));
+ const { clairvoyant } = (0, _game.gameState).perks;
+ let offset = getDashOffset((0, _game.gameState));
+ if (!(redBorderOnBricksWithWrongColor || redRowReach !== -1 || (0, _game.gameState).perks.zen)) offset = 0;
+ const clairVoyance = clairvoyant && (0, _game.gameState).brickHP.reduce((a, b)=>a + b, 0);
+ const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redRowReach + "_" + redBorderOnBricksWithWrongColor + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color + "_" + clairVoyance + "_" + offset;
+ if (newKey !== cachedBricksRenderKey) {
+ cachedBricksRenderKey = newKey;
+ cachedBricksRender.width = (0, _game.gameState).gameZoneWidth;
+ cachedBricksRender.height = (0, _game.gameState).gameZoneWidth + 1;
+ const canctx = cachedBricksRender.getContext("2d");
+ canctx.clearRect(0, 0, (0, _game.gameState).gameZoneWidth, (0, _game.gameState).gameZoneWidth);
+ canctx.resetTransform();
+ canctx.translate(-(0, _game.gameState).offsetX, 0);
+ // Bricks
+ (0, _game.gameState).bricks.forEach((color, index)=>{
+ const x = (0, _gameUtils.brickCenterX)((0, _game.gameState), index), y = (0, _gameUtils.brickCenterY)((0, _game.gameState), index);
+ if (!color) return;
+ let redBecauseOfReach = redRowReach === Math.floor(index / (0, _game.gameState).level.size);
+ let redBorder = (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || hasCombo && (0, _game.gameState).perks.zen && color === "black" || redBecauseOfReach;
+ canctx.globalCompositeOperation = "source-over";
+ drawBrick((0, _game.gameState), canctx, color, x, y, redBorder ? offset : -1, clairvoyant >= 2);
+ if ((0, _game.gameState).brickHP[index] > 1 && clairvoyant) {
+ canctx.globalCompositeOperation = "source-over";
+ drawText(canctx, (0, _game.gameState).brickHP[index].toString(), clairvoyant >= 2 ? color : (0, _game.gameState).level.color, (0, _game.gameState).puckHeight, x, y);
+ }
+ if (color === "black") {
+ canctx.globalCompositeOperation = "source-over";
+ drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y);
+ }
+ });
+ }
+ ctx.drawImage(cachedBricksRender, (0, _game.gameState).offsetX, 0);
+}
+let cachedGraphics = {};
+function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0, concave_puck, redBorderOffset) {
+ const key = "puck" + color + "_" + puckWidth + "_" + puckHeight + "_" + concave_puck + "_" + redBorderOffset;
+ if (!cachedGraphics[key]) {
+ const can = document.createElement("canvas");
+ can.width = puckWidth;
+ can.height = puckHeight * 2;
+ const canctx = can.getContext("2d");
+ canctx.fillStyle = color;
+ canctx.beginPath();
+ canctx.moveTo(0, puckHeight * 2);
+ if (concave_puck) {
+ canctx.lineTo(0, puckHeight * 0.75);
+ canctx.bezierCurveTo(puckWidth / 2, puckHeight * (2 + concave_puck) / 3, puckWidth / 2, puckHeight * (2 + concave_puck) / 3, puckWidth, puckHeight * 0.75);
+ canctx.lineTo(puckWidth, puckHeight * 2);
+ } else {
+ canctx.lineTo(0, puckHeight * 1.25);
+ canctx.bezierCurveTo(0, puckHeight * 0.75, puckWidth, puckHeight * 0.75, puckWidth, puckHeight * 1.25);
+ canctx.lineTo(puckWidth, puckHeight * 2);
+ }
+ canctx.fill();
+ if (redBorderOffset !== -1) {
+ canctx.strokeStyle = "#FF0000";
+ canctx.lineWidth = 4;
+ canctx.setLineDash(redBorderDash);
+ canctx.lineDashOffset = redBorderOffset;
+ canctx.stroke();
+ }
+ cachedGraphics[key] = can;
+ }
+ ctx.drawImage(cachedGraphics[key], Math.round((0, _game.gameState).puckPosition - puckWidth / 2), (0, _game.gameState).gameZoneHeight - puckHeight * 2 + yOffset);
+}
+function drawBall(ctx, color, width, x, y, borderColor = "") {
+ const key = "ball" + color + "_" + width + "_" + borderColor;
+ const size = Math.round(width);
+ if (!cachedGraphics[key]) {
+ const can = document.createElement("canvas");
+ can.width = size;
+ can.height = size;
+ const canctx = can.getContext("2d");
+ canctx.beginPath();
+ canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI);
+ canctx.fillStyle = color;
+ canctx.fill();
+ if (borderColor) {
+ canctx.lineWidth = 2;
+ canctx.strokeStyle = borderColor;
+ canctx.stroke();
+ }
+ cachedGraphics[key] = can;
+ }
+ ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
+}
+const angles = 32;
+function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) {
+ const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles;
+ const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "#ffd300" ? angle : "whatever");
+ if (!cachedGraphics[key]) {
+ const can = document.createElement("canvas");
+ can.width = size;
+ can.height = size;
+ const canctx = can.getContext("2d");
+ // coin
+ canctx.beginPath();
+ canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI);
+ canctx.fillStyle = color;
+ canctx.fill();
+ canctx.strokeStyle = borderColor;
+ if (borderColor == "#FF0000") {
+ canctx.lineWidth = 2;
+ canctx.setLineDash(redBorderDash);
+ }
+ if (color === "transparent") canctx.lineWidth = 2;
+ canctx.stroke();
+ if (color === "#ffd300") {
+ // Fill in
+ canctx.beginPath();
+ canctx.arc(size / 2, size / 2, size / 2 * 0.6, 0, 2 * Math.PI);
+ canctx.fillStyle = "rgba(255,255,255,0.5)";
+ canctx.fill();
+ canctx.translate(size / 2, size / 2);
+ canctx.rotate(angle / 16);
+ canctx.translate(-size / 2, -size / 2);
+ canctx.globalCompositeOperation = "multiply";
+ drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
+ drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
+ }
+ cachedGraphics[key] = can;
+ }
+ ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
+}
+function drawFuzzyBall(ctx, color, width, x, y) {
+ width = Math.max(width, 2);
+ const key = "fuzzy-circle" + color + "_" + width;
+ if (!color?.startsWith("#")) debugger;
+ const size = Math.round(width * 3);
+ if (!size || isNaN(size)) {
+ debugger;
+ return;
+ }
+ if (!cachedGraphics[key]) {
+ const can = document.createElement("canvas");
+ can.width = size;
+ can.height = size;
+ const canctx = can.getContext("2d");
+ const gradient = canctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
+ gradient.addColorStop(0, color);
+ gradient.addColorStop(0.3, color + "88");
+ gradient.addColorStop(0.6, color + "22");
+ gradient.addColorStop(1, "transparent");
+ canctx.fillStyle = gradient;
+ canctx.fillRect(0, 0, size, size);
+ cachedGraphics[key] = can;
+ }
+ ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
+}
+function drawBrick(gameState, ctx, color, x, y, offset = 0, borderOnly) {
+ const tlx = Math.ceil(x - gameState.brickWidth / 2);
+ const tly = Math.ceil(y - gameState.brickWidth / 2);
+ const brx = Math.ceil(x + gameState.brickWidth / 2) - 1;
+ const bry = Math.ceil(y + gameState.brickWidth / 2) - 1;
+ const width = brx - tlx, height = bry - tly;
+ const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + "_" + borderOnly + "_";
+ if (!cachedGraphics[key]) {
+ const can = document.createElement("canvas");
+ can.width = width;
+ can.height = height;
+ const bord = 4;
+ const cornerRadius = 2;
+ const canctx = can.getContext("2d");
+ canctx.fillStyle = color;
+ canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray);
+ canctx.lineDashOffset = offset;
+ canctx.strokeStyle = offset !== -1 && "#FF000033" || color;
+ canctx.lineJoin = "round";
+ canctx.lineWidth = bord;
+ roundRect(canctx, bord / 2, bord / 2, width - bord, height - bord, cornerRadius);
+ if (!borderOnly) canctx.fill();
+ canctx.stroke();
+ cachedGraphics[key] = can;
+ }
+ ctx.drawImage(cachedGraphics[key], tlx, tly, width, height);
+// It's not easy to have a 1px gap between bricks without antialiasing
+}
+function roundRect(ctx, x, y, width, height, radius) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+}
+function drawIMG(ctx, img, size, x, y) {
+ const key = "svg" + img + "_" + size + "_" + img.complete;
+ if (!cachedGraphics[key]) {
+ const can = document.createElement("canvas");
+ can.width = size;
+ can.height = size;
+ const canctx = can.getContext("2d");
+ const ratio = size / Math.max(img.width, img.height);
+ const w = img.width * ratio;
+ const h = img.height * ratio;
+ canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
+ cachedGraphics[key] = can;
+ }
+ ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
+}
+function drawText(ctx, text, color, fontSize, x, y, left = false) {
+ const key = "text" + text + "_" + color + "_" + fontSize + "_" + left;
+ if (!cachedGraphics[key]) {
+ const can = document.createElement("canvas");
+ can.width = fontSize * text.length;
+ can.height = fontSize;
+ const canctx = can.getContext("2d");
+ canctx.fillStyle = color;
+ canctx.textAlign = left ? "left" : "center";
+ canctx.textBaseline = "middle";
+ canctx.font = fontSize + "px monospace";
+ canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width);
+ cachedGraphics[key] = can;
+ }
+ ctx.drawImage(cachedGraphics[key], left ? x : Math.round(x - cachedGraphics[key].width / 2), Math.round(y - cachedGraphics[key].height / 2));
+}
+const scoreDisplay = document.getElementById("score");
+const menuLabel = document.getElementById("menuLabel");
+const emptyArray = [];
+const redBorderDash = [
+ 5,
+ 5
+];
+function getDashOffset(gameState) {
+ if ((0, _options.isOptionOn)("basic")) return 0;
+ return Math.floor(gameState.levelTime % 500 / 500 * 10) % 10;
+}
+let wakeLock = null, wakeLockPending = false;
+function askForWakeLock(gameState) {
+ if (gameState.startParams.computer_controlled && !wakeLock && !wakeLockPending) {
+ wakeLockPending = true;
+ try {
+ navigator.wakeLock.request("screen").then((lock)=>{
+ wakeLock = lock;
+ wakeLockPending = false;
+ lock.addEventListener("release", ()=>{
+ // the wake lock has been released
+ wakeLock = null;
+ });
+ });
+ } catch (e) {
+ console.warn("askForWakeLock error", e);
+ }
+ }
+}
+
+},{"./gameStateMutators":"9ZeQl","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./game":"edeGs","./options":"d5NoS","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"caCAf":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "addToTotalPlayTime", ()=>addToTotalPlayTime);
@@ -6706,54 +6750,7 @@ function getNearestUnlockHTML(gameState) {
`;
}
-},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./game_utils":"cEeac","./gameOver":"caCAf","./game":"edeGs","./loadGameData":"l1B4x","./pure_functions":"6pQh7","./settings":"5blfu","./get_level_unlock_condition":"a0fq0","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) {
-var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
-parcelHelpers.defineInteropFlag(exports);
-if ("serviceWorker" in navigator && window.location.href.endsWith("/index.html?isPWA=true")) {
- // @ts-ignore
- const url = new URL(require("b04459cc43e56e8c"));
- navigator.serviceWorker.register(url);
-}
-
-},{"b04459cc43e56e8c":"17ciJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"17ciJ":[function(require,module,exports,__globalThis) {
-module.exports = require("9c7c7951fd7c4db6").getBundleURL('arAGi') + "sw-b71.41cdff1b.js";
-
-},{"9c7c7951fd7c4db6":"lgJ39"}],"lgJ39":[function(require,module,exports,__globalThis) {
-"use strict";
-var bundleURL = {};
-function getBundleURLCached(id) {
- var value = bundleURL[id];
- if (!value) {
- value = getBundleURL();
- bundleURL[id] = value;
- }
- return value;
-}
-function getBundleURL() {
- try {
- throw new Error();
- } catch (err) {
- var matches = ('' + err.stack).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g);
- if (matches) // The first two stack frames will be this function and getBundleURLCached.
- // Use the 3rd one, which will be a runtime in the original bundle.
- return getBaseURL(matches[2]);
- }
- return '/';
-}
-function getBaseURL(url) {
- return ('' + url).replace(/^((?:https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/.+)\/[^/]+$/, '$1') + '/';
-}
-// TODO: Replace uses with `new URL(url).origin` when ie11 is no longer supported.
-function getOrigin(url) {
- var matches = ('' + url).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^/]+/);
- if (!matches) throw new Error('Origin not found');
- return matches[0];
-}
-exports.getBundleURL = getBundleURLCached;
-exports.getBaseURL = getBaseURL;
-exports.getOrigin = getOrigin;
-
-},{}],"aQN6X":[function(require,module,exports,__globalThis) {
+},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./game_utils":"cEeac","./gameOver":"caCAf","./game":"edeGs","./loadGameData":"l1B4x","./pure_functions":"6pQh7","./settings":"5blfu","./get_level_unlock_condition":"a0fq0","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"aQN6X":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getRunLevels", ()=>getRunLevels);
@@ -6981,8 +6978,8 @@ function helpMenuEntry() {
levelTimeGood: (0, _pureFunctions.levelTimeGood),
missesBest: (0, _pureFunctions.missesBest),
missesGood: (0, _pureFunctions.missesGood),
- wallBouncedBest: (0, _pureFunctions.wallBouncedBest),
- wallBouncedGood: (0, _pureFunctions.wallBouncedGood)
+ wallBouncedBest,
+ wallBouncedGood
})),
(0, _pureFunctions.miniMarkDown)((0, _i18N.t)("help.upgrades")),
...(0, _loadGameData.upgrades).map((u)=>`
diff --git a/src/PWA/sw-b71.js b/src/PWA/sw-b71.js
index 782cd47..aa934d2 100644
--- a/src/PWA/sw-b71.js
+++ b/src/PWA/sw-b71.js
@@ -1,5 +1,5 @@
// The version of the cache.
-const VERSION = "29104759";
+const VERSION = "29104940";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;
diff --git a/src/data/version.json b/src/data/version.json
index 3d24b63..1983246 100644
--- a/src/data/version.json
+++ b/src/data/version.json
@@ -1 +1 @@
-"29104759"
+"29104940"
diff --git a/src/game.less b/src/game.less
index b7e01fc..0cecfc4 100644
--- a/src/game.less
+++ b/src/game.less
@@ -8,7 +8,6 @@
box-sizing: border-box;
}
-
body {
margin: 0;
padding: 0;
@@ -642,7 +641,6 @@ h2.histogram-title strong {
opacity: 0.8;
transform: none;
}
-
}
.gridEdit > div > span,
@@ -732,24 +730,24 @@ h2.histogram-title strong {
color: #8a8a8a;
}
-@palette_B:black;
-@palette_W:#FFFFFF;
-@palette_g:#231f20;
-@palette_y:#FFD300;
-@palette_b:#6262EA;
-@palette_t:#5DA3EA;
-@palette_s:#E67070;
-@palette_r:#e32119;
-@palette_R:#ab0c0c;
-@palette_c:#59EEA3;
-@palette_G:#A1F051;
-@palette_v:#A664E8;
-@palette_p:#E869E8;
-@palette_a:#5BECEC;
-@palette_C:#53EE53;
-@palette_S:#F44848;
-@palette_P:#E66BA8;
-@palette_O:#F29E4A;
-@palette_k:#618227;
-@palette_e:#e1c8b4;
-@palette_l:#9b9fa;
\ No newline at end of file
+@palette_B: black;
+@palette_W: #ffffff;
+@palette_g: #231f20;
+@palette_y: #ffd300;
+@palette_b: #6262ea;
+@palette_t: #5da3ea;
+@palette_s: #e67070;
+@palette_r: #e32119;
+@palette_R: #ab0c0c;
+@palette_c: #59eea3;
+@palette_G: #a1f051;
+@palette_v: #a664e8;
+@palette_p: #e869e8;
+@palette_a: #5becec;
+@palette_C: #53ee53;
+@palette_S: #f44848;
+@palette_P: #e66ba8;
+@palette_O: #f29e4a;
+@palette_k: #618227;
+@palette_e: #e1c8b4;
+@palette_l: #9b9fa;
diff --git a/src/game.ts b/src/game.ts
index 3c341fe..dfde982 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -254,34 +254,34 @@ let timers = [];
function startPlayCountDown() {
stopPlayCountDown();
+ gameState.startCountDown = 3;
+ gameState.needsRender = true;
- gameState.startCountDown = 3
- gameState.needsRender = true
-
- timers.push(setTimeout(() => {
- gameState.startCountDown = 2
- gameState.needsRender = true
- }, 1000));
- timers.push(setTimeout(() => {
- gameState.startCountDown = 1
- gameState.needsRender = true
- }, 2000));
timers.push(
setTimeout(() => {
- gameState.startCountDown = 0
+ gameState.startCountDown = 2;
+ gameState.needsRender = true;
+ }, 1000),
+ );
+ timers.push(
+ setTimeout(() => {
+ gameState.startCountDown = 1;
+ gameState.needsRender = true;
+ }, 2000),
+ );
+ timers.push(
+ setTimeout(() => {
+ gameState.startCountDown = 0;
play();
}, 3000),
);
-
-
}
function stopPlayCountDown() {
- if(!timers.length) return
+ if (!timers.length) return;
- gameState.startCountDown = 0
+ gameState.startCountDown = 0;
timers.forEach((id) => clearTimeout(id));
timers.length = 0;
-
}
gameCanvas.addEventListener("touchstart", (e) => {
e.preventDefault();
diff --git a/src/gameStateMutators.ts b/src/gameStateMutators.ts
index ffdda50..97c39d1 100644
--- a/src/gameStateMutators.ts
+++ b/src/gameStateMutators.ts
@@ -1,2318 +1,2340 @@
import {
- Ball,
- BallLike,
- Coin,
- colorString,
- GameState,
- LightFlash,
- ParticleFlash,
- ReusableArray,
- TextFlash,
+ Ball,
+ BallLike,
+ Coin,
+ colorString,
+ GameState,
+ LightFlash,
+ ParticleFlash,
+ ReusableArray,
+ TextFlash,
} from "./types";
import {
- brickCenterX,
- brickCenterY,
- canvasCenterX,
- currentLevelInfo,
- distance2,
- distanceBetween,
- getClosestBall,
- getCoinRenderColor,
- getCornerOffset,
- getMajorityValue,
- getRowColIndex,
- isMovingWhilePassiveIncome,
- isPickyEatingPossible,
- max_levels,
- reachRedRowIndex,
- shouldPierceByColor,
- telekinesisEffectRate,
- yoyoEffectRate,
- zoneLeftBorderX,
- zoneRightBorderX,
+ brickCenterX,
+ brickCenterY,
+ canvasCenterX,
+ currentLevelInfo,
+ distance2,
+ distanceBetween,
+ getClosestBall,
+ getCoinRenderColor,
+ getCornerOffset,
+ getMajorityValue,
+ getRowColIndex,
+ isMovingWhilePassiveIncome,
+ isPickyEatingPossible,
+ max_levels,
+ reachRedRowIndex,
+ shouldPierceByColor,
+ telekinesisEffectRate,
+ yoyoEffectRate,
+ zoneLeftBorderX,
+ zoneRightBorderX,
} from "./game_utils";
-import {t} from "./i18n/i18n";
+import { t } from "./i18n/i18n";
-import {getCurrentMaxCoins, getCurrentMaxParticles} from "./settings";
-import {background} from "./render";
-import {gameOver} from "./gameOver";
-import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, pause, startComputerControlledGame,} from "./game";
-import {stopRecording} from "./recording";
-import {isOptionOn} from "./options";
-import {ballTransparency, clamp, coinsBoostedCombo, comboKeepingRate,} from "./pure_functions";
-import {addToTotalScore} from "./addToTotalScore";
-import {hashCode} from "./getLevelBackground";
-import {openUpgradesPicker} from "./openUpgradesPicker";
+import { getCurrentMaxCoins, getCurrentMaxParticles } from "./settings";
+import { background } from "./render";
+import { gameOver } from "./gameOver";
+import {
+ brickIndex,
+ fitSize,
+ gameState,
+ hasBrick,
+ hitsSomething,
+ pause,
+ startComputerControlledGame,
+} from "./game";
+import { stopRecording } from "./recording";
+import { isOptionOn } from "./options";
+import {
+ ballTransparency,
+ clamp,
+ coinsBoostedCombo,
+ comboKeepingRate,
+} from "./pure_functions";
+import { addToTotalScore } from "./addToTotalScore";
+import { hashCode } from "./getLevelBackground";
+import { openUpgradesPicker } from "./openUpgradesPicker";
export function setMousePos(gameState: GameState, x: number) {
- if (gameState.startParams.computer_controlled) return;
- gameState.puckPosition = x;
+ if (gameState.startParams.computer_controlled) return;
+ gameState.puckPosition = x;
- // Sets the puck position, and updates the ball position if they are supposed to follow it
- gameState.needsRender = true;
+ // Sets the puck position, and updates the ball position if they are supposed to follow it
+ gameState.needsRender = true;
}
function getBallDefaultVx(gameState: GameState) {
- return (
- (gameState.perks.concave_puck ? 0 : 1) *
- (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed)
- );
+ return (
+ (gameState.perks.concave_puck ? 0 : 1) *
+ (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed)
+ );
}
function computerControl(gameState: GameState) {
- let targetX = gameState.puckPosition;
- const ball = getClosestBall(
- gameState,
- gameState.puckPosition,
- gameState.gameZoneHeight,
- );
- if (!ball) return;
- const puckOffset =
- (((hashCode(gameState.runStatistics.puck_bounces + "goeirjgoriejg") % 100) -
- 50) /
- 100) *
- gameState.puckWidth;
+ let targetX = gameState.puckPosition;
+ const ball = getClosestBall(
+ gameState,
+ gameState.puckPosition,
+ gameState.gameZoneHeight,
+ );
+ if (!ball) return;
+ const puckOffset =
+ (((hashCode(gameState.runStatistics.puck_bounces + "goeirjgoriejg") % 100) -
+ 50) /
+ 100) *
+ gameState.puckWidth;
- if (ball.y > gameState.gameZoneHeight / 2 && ball.vy > 0) {
- targetX = ball.x + puckOffset;
+ if (ball.y > gameState.gameZoneHeight / 2 && ball.vy > 0) {
+ targetX = ball.x + puckOffset;
+ } else {
+ let coinsTotalX = 0,
+ coinsCount = 0;
+ forEachLiveOne(gameState.coins, (c) => {
+ if (c.vy > 0 && c.y > gameState.gameZoneHeight / 2) {
+ coinsTotalX += c.x;
+ coinsCount++;
+ }
+ });
+ if (coinsCount) {
+ targetX = coinsTotalX / coinsCount;
} else {
- let coinsTotalX = 0,
- coinsCount = 0;
- forEachLiveOne(gameState.coins, (c) => {
- if (c.vy > 0 && c.y > gameState.gameZoneHeight / 2) {
- coinsTotalX += c.x;
- coinsCount++;
- }
- });
- if (coinsCount) {
- targetX = coinsTotalX / coinsCount;
- } else {
- targetX = gameState.canvasWidth / 2;
- }
+ targetX = gameState.canvasWidth / 2;
}
+ }
- gameState.puckPosition += clamp(
- (targetX - gameState.puckPosition) / 10,
- -10,
- 10,
- );
- if (gameState.levelTime > 30000) {
- startComputerControlledGame(gameState.startParams.stress);
- }
+ gameState.puckPosition += clamp(
+ (targetX - gameState.puckPosition) / 10,
+ -10,
+ 10,
+ );
+ if (gameState.levelTime > 30000) {
+ startComputerControlledGame(gameState.startParams.stress);
+ }
}
export function resetBalls(gameState: GameState) {
- // Always compute speed first
- normalizeGameState(gameState);
- const count = 1 + (gameState.perks?.multiball || 0);
- const perBall = gameState.puckWidth / (count + 1);
- gameState.balls = [];
- gameState.ballsColor = "#FFFFFF";
- if (gameState.perks.picky_eater || gameState.perks.pierce_color) {
- gameState.ballsColor =
- getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFFFFF";
- }
- for (let i = 0; i < count; i++) {
- const x =
- gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
- const vx = getBallDefaultVx(gameState);
+ // Always compute speed first
+ normalizeGameState(gameState);
+ const count = 1 + (gameState.perks?.multiball || 0);
+ const perBall = gameState.puckWidth / (count + 1);
+ gameState.balls = [];
+ gameState.ballsColor = "#FFFFFF";
+ if (gameState.perks.picky_eater || gameState.perks.pierce_color) {
+ gameState.ballsColor =
+ getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFFFFF";
+ }
+ for (let i = 0; i < count; i++) {
+ const x =
+ gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
+ const vx = getBallDefaultVx(gameState);
- gameState.balls.push({
- x,
- previousX: x,
- y: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
- previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
- vx,
- previousVX: vx,
- vy: -gameState.baseSpeed,
- previousVY: -gameState.baseSpeed,
- piercePoints: gameState.perks.pierce * 3,
- hitSinceBounce: 0,
- brokenSinceBounce: 0,
- sidesHitsSinceBounce: 0,
- sapperUses: 0,
- });
- }
- gameState.ballStickToPuck = true;
+ gameState.balls.push({
+ x,
+ previousX: x,
+ y: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
+ previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
+ vx,
+ previousVX: vx,
+ vy: -gameState.baseSpeed,
+ previousVY: -gameState.baseSpeed,
+ piercePoints: gameState.perks.pierce * 3,
+ hitSinceBounce: 0,
+ brokenSinceBounce: 0,
+ sidesHitsSinceBounce: 0,
+ sapperUses: 0,
+ });
+ }
+ gameState.ballStickToPuck = true;
}
export function putBallsAtPuck(gameState: GameState) {
- // This reset could be abused to cheat quite easily
- const count = gameState.balls.length;
- const perBall = gameState.puckWidth / (count + 1);
- // const vx = getBallDefaultVx(gameState);
- gameState.balls.forEach((ball, i) => {
- const x =
- gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
+ // This reset could be abused to cheat quite easily
+ const count = gameState.balls.length;
+ const perBall = gameState.puckWidth / (count + 1);
+ // const vx = getBallDefaultVx(gameState);
+ gameState.balls.forEach((ball, i) => {
+ const x =
+ gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
- ball.x = x;
- ball.previousX = x;
- ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize;
- ball.previousY = ball.y;
- ball.hitSinceBounce = 0;
- ball.brokenSinceBounce = 0;
- ball.sidesHitsSinceBounce = 0;
- ball.piercePoints = gameState.perks.pierce * 3;
- });
+ ball.x = x;
+ ball.previousX = x;
+ ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize;
+ ball.previousY = ball.y;
+ ball.hitSinceBounce = 0;
+ ball.brokenSinceBounce = 0;
+ ball.sidesHitsSinceBounce = 0;
+ ball.piercePoints = gameState.perks.pierce * 3;
+ });
}
export function normalizeGameState(gameState: GameState) {
- // This function resets most parameters on the state to correct values, and should be used even when the game is paused
+ // This function resets most parameters on the state to correct values, and should be used even when the game is paused
- gameState.baseSpeed = Math.max(
- 3,
- gameState.gameZoneWidth / 12 / 10 +
- gameState.currentLevel / 3 +
- gameState.levelTime / (30 * 1000) -
- gameState.perks.slow_down * 2,
- );
+ gameState.baseSpeed = Math.max(
+ 3,
+ gameState.gameZoneWidth / 12 / 10 +
+ gameState.currentLevel / 3 +
+ gameState.levelTime / (30 * 1000) -
+ gameState.perks.slow_down * 2,
+ );
- gameState.puckWidth = Math.max(
- gameState.ballSize,
- (gameState.gameZoneWidth / 12) *
- Math.min(
- 12,
- 3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck,
- ),
- );
+ gameState.puckWidth = Math.max(
+ gameState.ballSize,
+ (gameState.gameZoneWidth / 12) *
+ Math.min(
+ 12,
+ 3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck,
+ ),
+ );
- const corner = getCornerOffset(gameState);
+ const corner = getCornerOffset(gameState);
- let minX = gameState.offsetXRoundedDown + gameState.puckWidth / 2 - corner;
+ let minX = gameState.offsetXRoundedDown + gameState.puckWidth / 2 - corner;
- let maxX =
- gameState.offsetXRoundedDown +
- gameState.gameZoneWidthRoundedUp -
- gameState.puckWidth / 2 +
- corner;
+ let maxX =
+ gameState.offsetXRoundedDown +
+ gameState.gameZoneWidthRoundedUp -
+ gameState.puckWidth / 2 +
+ corner;
- gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX);
+ gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX);
- if (gameState.ballStickToPuck) {
- putBallsAtPuck(gameState);
- }
+ if (gameState.ballStickToPuck) {
+ putBallsAtPuck(gameState);
+ }
- if (
- Math.abs(gameState.lastPuckPosition - gameState.puckPosition) > 1 &&
- gameState.running
- ) {
- gameState.lastPuckMove = gameState.levelTime;
- }
+ if (
+ Math.abs(gameState.lastPuckPosition - gameState.puckPosition) > 1 &&
+ gameState.running
+ ) {
+ gameState.lastPuckMove = gameState.levelTime;
+ }
}
export function baseCombo(gameState: GameState) {
- return 1 + gameState.perks.base_combo * 3;
+ return 1 + gameState.perks.base_combo * 3;
}
export function resetCombo(
- gameState: GameState,
- x: number | undefined,
- y: number | undefined,
+ gameState: GameState,
+ x: number | undefined,
+ y: number | undefined,
) {
- const prev = gameState.combo;
- gameState.combo = baseCombo(gameState);
+ const prev = gameState.combo;
+ gameState.combo = baseCombo(gameState);
- if (gameState.perks.double_or_nothing && prev > gameState.combo) {
- gameState.score = Math.floor(
- gameState.score * clamp(1 - gameState.perks.double_or_nothing / 10, 0, 1),
- );
- schedulGameSound(gameState, "lifeLost", x, 1);
- }
+ if (gameState.perks.double_or_nothing && prev > gameState.combo) {
+ gameState.score = Math.floor(
+ gameState.score * clamp(1 - gameState.perks.double_or_nothing / 10, 0, 1),
+ );
+ schedulGameSound(gameState, "lifeLost", x, 1);
+ }
- if (prev > gameState.combo && gameState.perks.soft_reset) {
- gameState.combo += Math.floor(
- (prev - gameState.combo) * comboKeepingRate(gameState.perks.soft_reset),
- );
+ if (prev > gameState.combo && gameState.perks.soft_reset) {
+ gameState.combo += Math.floor(
+ (prev - gameState.combo) * comboKeepingRate(gameState.perks.soft_reset),
+ );
+ }
+ const lost = Math.max(0, prev - gameState.combo);
+ if (lost) {
+ for (let i = 0; i < lost && i < 8; i++) {
+ setTimeout(
+ () => schedulGameSound(gameState, "comboDecrease", x, 1),
+ i * 100,
+ );
}
- const lost = Math.max(0, prev - gameState.combo);
- if (lost) {
- for (let i = 0; i < lost && i < 8; i++) {
- setTimeout(
- () => schedulGameSound(gameState, "comboDecrease", x, 1),
- i * 100,
- );
- }
- if (typeof x !== "undefined" && typeof y !== "undefined") {
- makeText(
- gameState,
- x,
- y,
- "#FF0000",
- "-" + lost,
- 20,
- 500 + clamp(lost, 0, 500),
- );
- }
+ if (typeof x !== "undefined" && typeof y !== "undefined") {
+ makeText(
+ gameState,
+ x,
+ y,
+ "#FF0000",
+ "-" + lost,
+ 20,
+ 500 + clamp(lost, 0, 500),
+ );
}
- return lost;
+ }
+ return lost;
}
export function offsetCombo(
- gameState: GameState,
- by: number,
- x: number,
- y: number,
+ gameState: GameState,
+ by: number,
+ x: number,
+ y: number,
) {
- if (!by) return;
- if (by > 0) {
- by *= 1 + gameState.perks.double_or_nothing;
- gameState.combo += by;
- makeText(gameState, x, y, "#ffd300", "+" + by, 25, 400 + by);
- } else {
- const prev = gameState.combo;
- gameState.combo = Math.max(baseCombo(gameState), gameState.combo + by);
- const lost = Math.max(0, prev - gameState.combo);
+ if (!by) return;
+ if (by > 0) {
+ by *= 1 + gameState.perks.double_or_nothing;
+ gameState.combo += by;
+ makeText(gameState, x, y, "#ffd300", "+" + by, 25, 400 + by);
+ } else {
+ const prev = gameState.combo;
+ gameState.combo = Math.max(baseCombo(gameState), gameState.combo + by);
+ const lost = Math.max(0, prev - gameState.combo);
- if (lost) {
- schedulGameSound(gameState, "comboDecrease", x, 1);
- makeText(gameState, x, y, "#FF0000", "-" + lost, 20, 400 + lost);
- }
+ if (lost) {
+ schedulGameSound(gameState, "comboDecrease", x, 1);
+ makeText(gameState, x, y, "#FF0000", "-" + lost, 20, 400 + lost);
}
+ }
}
export function spawnParticlesExplosion(
- gameState: GameState,
- count: number,
- x: number,
- y: number,
- color: string,
+ gameState: GameState,
+ count: number,
+ x: number,
+ y: number,
+ color: string,
) {
- if (!!isOptionOn("basic")) return;
+ if (!!isOptionOn("basic")) return;
- if (liveCount(gameState.particles) > getCurrentMaxParticles()) {
- // Avoid freezing when lots of explosion happen at once
- count = 1;
- }
- for (let i = 0; i < count; i++) {
- makeParticle(
- gameState,
+ if (liveCount(gameState.particles) > getCurrentMaxParticles()) {
+ // Avoid freezing when lots of explosion happen at once
+ count = 1;
+ }
+ for (let i = 0; i < count; i++) {
+ makeParticle(
+ gameState,
- x + ((Math.random() - 0.5) * gameState.brickWidth) / 2,
- y + ((Math.random() - 0.5) * gameState.brickWidth) / 2,
- (Math.random() - 0.5) * 30,
- (Math.random() - 0.5) * 30,
- color,
- false,
- );
- }
+ x + ((Math.random() - 0.5) * gameState.brickWidth) / 2,
+ y + ((Math.random() - 0.5) * gameState.brickWidth) / 2,
+ (Math.random() - 0.5) * 30,
+ (Math.random() - 0.5) * 30,
+ color,
+ false,
+ );
+ }
}
export function spawnParticlesImplosion(
- gameState: GameState,
- count: number,
- x: number,
- y: number,
- color: string,
+ gameState: GameState,
+ count: number,
+ x: number,
+ y: number,
+ color: string,
) {
- if (!!isOptionOn("basic")) return;
+ if (!!isOptionOn("basic")) return;
- if (liveCount(gameState.particles) > getCurrentMaxParticles()) {
- // Avoid freezing when lots of explosion happen at once
- count = 1;
- }
- for (let i = 0; i < count; i++) {
- const dx = ((Math.random() - 0.5) * gameState.brickWidth) / 2;
- const dy = ((Math.random() - 0.5) * gameState.brickWidth) / 2;
- makeParticle(gameState, x - dx * 10, y - dy * 10, dx, dy, color, false);
- }
+ if (liveCount(gameState.particles) > getCurrentMaxParticles()) {
+ // Avoid freezing when lots of explosion happen at once
+ count = 1;
+ }
+ for (let i = 0; i < count; i++) {
+ const dx = ((Math.random() - 0.5) * gameState.brickWidth) / 2;
+ const dy = ((Math.random() - 0.5) * gameState.brickWidth) / 2;
+ makeParticle(gameState, x - dx * 10, y - dy * 10, dx, dy, color, false);
+ }
}
export function explosionAt(
- gameState: GameState,
- index: number,
- x: number,
- y: number,
- ball: Ball,
- extraSize: number = 0,
+ gameState: GameState,
+ index: number,
+ x: number,
+ y: number,
+ ball: Ball,
+ extraSize: number = 0,
) {
- const size =
- 1 +
- gameState.perks.bigger_explosions +
- Math.max(0, gameState.perks.implosions - 1) +
- extraSize;
- schedulGameSound(gameState, "explode", ball.x, 1);
- if (index !== -1) {
- const col = index % gameState.gridSize;
- const row = Math.floor(index / gameState.gridSize);
- // Break bricks around
- for (let dx = -size; dx <= size; dx++) {
- for (let dy = -size; dy <= size; dy++) {
- const i = getRowColIndex(gameState, row + dy, col + dx);
- if (gameState.bricks[i] && i !== -1) {
- // Study bricks resist explosions too
- gameState.brickHP[i]--;
- if (gameState.brickHP[i] <= 0) {
- explodeBrick(gameState, i, ball, true);
- }
- }
- }
+ const size =
+ 1 +
+ gameState.perks.bigger_explosions +
+ Math.max(0, gameState.perks.implosions - 1) +
+ extraSize;
+ schedulGameSound(gameState, "explode", ball.x, 1);
+ if (index !== -1) {
+ const col = index % gameState.gridSize;
+ const row = Math.floor(index / gameState.gridSize);
+ // Break bricks around
+ for (let dx = -size; dx <= size; dx++) {
+ for (let dy = -size; dy <= size; dy++) {
+ const i = getRowColIndex(gameState, row + dy, col + dx);
+ if (gameState.bricks[i] && i !== -1) {
+ // Study bricks resist explosions too
+ gameState.brickHP[i]--;
+ if (gameState.brickHP[i] <= 0) {
+ explodeBrick(gameState, i, ball, true);
+ }
}
+ }
}
+ }
- const factor = gameState.perks.implosions ? -1 : 1;
- // Blow nearby coins
- forEachLiveOne(gameState.coins, (c) => {
- const dx = c.x - x;
- const dy = c.y - y;
- const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy));
- c.vx += (((dx / d2) * 10 * size) / c.weight) * factor;
- c.vy += (((dy / d2) * 10 * size) / c.weight) * factor;
- });
- gameState.lastExplosion = gameState.levelTime;
+ const factor = gameState.perks.implosions ? -1 : 1;
+ // Blow nearby coins
+ forEachLiveOne(gameState.coins, (c) => {
+ const dx = c.x - x;
+ const dy = c.y - y;
+ const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy));
+ c.vx += (((dx / d2) * 10 * size) / c.weight) * factor;
+ c.vy += (((dy / d2) * 10 * size) / c.weight) * factor;
+ });
+ gameState.lastExplosion = gameState.levelTime;
- if (gameState.perks.implosions) {
- spawnParticlesImplosion(gameState, 7 * size, x, y, "#FFFFFF");
- } else {
- spawnParticlesExplosion(gameState, 7 * size, x, y, "#FFFFFF");
- }
+ if (gameState.perks.implosions) {
+ spawnParticlesImplosion(gameState, 7 * size, x, y, "#FFFFFF");
+ } else {
+ spawnParticlesExplosion(gameState, 7 * size, x, y, "#FFFFFF");
+ }
- gameState.runStatistics.bricks_broken++;
+ gameState.runStatistics.bricks_broken++;
- if (gameState.perks.zen) {
- gameState.lastZenComboIncrease = gameState.levelTime;
- resetCombo(gameState, x, y);
- }
+ if (gameState.perks.zen) {
+ gameState.lastZenComboIncrease = gameState.levelTime;
+ resetCombo(gameState, x, y);
+ }
}
export function explodeBrick(
- gameState: GameState,
- index: number,
- ball: Ball,
- isExplosion: boolean,
+ gameState: GameState,
+ index: number,
+ ball: Ball,
+ isExplosion: boolean,
) {
- const color = gameState.bricks[index];
- if (!color) return;
+ const color = gameState.bricks[index];
+ if (!color) return;
- const wasPickyEaterPossible =
- gameState.perks.picky_eater && isPickyEatingPossible(gameState);
- const redRowReach = reachRedRowIndex(gameState);
+ const wasPickyEaterPossible =
+ gameState.perks.picky_eater && isPickyEatingPossible(gameState);
+ const redRowReach = reachRedRowIndex(gameState);
- gameState.lastBrickBroken = gameState.levelTime;
+ gameState.lastBrickBroken = gameState.levelTime;
- if (color === "black") {
- const x = brickCenterX(gameState, index),
- y = brickCenterY(gameState, index);
+ if (color === "black") {
+ const x = brickCenterX(gameState, index),
+ y = brickCenterY(gameState, index);
- setBrick(gameState, index, "");
- explosionAt(gameState, index, x, y, ball, 0);
- } else if (color) {
- // Even if it bounces we don't want to count that as a miss
+ setBrick(gameState, index, "");
+ explosionAt(gameState, index, x, y, ball, 0);
+ } else if (color) {
+ // Even if it bounces we don't want to count that as a miss
- // Flashing is take care of by the tick loop
- const x = brickCenterX(gameState, index),
- y = brickCenterY(gameState, index);
+ // Flashing is take care of by the tick loop
+ const x = brickCenterX(gameState, index),
+ y = brickCenterY(gameState, index);
- setBrick(gameState, index, "");
+ setBrick(gameState, index, "");
- let coinsToSpawn = coinsBoostedCombo(gameState);
+ let coinsToSpawn = coinsBoostedCombo(gameState);
- gameState.levelSpawnedCoins += coinsToSpawn;
- gameState.runStatistics.coins_spawned += coinsToSpawn;
- gameState.runStatistics.bricks_broken++;
+ gameState.levelSpawnedCoins += coinsToSpawn;
+ gameState.runStatistics.coins_spawned += coinsToSpawn;
+ gameState.runStatistics.bricks_broken++;
- const maxCoins = getCurrentMaxCoins();
- const spawnableCoins =
- liveCount(gameState.coins) > getCurrentMaxCoins()
- ? 1
- : Math.floor((maxCoins - liveCount(gameState.coins)) / 2);
+ const maxCoins = getCurrentMaxCoins();
+ const spawnableCoins =
+ liveCount(gameState.coins) > getCurrentMaxCoins()
+ ? 1
+ : Math.floor((maxCoins - liveCount(gameState.coins)) / 2);
- const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
+ const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
- while (coinsToSpawn > 0) {
- const points = Math.min(pointsPerCoin, coinsToSpawn);
- if (points < 0 || isNaN(points)) {
- console.error({points});
- debugger;
- }
+ while (coinsToSpawn > 0) {
+ const points = Math.min(pointsPerCoin, coinsToSpawn);
+ if (points < 0 || isNaN(points)) {
+ console.error({ points });
+ debugger;
+ }
- coinsToSpawn -= points;
+ coinsToSpawn -= points;
- const cx =
- x +
- (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize),
- cy =
- y +
- (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize);
+ const cx =
+ x +
+ (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize),
+ cy =
+ y +
+ (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize);
- makeCoin(
- gameState,
- cx,
- cy,
- ball.previousVX * (0.5 + Math.random()),
- ball.previousVY * (0.5 + Math.random()),
- color,
- points,
- );
- }
- let resetComboNeeeded = false;
- let comboGain =
- gameState.perks.streak_shots +
- gameState.perks.compound_interest +
- gameState.perks.left_is_lava +
- gameState.perks.right_is_lava +
- gameState.perks.top_is_lava +
- gameState.perks.picky_eater +
- gameState.perks.asceticism * 3 +
- gameState.perks.passive_income +
- gameState.perks.addiction;
+ makeCoin(
+ gameState,
+ cx,
+ cy,
+ ball.previousVX * (0.5 + Math.random()),
+ ball.previousVY * (0.5 + Math.random()),
+ color,
+ points,
+ );
+ }
+ let resetComboNeeeded = false;
+ let comboGain =
+ gameState.perks.streak_shots +
+ gameState.perks.compound_interest +
+ gameState.perks.left_is_lava +
+ gameState.perks.right_is_lava +
+ gameState.perks.top_is_lava +
+ gameState.perks.picky_eater +
+ gameState.perks.asceticism * 3 +
+ gameState.perks.passive_income +
+ gameState.perks.addiction;
- if (Math.abs(ball.y - y) < Math.abs(ball.x - x)) {
- if (gameState.perks.side_kick) {
- if (ball.previousVX > 0) {
- comboGain += gameState.perks.side_kick;
- } else {
- comboGain -= gameState.perks.side_kick * 2;
- }
- }
- if (gameState.perks.side_flip) {
- if (ball.previousVX < 0) {
- comboGain += gameState.perks.side_flip;
- } else {
- comboGain -= gameState.perks.side_flip * 2;
- }
- }
- }
-
- if (redRowReach !== -1) {
- if (Math.floor(index / gameState.level.size) === redRowReach) {
- resetComboNeeeded = true;
- } else {
- for (let x = 0; x < gameState.level.size; x++) {
- if (gameState.bricks[redRowReach * gameState.level.size + x])
- comboGain += gameState.perks.reach;
- }
- }
- }
-
- if (!isExplosion) {
- // color change
- if (
- (gameState.perks.picky_eater || gameState.perks.pierce_color) &&
- color !== gameState.ballsColor &&
- color
- ) {
- if (wasPickyEaterPossible) {
- resetComboNeeeded = true;
- }
- schedulGameSound(gameState, "colorChange", ball.x, 0.8);
- // gameState.lastExplosion = gameState.levelTime;
- gameState.ballsColor = color;
- gameState.balls.forEach((ball) => {
- spawnParticlesExplosion(
- gameState,
- 7,
- ball.previousX,
- ball.previousY,
- color,
- );
- });
- } else {
- schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1);
- }
- }
-
- if (resetComboNeeeded) {
- resetCombo(gameState, ball.x, ball.y);
+ if (Math.abs(ball.y - y) < Math.abs(ball.x - x)) {
+ if (gameState.perks.side_kick) {
+ if (ball.previousVX > 0) {
+ comboGain += gameState.perks.side_kick;
} else {
- offsetCombo(gameState, comboGain, ball.x, ball.y);
+ comboGain -= gameState.perks.side_kick * 2;
}
- // Particle effect
- spawnParticlesExplosion(
- gameState,
- 5 + Math.min(gameState.combo, 30),
- x,
- y,
- color,
- );
+ }
+ if (gameState.perks.side_flip) {
+ if (ball.previousVX < 0) {
+ comboGain += gameState.perks.side_flip;
+ } else {
+ comboGain -= gameState.perks.side_flip * 2;
+ }
+ }
}
- if (
- gameState.perks.respawn &&
- color !== "black" &&
- !gameState.bricks[index]
- ) {
- if (Math.random() < comboKeepingRate(gameState.perks.respawn)) {
- append(gameState.respawns, (b) => {
- b.color = color;
- b.index = index;
- b.time = gameState.levelTime + (3 * 1000) / gameState.perks.respawn;
- });
+ if (redRowReach !== -1) {
+ if (Math.floor(index / gameState.level.size) === redRowReach) {
+ resetComboNeeeded = true;
+ } else {
+ for (let x = 0; x < gameState.level.size; x++) {
+ if (gameState.bricks[redRowReach * gameState.level.size + x])
+ comboGain += gameState.perks.reach;
}
+ }
}
+
+ if (!isExplosion) {
+ // color change
+ if (
+ (gameState.perks.picky_eater || gameState.perks.pierce_color) &&
+ color !== gameState.ballsColor &&
+ color
+ ) {
+ if (wasPickyEaterPossible) {
+ resetComboNeeeded = true;
+ }
+ schedulGameSound(gameState, "colorChange", ball.x, 0.8);
+ // gameState.lastExplosion = gameState.levelTime;
+ gameState.ballsColor = color;
+ gameState.balls.forEach((ball) => {
+ spawnParticlesExplosion(
+ gameState,
+ 7,
+ ball.previousX,
+ ball.previousY,
+ color,
+ );
+ });
+ } else {
+ schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1);
+ }
+ }
+
+ if (resetComboNeeeded) {
+ resetCombo(gameState, ball.x, ball.y);
+ } else {
+ offsetCombo(gameState, comboGain, ball.x, ball.y);
+ }
+ // Particle effect
+ spawnParticlesExplosion(
+ gameState,
+ 5 + Math.min(gameState.combo, 30),
+ x,
+ y,
+ color,
+ );
+ }
+
+ if (
+ gameState.perks.respawn &&
+ color !== "black" &&
+ !gameState.bricks[index]
+ ) {
+ if (Math.random() < comboKeepingRate(gameState.perks.respawn)) {
+ append(gameState.respawns, (b) => {
+ b.color = color;
+ b.index = index;
+ b.time = gameState.levelTime + (3 * 1000) / gameState.perks.respawn;
+ });
+ }
+ }
}
export function schedulGameSound(
- gameState: GameState,
- sound: keyof GameState["aboutToPlaySound"],
- x: number | void,
- vol: number,
+ gameState: GameState,
+ sound: keyof GameState["aboutToPlaySound"],
+ x: number | void,
+ vol: number,
) {
- if (!vol) return;
- if (!isOptionOn("sound")) return;
+ if (!vol) return;
+ if (!isOptionOn("sound")) return;
- x ??= gameState.offsetX + gameState.gameZoneWidth / 2;
- const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number };
+ x ??= gameState.offsetX + gameState.gameZoneWidth / 2;
+ const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number };
- ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol);
- ex.vol += vol;
+ ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol);
+ ex.vol += vol;
}
export function addToScore(gameState: GameState, coin: Coin) {
- gameState.score += coin.points;
+ gameState.score += coin.points;
- gameState.levelCoughtCoins += coin.points;
- gameState.lastScoreIncrease = gameState.levelTime;
- addToTotalScore(gameState, coin.points);
- if (gameState.score > gameState.highScore && !gameState.creative) {
- gameState.highScore = gameState.score;
- try {
- localStorage.setItem("breakout-3-hs-short", gameState.score.toString());
- } catch (e) {
- }
- }
- if (!isOptionOn("basic")) {
- makeParticle(
- gameState,
- coin.previousX,
- coin.previousY,
- (gameState.canvasWidth - coin.x) / 100,
- -coin.y / 100,
- getCoinRenderColor(gameState, coin),
- true,
- gameState.coinSize / 2,
- 100 + Math.random() * 50,
- );
- }
+ gameState.levelCoughtCoins += coin.points;
+ gameState.lastScoreIncrease = gameState.levelTime;
+ addToTotalScore(gameState, coin.points);
+ if (gameState.score > gameState.highScore && !gameState.creative) {
+ gameState.highScore = gameState.score;
+ try {
+ localStorage.setItem("breakout-3-hs-short", gameState.score.toString());
+ } catch (e) {}
+ }
+ if (!isOptionOn("basic")) {
+ makeParticle(
+ gameState,
+ coin.previousX,
+ coin.previousY,
+ (gameState.canvasWidth - coin.x) / 100,
+ -coin.y / 100,
+ getCoinRenderColor(gameState, coin),
+ true,
+ gameState.coinSize / 2,
+ 100 + Math.random() * 50,
+ );
+ }
- schedulGameSound(gameState, "coinCatch", coin.x, 1);
- gameState.runStatistics.score += coin.points;
- if (gameState.perks.asceticism) {
- offsetCombo(
- gameState,
- -gameState.perks.asceticism * 3 * coin.points,
- coin.x,
- coin.y,
- );
- }
+ schedulGameSound(gameState, "coinCatch", coin.x, 1);
+ gameState.runStatistics.score += coin.points;
+ if (gameState.perks.asceticism) {
+ offsetCombo(
+ gameState,
+ -gameState.perks.asceticism * 3 * coin.points,
+ coin.x,
+ coin.y,
+ );
+ }
}
export async function setLevel(gameState: GameState, l: number) {
- // Ignore duplicated calls, can happen when ticking is split in multiple updates because the ball goes fast
- if (gameState.upgradesOfferedFor >= l) {
- return;
- }
- pause(false);
- gameState.upgradesOfferedFor = l;
- stopRecording();
+ // Ignore duplicated calls, can happen when ticking is split in multiple updates because the ball goes fast
+ if (gameState.upgradesOfferedFor >= l) {
+ return;
+ }
+ pause(false);
+ gameState.upgradesOfferedFor = l;
+ stopRecording();
- gameState.currentLevel = l;
- gameState.level = gameState.runLevels[l % gameState.runLevels.length];
+ gameState.currentLevel = l;
+ gameState.level = gameState.runLevels[l % gameState.runLevels.length];
- if (l > 0) {
- await openUpgradesPicker(gameState);
- }
+ if (l > 0) {
+ await openUpgradesPicker(gameState);
+ }
- gameState.levelTime = 0;
- gameState.winAt = 0;
- gameState.levelWallBounces = 0;
- gameState.lastPuckMove = 0;
- gameState.lastZenComboIncrease = 0;
- gameState.autoCleanUses = 0;
+ gameState.levelTime = 0;
+ gameState.winAt = 0;
+ gameState.levelWallBounces = 0;
+ gameState.lastPuckMove = 0;
+ gameState.lastZenComboIncrease = 0;
+ gameState.autoCleanUses = 0;
- gameState.lastTickDown = gameState.levelTime;
- gameState.levelStartScore = gameState.score;
- gameState.levelSpawnedCoins = 0;
- gameState.levelCoughtCoins = 0;
- gameState.levelLostCoins = 0;
- gameState.levelMisses = 0;
- gameState.lastBrickBroken = 0;
- gameState.runStatistics.levelsPlayed++;
+ gameState.lastTickDown = gameState.levelTime;
+ gameState.levelStartScore = gameState.score;
+ gameState.levelSpawnedCoins = 0;
+ gameState.levelCoughtCoins = 0;
+ gameState.levelLostCoins = 0;
+ gameState.levelMisses = 0;
+ gameState.lastBrickBroken = 0;
+ gameState.runStatistics.levelsPlayed++;
- // Reset combo silently
- const finalCombo = gameState.combo;
- gameState.combo = baseCombo(gameState);
- if (gameState.perks.shunt) {
- gameState.combo += Math.round(
- Math.max(
- 0,
- (finalCombo - gameState.combo) *
- comboKeepingRate(gameState.perks.shunt),
- ),
- );
- }
+ // Reset combo silently
+ const finalCombo = gameState.combo;
+ gameState.combo = baseCombo(gameState);
+ if (gameState.perks.shunt) {
+ gameState.combo += Math.round(
+ Math.max(
+ 0,
+ (finalCombo - gameState.combo) *
+ comboKeepingRate(gameState.perks.shunt),
+ ),
+ );
+ }
- gameState.combo += gameState.perks.hot_start * 30;
+ gameState.combo += gameState.perks.hot_start * 30;
- const lvl = currentLevelInfo(gameState);
- if (lvl.size !== gameState.gridSize) {
- gameState.gridSize = lvl.size;
- fitSize(gameState);
- }
- gameState.levelLostCoins += empty(gameState.coins);
- empty(gameState.particles);
- empty(gameState.lights);
- empty(gameState.texts);
- empty(gameState.respawns);
- gameState.bricks = [];
+ const lvl = currentLevelInfo(gameState);
+ if (lvl.size !== gameState.gridSize) {
+ gameState.gridSize = lvl.size;
+ fitSize(gameState);
+ }
+ gameState.levelLostCoins += empty(gameState.coins);
+ empty(gameState.particles);
+ empty(gameState.lights);
+ empty(gameState.texts);
+ empty(gameState.respawns);
+ gameState.bricks = [];
- for (let i = 0; i < lvl.size * lvl.size; i++) {
- setBrick(gameState, i, lvl.bricks[i]);
- }
+ for (let i = 0; i < lvl.size * lvl.size; i++) {
+ setBrick(gameState, i, lvl.bricks[i]);
+ }
- // Balls color will depend on most common brick color sometimes
- resetBalls(gameState);
- gameState.needsRender = true;
- // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons
- // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
- background.src = "data:image/svg+xml;UTF8," + lvl.svg;
- document.body.style.setProperty("--level-background", lvl.color || "#000000");
- document
- .getElementById("themeColor")
- ?.setAttribute("content", lvl.color || "#000000");
+ // Balls color will depend on most common brick color sometimes
+ resetBalls(gameState);
+ gameState.needsRender = true;
+ // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons
+ // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
+ background.src = "data:image/svg+xml;UTF8," + lvl.svg;
+ document.body.style.setProperty("--level-background", lvl.color || "#000000");
+ document
+ .getElementById("themeColor")
+ ?.setAttribute("content", lvl.color || "#000000");
}
function setBrick(gameState: GameState, index: number, color: string) {
- gameState.bricks[index] = color || "";
- gameState.brickHP[index] =
- (color === "black" && 1) ||
- (color && 1 + gameState.perks.sturdy_bricks) ||
- 0;
+ gameState.bricks[index] = color || "";
+ gameState.brickHP[index] =
+ (color === "black" && 1) ||
+ (color && 1 + gameState.perks.sturdy_bricks) ||
+ 0;
}
const rainbow = [
- "#ff2e2e",
- "#ffe02e",
- "#70ff33",
- "#33ffa7",
- "#38acff",
- "#6262EA",
- "#ff3de5",
+ "#ff2e2e",
+ "#ffe02e",
+ "#70ff33",
+ "#33ffa7",
+ "#38acff",
+ "#6262EA",
+ "#ff3de5",
];
export function rainbowColor(): colorString {
- return rainbow[Math.floor(gameState.levelTime / 50) % rainbow.length];
+ return rainbow[Math.floor(gameState.levelTime / 50) % rainbow.length];
}
export function repulse(
- gameState: GameState,
- a: Ball,
- b: BallLike,
- power: number,
- impactsBToo: boolean,
+ gameState: GameState,
+ a: Ball,
+ b: BallLike,
+ power: number,
+ impactsBToo: boolean,
) {
- const distance = distanceBetween(a, b);
- // Ensure we don't get soft locked
- const max = gameState.gameZoneWidth / 4;
- if (distance > max) return;
- // Unit vector
- const dx = (a.x - b.x) / distance;
- const dy = (a.y - b.y) / distance;
- const fact =
- (((-power * (max - distance)) / (max * 1.2) / 3) *
- Math.min(500, gameState.levelTime)) /
- 500;
- if (
- impactsBToo &&
- typeof b.vx !== "undefined" &&
- typeof b.vy !== "undefined"
- ) {
- b.vx += dx * fact;
- b.vy += dy * fact;
- }
- a.vx -= dx * fact;
- a.vy -= dy * fact;
+ const distance = distanceBetween(a, b);
+ // Ensure we don't get soft locked
+ const max = gameState.gameZoneWidth / 4;
+ if (distance > max) return;
+ // Unit vector
+ const dx = (a.x - b.x) / distance;
+ const dy = (a.y - b.y) / distance;
+ const fact =
+ (((-power * (max - distance)) / (max * 1.2) / 3) *
+ Math.min(500, gameState.levelTime)) /
+ 500;
+ if (
+ impactsBToo &&
+ typeof b.vx !== "undefined" &&
+ typeof b.vy !== "undefined"
+ ) {
+ b.vx += dx * fact;
+ b.vy += dy * fact;
+ }
+ a.vx -= dx * fact;
+ a.vy -= dy * fact;
- const speed = 10;
- const rand = 2;
+ const speed = 10;
+ const rand = 2;
+ makeParticle(
+ gameState,
+ a.x,
+ a.y,
+ -dx * speed + a.vx + (Math.random() - 0.5) * rand,
+ -dy * speed + a.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
+ if (
+ impactsBToo &&
+ typeof b.vx !== "undefined" &&
+ typeof b.vy !== "undefined"
+ ) {
makeParticle(
- gameState,
- a.x,
- a.y,
- -dx * speed + a.vx + (Math.random() - 0.5) * rand,
- -dy * speed + a.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
+ gameState,
+ b.x,
+ b.y,
+ dx * speed + b.vx + (Math.random() - 0.5) * rand,
+ dy * speed + b.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
);
- if (
- impactsBToo &&
- typeof b.vx !== "undefined" &&
- typeof b.vy !== "undefined"
- ) {
- makeParticle(
- gameState,
- b.x,
- b.y,
- dx * speed + b.vx + (Math.random() - 0.5) * rand,
- dy * speed + b.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
- );
- }
+ }
}
export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
- const distance = distanceBetween(a, b);
- // Ensure we don't get soft locked
- const min = (gameState.gameZoneWidth * 3) / 4;
- if (distance < min) return;
- // Unit vector
- const dx = (a.x - b.x) / distance;
- const dy = (a.y - b.y) / distance;
+ const distance = distanceBetween(a, b);
+ // Ensure we don't get soft locked
+ const min = (gameState.gameZoneWidth * 3) / 4;
+ if (distance < min) return;
+ // Unit vector
+ const dx = (a.x - b.x) / distance;
+ const dy = (a.y - b.y) / distance;
- const fact =
- (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) /
- 500;
- b.vx += dx * fact;
- b.vy += dy * fact;
- a.vx -= dx * fact;
- a.vy -= dy * fact;
+ const fact =
+ (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) /
+ 500;
+ b.vx += dx * fact;
+ b.vy += dy * fact;
+ a.vx -= dx * fact;
+ a.vy -= dy * fact;
- const speed = 10;
- const rand = 2;
+ const speed = 10;
+ const rand = 2;
- makeParticle(
- gameState,
- a.x,
- a.y,
- dx * speed + a.vx + (Math.random() - 0.5) * rand,
- dy * speed + a.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
- );
- makeParticle(
- gameState,
- b.x,
- b.y,
- -dx * speed + b.vx + (Math.random() - 0.5) * rand,
- -dy * speed + b.vy + (Math.random() - 0.5) * rand,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
- );
+ makeParticle(
+ gameState,
+ a.x,
+ a.y,
+ dx * speed + a.vx + (Math.random() - 0.5) * rand,
+ dy * speed + a.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
+ makeParticle(
+ gameState,
+ b.x,
+ b.y,
+ -dx * speed + b.vx + (Math.random() - 0.5) * rand,
+ -dy * speed + b.vy + (Math.random() - 0.5) * rand,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
}
export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
- // Make ball/coin bonce, and return bricks that were hit
- const radius = coin.size / 2;
- const {x, y, previousX, previousY} = coin;
+ // Make ball/coin bonce, and return bricks that were hit
+ const radius = coin.size / 2;
+ const { x, y, previousX, previousY } = coin;
- const vhit = hitsSomething(previousX, y, radius);
- const hhit = hitsSomething(x, previousY, radius);
- const chit =
- (typeof vhit == "undefined" &&
- typeof hhit == "undefined" &&
- hitsSomething(x, y, radius)) ||
- undefined;
+ const vhit = hitsSomething(previousX, y, radius);
+ const hhit = hitsSomething(x, previousY, radius);
+ const chit =
+ (typeof vhit == "undefined" &&
+ typeof hhit == "undefined" &&
+ hitsSomething(x, y, radius)) ||
+ undefined;
- if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
- if (gameState.perks.ghost_coins) {
- // slow down
- coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
- coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins;
- } else {
- if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
- coin.y = coin.previousY;
- coin.vy *= -1;
+ if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
+ if (gameState.perks.ghost_coins) {
+ // slow down
+ coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
+ coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins;
+ } else {
+ if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
+ coin.y = coin.previousY;
+ coin.vy *= -1;
- // Roll on corners
- const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)];
- const rightHit = gameState.bricks[brickIndex(x + radius, y + radius)];
+ // Roll on corners
+ const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)];
+ const rightHit = gameState.bricks[brickIndex(x + radius, y + radius)];
- if (leftHit && !rightHit) {
- coin.vx += 1;
- coin.sa -= 1;
- }
- if (!leftHit && rightHit) {
- coin.vx -= 1;
- coin.sa += 1;
- }
- }
- if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
- coin.x = coin.previousX;
- coin.vx *= -1;
- }
+ if (leftHit && !rightHit) {
+ coin.vx += 1;
+ coin.sa -= 1;
}
+ if (!leftHit && rightHit) {
+ coin.vx -= 1;
+ coin.sa += 1;
+ }
+ }
+ if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
+ coin.x = coin.previousX;
+ coin.vx *= -1;
+ }
}
- return vhit ?? hhit ?? chit;
+ }
+ return vhit ?? hhit ?? chit;
}
export function bordersHitCheck(
- gameState: GameState,
- coin: Coin | Ball,
- radius: number,
- delta: number,
+ gameState: GameState,
+ coin: Coin | Ball,
+ radius: number,
+ delta: number,
) {
- if (coin.destroyed) return;
- coin.previousX = coin.x;
- coin.previousY = coin.y;
- coin.x += coin.vx * delta;
- coin.y += coin.vy * delta;
+ if (coin.destroyed) return;
+ coin.previousX = coin.x;
+ coin.previousY = coin.y;
+ coin.x += coin.vx * delta;
+ coin.y += coin.vy * delta;
- if (gameState.perks.wind) {
- coin.vx +=
- ((gameState.puckPosition -
- (gameState.offsetX + gameState.gameZoneWidth / 2)) /
- gameState.gameZoneWidth) *
- gameState.perks.wind *
- 0.5;
- }
+ if (gameState.perks.wind) {
+ coin.vx +=
+ ((gameState.puckPosition -
+ (gameState.offsetX + gameState.gameZoneWidth / 2)) /
+ gameState.gameZoneWidth) *
+ gameState.perks.wind *
+ 0.5;
+ }
- let vhit = 0,
- hhit = 0;
+ let vhit = 0,
+ hhit = 0;
- if (
- coin.x < gameState.offsetXRoundedDown + radius &&
- gameState.perks.left_is_lava < 2
- ) {
- coin.x =
- gameState.offsetXRoundedDown +
- radius +
- (gameState.offsetXRoundedDown + radius - coin.x);
- coin.vx *= -1;
- hhit = 1;
- }
- if (coin.y < radius && gameState.perks.top_is_lava < 2) {
- coin.y = radius + (radius - coin.y);
- coin.vy *= -1;
- vhit = 1;
- }
- if (
- coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius &&
- gameState.perks.right_is_lava < 2
- ) {
- coin.x =
- gameState.canvasWidth -
- gameState.offsetXRoundedDown -
- radius -
- (coin.x -
- (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
- coin.vx *= -1;
- hhit = 1;
- }
+ if (
+ coin.x < gameState.offsetXRoundedDown + radius &&
+ gameState.perks.left_is_lava < 2
+ ) {
+ coin.x =
+ gameState.offsetXRoundedDown +
+ radius +
+ (gameState.offsetXRoundedDown + radius - coin.x);
+ coin.vx *= -1;
+ hhit = 1;
+ }
+ if (coin.y < radius && gameState.perks.top_is_lava < 2) {
+ coin.y = radius + (radius - coin.y);
+ coin.vy *= -1;
+ vhit = 1;
+ }
+ if (
+ coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius &&
+ gameState.perks.right_is_lava < 2
+ ) {
+ coin.x =
+ gameState.canvasWidth -
+ gameState.offsetXRoundedDown -
+ radius -
+ (coin.x -
+ (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
+ coin.vx *= -1;
+ hhit = 1;
+ }
- return hhit + vhit * 2;
+ return hhit + vhit * 2;
}
export function gameStateTick(
- gameState: GameState,
- // How many frames to compute at once, can go above 1 to compensate lag
- frames = 1,
+ gameState: GameState,
+ // How many frames to compute at once, can go above 1 to compensate lag
+ frames = 1,
) {
- // Going to the next level or getting a game over in a previous sub-tick would pause the game
- if (!gameState.running) {
- return;
- }
- // Ai movement of puck
- if (gameState.startParams.computer_controlled) computerControl(gameState);
+ // Going to the next level or getting a game over in a previous sub-tick would pause the game
+ if (!gameState.running) {
+ return;
+ }
+ // Ai movement of puck
+ if (gameState.startParams.computer_controlled) computerControl(gameState);
- gameState.runStatistics.max_combo = Math.max(
- gameState.runStatistics.max_combo,
- gameState.combo,
+ gameState.runStatistics.max_combo = Math.max(
+ gameState.runStatistics.max_combo,
+ gameState.combo,
+ );
+ gameState.lastCombo = gameState.combo;
+ zenTick(gameState);
+
+ if (
+ gameState.perks.addiction &&
+ gameState.lastBrickBroken &&
+ gameState.lastBrickBroken <
+ gameState.levelTime - 5000 / gameState.perks.addiction
+ ) {
+ resetCombo(
+ gameState,
+ gameState.puckPosition,
+ gameState.gameZoneHeight - gameState.puckHeight * 2,
);
- gameState.lastCombo = gameState.combo;
- zenTick(gameState);
+ }
- if (
- gameState.perks.addiction &&
- gameState.lastBrickBroken &&
- gameState.lastBrickBroken <
- gameState.levelTime - 5000 / gameState.perks.addiction
- ) {
- resetCombo(
- gameState,
- gameState.puckPosition,
- gameState.gameZoneHeight - gameState.puckHeight * 2,
- );
- }
+ gameState.balls = gameState.balls.filter((ball) => !ball.destroyed);
+ const remainingBricks = gameState.bricks.filter(
+ (b) => b && b !== "black",
+ ).length;
- gameState.balls = gameState.balls.filter((ball) => !ball.destroyed);
- const remainingBricks = gameState.bricks.filter(
- (b) => b && b !== "black",
- ).length;
+ if (!remainingBricks && gameState.lastBrickBroken) {
+ // Avoid a combo reset just because we're waiting for coins
+ gameState.lastBrickBroken = 0;
+ }
- if (!remainingBricks && gameState.lastBrickBroken) {
- // Avoid a combo reset just because we're waiting for coins
- gameState.lastBrickBroken = 0;
- }
-
- if (gameState.perks.hot_start) {
- if (gameState.combo === baseCombo(gameState)) {
- // Give 1s of time between catching a coin and tick down
- gameState.lastTickDown = gameState.levelTime;
- } else if (gameState.levelTime > gameState.lastTickDown + 1000) {
- gameState.lastTickDown = gameState.levelTime;
- offsetCombo(
- gameState,
- -gameState.perks.hot_start,
- gameState.puckPosition,
- gameState.gameZoneHeight - 2 * gameState.puckHeight,
- );
- }
- }
-
- if (
- (window.location.search.includes("skipplaying") ||
- remainingBricks <= gameState.perks.skip_last) &&
- !gameState.autoCleanUses
- ) {
- gameState.bricks.forEach((type, index) => {
- if (type) {
- explodeBrick(gameState, index, gameState.balls[0], true);
- }
- });
- gameState.autoCleanUses++;
- }
-
- const hasPendingBricks = liveCount(gameState.respawns);
-
- if (!remainingBricks && !hasPendingBricks) {
- if (!gameState.winAt) {
- gameState.winAt = gameState.levelTime + 5000;
- }
- } else {
- gameState.winAt = 0;
- }
-
- if (
- // Lost ball while waiting to win, will level up for fairness
- (gameState.winAt && !gameState.balls.find(b => !b.destroyed)) ||
- // Delayed win when coins are still flying
- (gameState.winAt && gameState.levelTime > gameState.winAt) ||
- // instant win condition
- (gameState.levelTime &&
- !remainingBricks &&
- !hasPendingBricks &&
- !liveCount(gameState.coins))
- ) {
- if (gameState.startParams.computer_controlled) {
- startComputerControlledGame(gameState.startParams.stress);
- } else if (gameState.currentLevel + 1 < max_levels(gameState)) {
- setLevel(gameState, gameState.currentLevel + 1);
- } else {
- gameOver(
- t("gameOver.win.title"),
- t("gameOver.win.summary", {score: gameState.score}),
- );
- }
- } else {
- const coinRadius = Math.round(gameState.coinSize / 2);
-
- forEachLiveOne(gameState.coins, (coin, coinIndex) => {
- if (gameState.perks.coin_magnet) {
- const strength =
- (100 /
- (100 +
- Math.pow(coin.y - gameState.gameZoneHeight, 2) +
- Math.pow(coin.x - gameState.puckPosition, 2))) *
- gameState.perks.coin_magnet;
-
- const attractionX =
- frames * (gameState.puckPosition - coin.x) * strength;
-
- coin.vx += attractionX;
- coin.vy +=
- (frames * (gameState.gameZoneHeight - coin.y) * strength) / 2;
- coin.sa -= attractionX / 10;
- }
-
- if (gameState.perks.ball_attracts_coins && gameState.balls.length) {
- // Find closest ball
- let closestBall = getClosestBall(gameState, coin.x, coin.y);
- if (closestBall) {
- let dist = distance2(closestBall, coin);
-
- const minDist = gameState.brickWidth * gameState.brickWidth;
- if (
- dist > minDist &&
- dist < minDist * 4 * 4 * gameState.perks.ball_attracts_coins
- ) {
- // Slow down coins in effect radius
- const ratio =
- 1 - 0.02 * (0.5 + gameState.perks.ball_attracts_coins);
- coin.vx *= ratio;
- coin.vy *= ratio;
- coin.vy *= ratio;
- // Carry them
- const dx =
- ((closestBall.x - coin.x) / dist) *
- 50 *
- gameState.perks.ball_attracts_coins;
- const dy =
- ((closestBall.y - coin.y) / dist) *
- 50 *
- gameState.perks.ball_attracts_coins;
- coin.vx += dx;
- coin.vy += dy;
-
- if (
- !isOptionOn("basic") &&
- Math.random() * gameState.perks.ball_attracts_coins * frames > 0.9
- ) {
- makeParticle(
- gameState,
- coin.x + dx * 5,
- coin.y + dy * 5,
- dx * 2,
- dy * 2,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 100,
- );
- }
- }
- }
- }
-
- if (gameState.perks.bricks_attract_coins) {
- goToNearestBrick(
- gameState,
- coin,
- gameState.perks.bricks_attract_coins * frames,
- 2,
- false,
- );
- }
-
- const ratio =
- 1 -
- ((gameState.perks.viscosity * 0.03 +
- 0.002 +
- (coin.y > gameState.gameZoneHeight ? 0.2 : 0)) *
- frames) /
- (1 + gameState.perks.etherealcoins);
-
- if (!gameState.perks.etherealcoins) {
- coin.vy *= ratio;
- coin.vx *= ratio;
- }
- if (
- coin.y > gameState.gameZoneHeight &&
- coin.floatingTime < gameState.perks.buoy * 30
- ) {
- coin.floatingTime += frames;
- coin.vy -= 1.5;
- }
-
- if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
- if (coin.vx < -7 * gameState.baseSpeed)
- coin.vx = -7 * gameState.baseSpeed;
- if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed;
- if (coin.vy < -7 * gameState.baseSpeed)
- coin.vy = -7 * gameState.baseSpeed;
- coin.a += coin.sa;
-
- // Gravity
- const flip =
- gameState.perks.helium > 0 &&
- Math.abs(coin.x - gameState.puckPosition) * 2 >
- gameState.puckWidth + coin.size;
- let dvy =
- frames *
- coin.weight *
- 0.8 *
- (flip ? 1 - gameState.perks.helium * 0.6 : 1);
-
- if (gameState.perks.etherealcoins) {
- if (gameState.perks.helium) {
- dvy *= 0.2 / gameState.perks.etherealcoins;
- } else {
- dvy *= 0;
- }
- }
-
- coin.vy += dvy;
-
- if (
- gameState.perks.helium &&
- !isOptionOn("basic") &&
- Math.random() < 0.1 * frames
- ) {
- makeParticle(
- gameState,
- coin.x,
- coin.y,
- 0,
- dvy * 10,
- getCoinRenderColor(gameState, coin),
- true,
- 5,
- 250,
- );
- }
-
- const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
-
- const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
-
- if (
- gameState.perks.wrap_left > 1 &&
- hitBorder % 2 &&
- coin.previousX < gameState.offsetX + gameState.gameZoneWidth / 2
- ) {
- schedulGameSound(gameState, "plouf", coin.x, 1);
- coin.x =
- gameState.offsetX + gameState.gameZoneWidth - gameState.coinSize;
- if (coin.vx > 0) {
- coin.vx *= -1;
- }
- spawnParticlesExplosion(gameState, 3, coin.x, coin.y, "#6262EA");
- spawnParticlesImplosion(
- gameState,
- 3,
- coin.previousX,
- coin.previousY,
- "#6262EA",
- );
- }
-
- if (
- gameState.perks.wrap_right > 1 &&
- hitBorder % 2 &&
- coin.previousX > canvasCenterX(gameState)
- ) {
- schedulGameSound(gameState, "plouf", coin.x, 1);
- coin.x = gameState.offsetX + gameState.coinSize;
-
- if (coin.vx < 0) {
- coin.vx *= -1;
- }
- spawnParticlesExplosion(gameState, 3, coin.x, coin.y, "#6262EA");
- spawnParticlesImplosion(
- gameState,
- 3,
- coin.previousX,
- coin.previousY,
- "#6262EA",
- );
- }
-
- if (
- coin.previousY < gameState.gameZoneHeight &&
- coin.y > gameState.gameZoneHeight &&
- coin.vy > 0 &&
- speed > 20 &&
- !coin.floatingTime
- ) {
- schedulGameSound(
- gameState,
- "plouf",
- coin.x,
- (clamp(speed, 20, 100) / 100) * 0.2,
- );
- if (!isOptionOn("basic")) {
- makeParticle(
- gameState,
- coin.x,
- gameState.gameZoneHeight,
- -coin.vx / 5,
- -coin.vy / 5,
- getCoinRenderColor(gameState, coin),
- false,
- );
- }
-
- if (gameState.perks.compound_interest && !gameState.perks.buoy) {
- // If you dont have buoy, we directly declare the coin "lost" to make it clear
- resetCombo(gameState, coin.x, coin.y);
- }
- }
-
- if (
- coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
- coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy &&
- Math.abs(coin.x - gameState.puckPosition) <
- coinRadius +
- gameState.puckWidth / 2 +
- // a bit of margin to be nice , negative in case it's a negative coin
- gameState.puckHeight * (coin.points ? 1 : -1) &&
- !isMovingWhilePassiveIncome(gameState)
- ) {
- addToScore(gameState, coin);
- destroy(gameState.coins, coinIndex);
- } else if (
- coin.y > gameState.canvasHeight + coinRadius * 10 ||
- coin.y < -coinRadius * 10 ||
- coin.x < -coinRadius * 10 ||
- coin.x > gameState.canvasWidth + coinRadius * 10
- ) {
- gameState.levelLostCoins += coin.points;
- destroy(gameState.coins, coinIndex);
- if (gameState.perks.compound_interest && gameState.perks.buoy) {
- // If you have buoy, we wait a bit more before declaring a coin "lost"
- resetCombo(gameState, coin.x, coin.y);
- }
-
- if (
- gameState.combo < gameState.perks.fountain_toss * 30 &&
- Math.random() / coin.points <
- (1 / gameState.combo) * gameState.perks.fountain_toss
- ) {
- offsetCombo(gameState, 1, coin.x, coin.y);
- }
- }
-
- const positionBeforeBrickBounceX = coin.x;
- const positionBeforeBrickBounceY = coin.y;
- const hitBrick = coinBrickHitCheck(gameState, coin);
- if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
- if (
- gameState.bricks[hitBrick] &&
- coin.color !== gameState.bricks[hitBrick] &&
- gameState.bricks[hitBrick] !== "black" &&
- coin.metamorphosisPoints
- ) {
- // Not using setbrick because we don't want to reset HP
- gameState.bricks[hitBrick] = coin.color;
- coin.metamorphosisPoints--;
- schedulGameSound(gameState, "colorChange", coin.x, 0.3);
- }
- }
-
- if (
- gameState.perks.sticky_coins &&
- typeof hitBrick !== "undefined" &&
- (coin.color === gameState.bricks[hitBrick] ||
- gameState.perks.sticky_coins > 1)
- ) {
- if (coin.collidedLastFrame) {
- coin.x = coin.previousX;
- coin.y = coin.previousY;
- } else {
- coin.x = positionBeforeBrickBounceX;
- coin.y = positionBeforeBrickBounceY;
- }
- coin.vx = 0;
- coin.vy = 0;
- }
-
- // Sound and slow down
- if (
- (!gameState.perks.ghost_coins && typeof hitBrick !== "undefined") ||
- hitBorder
- ) {
- const ratio = 1 - 0.2 / (1 + gameState.perks.etherealcoins);
- coin.vx *= ratio;
- coin.vy *= ratio;
- if (Math.abs(coin.vy) < 1) {
- coin.vy = 0;
- }
- coin.sa *= 0.9;
- if (speed > 20 && !coin.collidedLastFrame) {
- schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
- }
- }
-
- if (
- (gameState.perks.golden_goose && typeof hitBrick !== "undefined") ||
- (gameState.perks.golden_goose > 1 && hitBorder)
- ) {
- const closestBall = getClosestBall(gameState, coin.x, coin.y);
- if (closestBall) {
- spawnParticlesExplosion(gameState, 3, coin.x, coin.y, "#6262EA");
- spawnParticlesImplosion(
- gameState,
- 3,
- closestBall.x,
- closestBall.y,
- "#6262EA",
- );
- coin.x = closestBall.x;
- coin.y = closestBall.y;
- }
- }
-
- // remember collision
- coin.collidedLastFrame = !!(typeof hitBrick !== "undefined" || hitBorder);
- });
-
- gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
-
- if (gameState.perks.shocks) {
- gameState.balls.forEach((a, ai) =>
- gameState.balls.forEach((b, bi) => {
- if (
- ai < bi &&
- !a.destroyed &&
- !b.destroyed &&
- distance2(a, b) < gameState.ballSize * gameState.ballSize
- ) {
- // switch speeds
- let tempVx = a.vx;
- let tempVy = a.vy;
- a.vx = b.vx;
- a.vy = b.vy;
- b.vx = tempVx;
- b.vy = tempVy;
- // Compute center
- let x = (a.x + b.x) / 2;
- let y = (a.y + b.y) / 2;
- // space out the balls with extra speed
- if (gameState.perks.shocks > 1) {
- const limit = (gameState.baseSpeed * gameState.perks.shocks) / 2;
- a.vx +=
- clamp(a.x - x, -limit, limit) +
- ((Math.random() - 0.5) * limit) / 3;
- a.vy +=
- clamp(a.y - y, -limit, limit) +
- ((Math.random() - 0.5) * limit) / 3;
- b.vx +=
- clamp(b.x - x, -limit, limit) +
- ((Math.random() - 0.5) * limit) / 3;
- b.vy +=
- clamp(b.y - y, -limit, limit) +
- ((Math.random() - 0.5) * limit) / 3;
- }
- let index = brickIndex(x, y);
- explosionAt(
- gameState,
- index,
- x,
- y,
- a,
- Math.max(0, gameState.perks.shocks - 1),
- );
- }
- }),
- );
- }
-
- if (gameState.perks.wind) {
- const windD =
- ((gameState.puckPosition -
- canvasCenterX(gameState)) /
- gameState.gameZoneWidth) *
- 2 *
- gameState.perks.wind;
- for (let i = 0; i < gameState.perks.wind; i++) {
- if (Math.random() * Math.abs(windD) > 0.5) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown +
- Math.random() * gameState.gameZoneWidthRoundedUp,
- Math.random() * gameState.gameZoneHeight,
- windD * 8,
- 0,
- rainbowColor(),
- true,
- gameState.coinSize / 2,
- 150,
- );
- }
- }
- }
- forEachLiveOne(gameState.particles, (flash, index) => {
- flash.x += flash.vx * frames;
- flash.y += flash.vy * frames;
- if (!flash.ethereal) {
- flash.vy += 0.5 * frames;
- if (hasBrick(brickIndex(flash.x, flash.y))) {
- destroy(gameState.particles, index);
- }
- }
- });
- }
-
- if (
- gameState.combo > baseCombo(gameState) &&
- !isOptionOn("basic") &&
- (gameState.combo - baseCombo(gameState)) * Math.random() * frames > 5
- ) {
- // The red should still be visible on a white bg
-
- if (gameState.perks.top_is_lava == 1) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown +
- Math.random() * gameState.gameZoneWidthRoundedUp,
- 0,
- (Math.random() - 0.5) * 10,
- 5,
- "#FF0000",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
-
- if (gameState.perks.left_is_lava == 1) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown,
- Math.random() * gameState.gameZoneHeight,
- 5,
- (Math.random() - 0.5) * 10,
- "#FF0000",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
-
- if (gameState.perks.right_is_lava == 1) {
- makeParticle(
- gameState,
- gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp,
- Math.random() * gameState.gameZoneHeight,
- -5,
- (Math.random() - 0.5) * 10,
- "#FF0000",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
-
- if (gameState.perks.compound_interest) {
- let x = gameState.puckPosition,
- attemps = 0;
- do {
- x =
- gameState.offsetXRoundedDown +
- gameState.gameZoneWidthRoundedUp * Math.random();
- attemps++;
- } while (
- Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 &&
- attemps < 10
- );
-
- makeParticle(
- gameState,
- x,
- gameState.gameZoneHeight,
- (Math.random() - 0.5) * 10,
- -5,
- "#FF0000",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
- if (
- gameState.perks.streak_shots &&
- !isMovingWhilePassiveIncome(gameState)
- ) {
- const pos = 0.5 - Math.random();
- makeParticle(
- gameState,
- gameState.puckPosition + gameState.puckWidth * pos,
- gameState.gameZoneHeight - gameState.puckHeight,
- pos * 10,
- -5,
- "#FF0000",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
- }
-
- if (
- gameState.perks.wrap_left &&
- gameState.perks.left_is_lava < 2 &&
- Math.random() * frames > 0.1
- ) {
- makeParticle(
- gameState,
- zoneLeftBorderX(gameState),
- Math.random() * gameState.gameZoneHeight,
- 5,
- (Math.random() - 0.5) * 10,
- "#6262EA",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
- if (
- gameState.perks.wrap_right &&
- gameState.perks.right_is_lava < 2 &&
- Math.random() * frames > 0.1
- ) {
- makeParticle(
- gameState,
- zoneRightBorderX(gameState),
- Math.random() * gameState.gameZoneHeight,
- -5,
- (Math.random() - 0.5) * 10,
- "#6262EA",
- true,
- gameState.coinSize / 2,
- 100 * (Math.random() + 1),
- );
- }
-
- // Respawn what's needed, show particles
- forEachLiveOne(gameState.respawns, (r, ri) => {
- if (gameState.bricks[r.index]) {
- destroy(gameState.respawns, ri);
- } else if (gameState.levelTime > r.time) {
- setBrick(gameState, r.index, r.color);
- destroy(gameState.respawns, ri);
- } else {
- const {index, color} = r;
- const vertical = Math.random() > 0.5;
- const dx = Math.random() > 0.5 ? 1 : -1;
- const dy = Math.random() > 0.5 ? 1 : -1;
-
- makeParticle(
- gameState,
- brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2,
- brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2,
- vertical ? 0 : -dx * gameState.baseSpeed,
- vertical ? -dy * gameState.baseSpeed : 0,
- color,
- true,
- gameState.coinSize / 2,
- 250,
- );
- }
- });
-
- forEachLiveOne(gameState.particles, (p, pi) => {
- if (gameState.levelTime > p.time + p.duration) {
- destroy(gameState.particles, pi);
- }
- });
- forEachLiveOne(gameState.texts, (p, pi) => {
- if (gameState.levelTime > p.time + p.duration) {
- destroy(gameState.texts, pi);
- }
- });
- forEachLiveOne(gameState.lights, (p, pi) => {
- if (gameState.levelTime > p.time + p.duration) {
- destroy(gameState.lights, pi);
- }
- });
-}
-
-export function ballTick(gameState: GameState, ball: Ball, frames: number) {
- ball.previousVX = ball.vx;
- ball.previousVY = ball.vy;
-
- let speedLimitDampener =
- 1 +
- gameState.perks.telekinesis +
- gameState.perks.ball_repulse_ball +
- gameState.perks.puck_repulse_ball +
- gameState.perks.ball_attract_ball;
-
- // Speed changes
-
- if (telekinesisEffectRate(gameState, ball) > 0) {
- speedLimitDampener += 3;
- ball.vx +=
- ((gameState.puckPosition - ball.x) / 1000) *
- frames *
- gameState.perks.telekinesis *
- telekinesisEffectRate(gameState, ball);
- }
- if (yoyoEffectRate(gameState, ball) > 0) {
- speedLimitDampener += 3;
-
- ball.vx +=
- (gameState.puckPosition > ball.x ? 1 : -1) *
- frames *
- yoyoEffectRate(gameState, ball);
- }
-
- if (ball.hitSinceBounce < gameState.perks.bricks_attract_ball * 3) {
- goToNearestBrick(
- gameState,
- ball,
- gameState.perks.bricks_attract_ball * frames * 0.2,
- 2 + gameState.perks.bricks_attract_ball,
- Math.random() < 0.5 * frames,
- );
- }
-
- if (
- ball.vx * ball.vx + ball.vy * ball.vy <
- gameState.baseSpeed * gameState.baseSpeed * 2
- ) {
- ball.vx *= 1 + 0.02 / speedLimitDampener;
- ball.vy *= 1 + 0.02 / speedLimitDampener;
- } else {
- ball.vx *= 1 - 0.02 / speedLimitDampener;
- ball.vy *= 1 - 0.02 / speedLimitDampener;
- }
-
- // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract
- if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) {
- ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener;
- }
-
- if (gameState.perks.ball_repulse_ball) {
- for (let b2 of gameState.balls) {
- // avoid computing this twice, and repulsing itself
- if (b2.x >= ball.x) continue;
- repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true);
- }
- }
-
- if (gameState.perks.ball_attract_ball) {
- for (let b2 of gameState.balls) {
- // avoid computing this twice, and repulsing itself
- if (b2.x >= ball.x) continue;
- attract(gameState, ball, b2, gameState.perks.ball_attract_ball);
- }
- }
-
- if (
- gameState.perks.puck_repulse_ball &&
- !isMovingWhilePassiveIncome(gameState) &&
- Math.abs(ball.x - gameState.puckPosition) <
- gameState.puckWidth / 2 +
- (gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10
- ) {
- repulse(
- gameState,
- ball,
- {
- x: gameState.puckPosition,
- y: gameState.gameZoneHeight,
- },
- gameState.perks.puck_repulse_ball + 1,
- false,
- );
- }
-
- if (gameState.perks.steering) {
- const delta = gameState.puckPosition - gameState.lastPuckPosition;
- if (Math.abs(delta) > 1) {
- const angle =
- Math.atan2(ball.vy, ball.vx) +
- ((((delta / gameState.gameZoneWidth) * Math.PI) / 2) *
- gameState.perks.steering *
- frames) /
- 2;
- const d = Math.sqrt(ball.vy * ball.vy + ball.vx * ball.vx);
- ball.vy = Math.sin(angle) * d;
- ball.vx = Math.cos(angle) * d;
- if (Math.random() < frames && !isOptionOn('basic')) {
- makeParticle(gameState, ball.x, ball.y, -ball.vx / 10, -ball.vy / 10,
- '#6262EA', true, 8, 500)
- }
- }
- }
-
- // Bounces
-
- const borderHitCode = bordersHitCheck(
+ if (gameState.perks.hot_start) {
+ if (gameState.combo === baseCombo(gameState)) {
+ // Give 1s of time between catching a coin and tick down
+ gameState.lastTickDown = gameState.levelTime;
+ } else if (gameState.levelTime > gameState.lastTickDown + 1000) {
+ gameState.lastTickDown = gameState.levelTime;
+ offsetCombo(
gameState,
- ball,
- gameState.ballSize / 2,
- frames,
- );
- if (borderHitCode) {
- ball.sidesHitsSinceBounce++;
- if (ball.sidesHitsSinceBounce <= gameState.perks.three_cushion * 3) {
- offsetCombo(gameState, 1, ball.x, ball.y);
- }
- if (
- gameState.perks.wrap_left &&
- borderHitCode % 2 &&
- // x might be moved by wrap so we rely on previousX
- ball.previousX < gameState.offsetX + gameState.gameZoneWidth / 2
- ) {
- schedulGameSound(gameState, "plouf", ball.x, 1);
- ball.x = gameState.offsetX + gameState.gameZoneWidth - gameState.ballSize;
- if (ball.vx > 0) {
- ball.vx *= -1;
- }
-
- spawnParticlesExplosion(gameState, 7, ball.x, ball.y, "#6262EA");
- spawnParticlesImplosion(
- gameState,
- 7,
- ball.previousX,
- ball.previousY,
- "#6262EA",
- );
- }
-
- if (
- gameState.perks.wrap_right &&
- borderHitCode % 2 &&
- // x might be moved by wrap so we rely on previousX
- ball.previousX > gameState.offsetX + gameState.gameZoneWidth / 2
- ) {
- schedulGameSound(gameState, "plouf", ball.x, 1);
- ball.x = gameState.offsetX + gameState.ballSize;
-
- if (ball.vx < 0) {
- ball.vx *= -1;
- }
-
- spawnParticlesExplosion(gameState, 7, ball.x, ball.y, "#6262EA");
- spawnParticlesImplosion(
- gameState,
- 7,
- ball.previousX,
- ball.previousY,
- "#6262EA",
- );
- }
-
- if (
- gameState.perks.left_is_lava &&
- borderHitCode % 2 &&
- // x might be moved by wrap so we rely on previousX
- ball.previousX < gameState.offsetX + gameState.gameZoneWidth / 2
- ) {
- resetCombo(gameState, ball.x, ball.y);
- }
-
- if (
- gameState.perks.right_is_lava &&
- borderHitCode % 2 &&
- // x might be moved by wrap so we rely on previousX
- ball.previousX > gameState.offsetX + gameState.gameZoneWidth / 2
- ) {
- resetCombo(gameState, ball.x, ball.y);
- }
-
- if (gameState.perks.top_is_lava && borderHitCode >= 2) {
- resetCombo(gameState, ball.x, ball.y);
- }
- if (gameState.perks.trampoline) {
- offsetCombo(gameState, -gameState.perks.trampoline, ball.x, ball.y);
- }
-
- schedulGameSound(gameState, "wallBeep", ball.x, 1);
- gameState.levelWallBounces++;
- gameState.runStatistics.wall_bounces++;
+ -gameState.perks.hot_start,
+ gameState.puckPosition,
+ gameState.gameZoneHeight - 2 * gameState.puckHeight,
+ );
}
+ }
- // Puck collision
- const ylimit =
- gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2;
- const ballIsUnderPuck =
- Math.abs(ball.x - gameState.puckPosition) <
- gameState.ballSize / 2 + gameState.puckWidth / 2 &&
- !isMovingWhilePassiveIncome(gameState);
- if (
- ball.y > ylimit &&
- ball.vy > 0 &&
- (ballIsUnderPuck ||
- (gameState.balls.length < 2 &&
- gameState.perks.extra_life &&
- ball.y > ylimit + gameState.puckHeight / 2))
- ) {
- if (ballIsUnderPuck) {
- const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
- const angle = Math.atan2(
- -gameState.puckWidth / 2,
- (ball.x - gameState.puckPosition) *
- (gameState.perks.concave_puck
- ? -1 / (1 + gameState.perks.concave_puck)
- : 1),
- );
- ball.vx = speed * Math.cos(angle);
- ball.vy = speed * Math.sin(angle);
- schedulGameSound(gameState, "wallBeep", ball.x, 1);
- } else {
- ball.vy *= -1;
- justLostALife(gameState, ball, ball.x, ball.y);
- }
- if (gameState.perks.streak_shots) {
- resetCombo(gameState, ball.x, ball.y);
- }
+ if (
+ (window.location.search.includes("skipplaying") ||
+ remainingBricks <= gameState.perks.skip_last) &&
+ !gameState.autoCleanUses
+ ) {
+ gameState.bricks.forEach((type, index) => {
+ if (type) {
+ explodeBrick(gameState, index, gameState.balls[0], true);
+ }
+ });
+ gameState.autoCleanUses++;
+ }
- offsetCombo(
- gameState,
- gameState.perks.trampoline +
- gameState.perks.happy_family * Math.max(0, gameState.balls.length - 1),
- ball.x,
- ball.y,
- );
+ const hasPendingBricks = liveCount(gameState.respawns);
- if (
- gameState.perks.nbricks &&
- ball.hitSinceBounce < gameState.perks.nbricks
- ) {
- resetCombo(gameState, ball.x, ball.y);
- }
-
- if (!ball.hitSinceBounce && gameState.bricks.find((i) => i)) {
- gameState.runStatistics.misses++;
- if (gameState.perks.forgiving) {
- const loss = Math.floor(
- (gameState.levelMisses / 10 / gameState.perks.forgiving) *
- (gameState.combo - baseCombo(gameState)),
- );
- offsetCombo(gameState, -loss, ball.x, ball.y);
- } else {
- resetCombo(gameState, ball.x, ball.y);
- }
- gameState.levelMisses++;
- makeText(
- gameState,
- gameState.puckPosition,
- gameState.gameZoneHeight - gameState.puckHeight * 2,
- "#FF0000",
- t("play.missed_ball"),
- gameState.puckHeight,
- 500,
- );
- }
- gameState.runStatistics.puck_bounces++;
- ball.hitSinceBounce = 0;
- ball.brokenSinceBounce = 0;
- ball.sidesHitsSinceBounce = 0;
- ball.sapperUses = 0;
- ball.piercePoints = gameState.perks.pierce * 3;
+ if (!remainingBricks && !hasPendingBricks) {
+ if (!gameState.winAt) {
+ gameState.winAt = gameState.levelTime + 5000;
}
+ } else {
+ gameState.winAt = 0;
+ }
- if (
- ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
- ball.y < -gameState.gameZoneHeight ||
- ball.x < -gameState.gameZoneHeight ||
- ball.x > gameState.canvasWidth + gameState.gameZoneHeight
- ) {
- ball.destroyed = true;
- gameState.runStatistics.balls_lost++;
- if (gameState.perks.happy_family) {
- resetCombo(gameState, ball.x, ball.y);
- }
- // If you loose a ball while waiting to level up, setLevel is called and pauses the game
- // In that case it's ok to not have any ball, don't game over
- if (!gameState.balls.find((b) => !b.destroyed) && gameState.running && !gameState.winAt) {
-
- if (gameState.startParams.computer_controlled) {
- startComputerControlledGame(gameState.startParams.stress);
- } else {
- gameOver(
- t("gameOver.lost.title"),
- t("gameOver.lost.summary", {score: gameState.score}),
- );
- }
- }
+ if (
+ // Lost ball while waiting to win, will level up for fairness
+ (gameState.winAt && !gameState.balls.find((b) => !b.destroyed)) ||
+ // Delayed win when coins are still flying
+ (gameState.winAt && gameState.levelTime > gameState.winAt) ||
+ // instant win condition
+ (gameState.levelTime &&
+ !remainingBricks &&
+ !hasPendingBricks &&
+ !liveCount(gameState.coins))
+ ) {
+ if (gameState.startParams.computer_controlled) {
+ startComputerControlledGame(gameState.startParams.stress);
+ } else if (gameState.currentLevel + 1 < max_levels(gameState)) {
+ setLevel(gameState, gameState.currentLevel + 1);
+ } else {
+ gameOver(
+ t("gameOver.win.title"),
+ t("gameOver.win.summary", { score: gameState.score }),
+ );
}
- const radius = gameState.ballSize / 2;
- // Make ball/coin bonce, and return bricks that were hit
- const {x, y, previousX, previousY} = ball;
+ } else {
+ const coinRadius = Math.round(gameState.coinSize / 2);
- const vhit = hitsSomething(previousX, y, radius);
- const hhit = hitsSomething(x, previousY, radius);
- const chit =
- (typeof vhit == "undefined" &&
- typeof hhit == "undefined" &&
- hitsSomething(x, y, radius)) ||
- undefined;
+ forEachLiveOne(gameState.coins, (coin, coinIndex) => {
+ if (gameState.perks.coin_magnet) {
+ const strength =
+ (100 /
+ (100 +
+ Math.pow(coin.y - gameState.gameZoneHeight, 2) +
+ Math.pow(coin.x - gameState.puckPosition, 2))) *
+ gameState.perks.coin_magnet;
- const hitBrick = vhit ?? hhit ?? chit;
+ const attractionX =
+ frames * (gameState.puckPosition - coin.x) * strength;
- if (typeof hitBrick !== "undefined") {
- const initialBrickColor = gameState.bricks[hitBrick];
- ball.hitSinceBounce++;
+ coin.vx += attractionX;
+ coin.vy +=
+ (frames * (gameState.gameZoneHeight - coin.y) * strength) / 2;
+ coin.sa -= attractionX / 10;
+ }
- if (!ball.sidesHitsSinceBounce && gameState.perks.three_cushion) {
- resetCombo(gameState, ball.x, ball.y);
- }
- if (gameState.perks.nbricks) {
- if (ball.hitSinceBounce > gameState.perks.nbricks) {
- resetCombo(gameState, ball.x, ball.y);
- } else {
- offsetCombo(gameState, gameState.perks.nbricks, ball.x, ball.y);
- }
- // We need to reset at each hit, otherwise it's just an OP version of single puck hit streak
- }
+ if (gameState.perks.ball_attracts_coins && gameState.balls.length) {
+ // Find closest ball
+ let closestBall = getClosestBall(gameState, coin.x, coin.y);
+ if (closestBall) {
+ let dist = distance2(closestBall, coin);
- let pierce = false;
- let damage =
- 1 +
- (shouldPierceByColor(gameState, vhit, hhit, chit)
- ? gameState.perks.pierce_color
- : 0);
+ const minDist = gameState.brickWidth * gameState.brickWidth;
+ if (
+ dist > minDist &&
+ dist < minDist * 4 * 4 * gameState.perks.ball_attracts_coins
+ ) {
+ // Slow down coins in effect radius
+ const ratio =
+ 1 - 0.02 * (0.5 + gameState.perks.ball_attracts_coins);
+ coin.vx *= ratio;
+ coin.vy *= ratio;
+ coin.vy *= ratio;
+ // Carry them
+ const dx =
+ ((closestBall.x - coin.x) / dist) *
+ 50 *
+ gameState.perks.ball_attracts_coins;
+ const dy =
+ ((closestBall.y - coin.y) / dist) *
+ 50 *
+ gameState.perks.ball_attracts_coins;
+ coin.vx += dx;
+ coin.vy += dy;
- gameState.brickHP[hitBrick] -= damage;
-
- const used = Math.min(
- ball.piercePoints,
- Math.max(1, gameState.brickHP[hitBrick] + 1),
- );
- gameState.brickHP[hitBrick] -= used;
- ball.piercePoints -= used;
-
- if (gameState.brickHP[hitBrick] < 0) {
- gameState.brickHP[hitBrick] = 0;
- pierce = true;
- }
- if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
- if (!pierce) {
- ball.y = ball.previousY;
- ball.vy *= -1;
- }
- }
- if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
- if (!pierce) {
- ball.x = ball.previousX;
- ball.vx *= -1;
- }
- }
-
- if (!gameState.brickHP[hitBrick]) {
- ball.brokenSinceBounce++;
- applyOttawaTreatyPerk(gameState, hitBrick, ball);
- explodeBrick(gameState, hitBrick, ball, false);
if (
- ball.sapperUses < gameState.perks.sapper &&
- initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
- !gameState.bricks[hitBrick]
+ !isOptionOn("basic") &&
+ Math.random() * gameState.perks.ball_attracts_coins * frames > 0.9
) {
- setBrick(gameState, hitBrick, "black");
- ball.sapperUses++;
- }
- } else {
- schedulGameSound(gameState, "wallBeep", x, 1);
- makeLight(
+ makeParticle(
gameState,
- brickCenterX(gameState, hitBrick),
- brickCenterY(gameState, hitBrick),
- "#FFFFFF",
- gameState.brickWidth + 2,
- 50 * gameState.brickHP[hitBrick],
- );
- }
- }
-
- if (
- !isOptionOn("basic") &&
- ballTransparency(ball, gameState) < Math.random()
- ) {
- const remainingPierce = ball.piercePoints;
- const remainingSapper = ball.sapperUses < gameState.perks.sapper;
- const willMiss = ball.vy > 0 && !ball.hitSinceBounce;
- const extraCombo = gameState.combo - 1;
-
- if (
- willMiss ||
- (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) ||
- (remainingSapper && Math.random() > 0.1 / (1 + remainingSapper)) ||
- (extraCombo && Math.random() > 0.1 / (1 + extraCombo))
- ) {
- const color =
- (remainingSapper && (Math.random() > 0.5 ? "#ffb92a" : "#FF0000")) ||
- (willMiss && "#FF0000") ||
- gameState.ballsColor;
-
- makeParticle(
- gameState,
- ball.x,
- ball.y,
- gameState.perks.pierce_color || remainingPierce
- ? -ball.vx + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
- : (Math.random() - 0.5) * gameState.baseSpeed,
- gameState.perks.pierce_color || remainingPierce
- ? -ball.vy + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
- : (Math.random() - 0.5) * gameState.baseSpeed,
- color,
+ coin.x + dx * 5,
+ coin.y + dy * 5,
+ dx * 2,
+ dy * 2,
+ rainbowColor(),
true,
gameState.coinSize / 2,
100,
- );
+ );
+ }
+ }
}
+ }
+
+ if (gameState.perks.bricks_attract_coins) {
+ goToNearestBrick(
+ gameState,
+ coin,
+ gameState.perks.bricks_attract_coins * frames,
+ 2,
+ false,
+ );
+ }
+
+ const ratio =
+ 1 -
+ ((gameState.perks.viscosity * 0.03 +
+ 0.002 +
+ (coin.y > gameState.gameZoneHeight ? 0.2 : 0)) *
+ frames) /
+ (1 + gameState.perks.etherealcoins);
+
+ if (!gameState.perks.etherealcoins) {
+ coin.vy *= ratio;
+ coin.vx *= ratio;
+ }
+ if (
+ coin.y > gameState.gameZoneHeight &&
+ coin.floatingTime < gameState.perks.buoy * 30
+ ) {
+ coin.floatingTime += frames;
+ coin.vy -= 1.5;
+ }
+
+ if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
+ if (coin.vx < -7 * gameState.baseSpeed)
+ coin.vx = -7 * gameState.baseSpeed;
+ if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed;
+ if (coin.vy < -7 * gameState.baseSpeed)
+ coin.vy = -7 * gameState.baseSpeed;
+ coin.a += coin.sa;
+
+ // Gravity
+ const flip =
+ gameState.perks.helium > 0 &&
+ Math.abs(coin.x - gameState.puckPosition) * 2 >
+ gameState.puckWidth + coin.size;
+ let dvy =
+ frames *
+ coin.weight *
+ 0.8 *
+ (flip ? 1 - gameState.perks.helium * 0.6 : 1);
+
+ if (gameState.perks.etherealcoins) {
+ if (gameState.perks.helium) {
+ dvy *= 0.2 / gameState.perks.etherealcoins;
+ } else {
+ dvy *= 0;
+ }
+ }
+
+ coin.vy += dvy;
+
+ if (
+ gameState.perks.helium &&
+ !isOptionOn("basic") &&
+ Math.random() < 0.1 * frames
+ ) {
+ makeParticle(
+ gameState,
+ coin.x,
+ coin.y,
+ 0,
+ dvy * 10,
+ getCoinRenderColor(gameState, coin),
+ true,
+ 5,
+ 250,
+ );
+ }
+
+ const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
+
+ const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
+
+ if (
+ gameState.perks.wrap_left > 1 &&
+ hitBorder % 2 &&
+ coin.previousX < gameState.offsetX + gameState.gameZoneWidth / 2
+ ) {
+ schedulGameSound(gameState, "plouf", coin.x, 1);
+ coin.x =
+ gameState.offsetX + gameState.gameZoneWidth - gameState.coinSize;
+ if (coin.vx > 0) {
+ coin.vx *= -1;
+ }
+ spawnParticlesExplosion(gameState, 3, coin.x, coin.y, "#6262EA");
+ spawnParticlesImplosion(
+ gameState,
+ 3,
+ coin.previousX,
+ coin.previousY,
+ "#6262EA",
+ );
+ }
+
+ if (
+ gameState.perks.wrap_right > 1 &&
+ hitBorder % 2 &&
+ coin.previousX > canvasCenterX(gameState)
+ ) {
+ schedulGameSound(gameState, "plouf", coin.x, 1);
+ coin.x = gameState.offsetX + gameState.coinSize;
+
+ if (coin.vx < 0) {
+ coin.vx *= -1;
+ }
+ spawnParticlesExplosion(gameState, 3, coin.x, coin.y, "#6262EA");
+ spawnParticlesImplosion(
+ gameState,
+ 3,
+ coin.previousX,
+ coin.previousY,
+ "#6262EA",
+ );
+ }
+
+ if (
+ coin.previousY < gameState.gameZoneHeight &&
+ coin.y > gameState.gameZoneHeight &&
+ coin.vy > 0 &&
+ speed > 20 &&
+ !coin.floatingTime
+ ) {
+ schedulGameSound(
+ gameState,
+ "plouf",
+ coin.x,
+ (clamp(speed, 20, 100) / 100) * 0.2,
+ );
+ if (!isOptionOn("basic")) {
+ makeParticle(
+ gameState,
+ coin.x,
+ gameState.gameZoneHeight,
+ -coin.vx / 5,
+ -coin.vy / 5,
+ getCoinRenderColor(gameState, coin),
+ false,
+ );
+ }
+
+ if (gameState.perks.compound_interest && !gameState.perks.buoy) {
+ // If you dont have buoy, we directly declare the coin "lost" to make it clear
+ resetCombo(gameState, coin.x, coin.y);
+ }
+ }
+
+ if (
+ coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
+ coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy &&
+ Math.abs(coin.x - gameState.puckPosition) <
+ coinRadius +
+ gameState.puckWidth / 2 +
+ // a bit of margin to be nice , negative in case it's a negative coin
+ gameState.puckHeight * (coin.points ? 1 : -1) &&
+ !isMovingWhilePassiveIncome(gameState)
+ ) {
+ addToScore(gameState, coin);
+ destroy(gameState.coins, coinIndex);
+ } else if (
+ coin.y > gameState.canvasHeight + coinRadius * 10 ||
+ coin.y < -coinRadius * 10 ||
+ coin.x < -coinRadius * 10 ||
+ coin.x > gameState.canvasWidth + coinRadius * 10
+ ) {
+ gameState.levelLostCoins += coin.points;
+ destroy(gameState.coins, coinIndex);
+ if (gameState.perks.compound_interest && gameState.perks.buoy) {
+ // If you have buoy, we wait a bit more before declaring a coin "lost"
+ resetCombo(gameState, coin.x, coin.y);
+ }
+
+ if (
+ gameState.combo < gameState.perks.fountain_toss * 30 &&
+ Math.random() / coin.points <
+ (1 / gameState.combo) * gameState.perks.fountain_toss
+ ) {
+ offsetCombo(gameState, 1, coin.x, coin.y);
+ }
+ }
+
+ const positionBeforeBrickBounceX = coin.x;
+ const positionBeforeBrickBounceY = coin.y;
+ const hitBrick = coinBrickHitCheck(gameState, coin);
+ if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
+ if (
+ gameState.bricks[hitBrick] &&
+ coin.color !== gameState.bricks[hitBrick] &&
+ gameState.bricks[hitBrick] !== "black" &&
+ coin.metamorphosisPoints
+ ) {
+ // Not using setbrick because we don't want to reset HP
+ gameState.bricks[hitBrick] = coin.color;
+ coin.metamorphosisPoints--;
+ schedulGameSound(gameState, "colorChange", coin.x, 0.3);
+ }
+ }
+
+ if (
+ gameState.perks.sticky_coins &&
+ typeof hitBrick !== "undefined" &&
+ (coin.color === gameState.bricks[hitBrick] ||
+ gameState.perks.sticky_coins > 1)
+ ) {
+ if (coin.collidedLastFrame) {
+ coin.x = coin.previousX;
+ coin.y = coin.previousY;
+ } else {
+ coin.x = positionBeforeBrickBounceX;
+ coin.y = positionBeforeBrickBounceY;
+ }
+ coin.vx = 0;
+ coin.vy = 0;
+ }
+
+ // Sound and slow down
+ if (
+ (!gameState.perks.ghost_coins && typeof hitBrick !== "undefined") ||
+ hitBorder
+ ) {
+ const ratio = 1 - 0.2 / (1 + gameState.perks.etherealcoins);
+ coin.vx *= ratio;
+ coin.vy *= ratio;
+ if (Math.abs(coin.vy) < 1) {
+ coin.vy = 0;
+ }
+ coin.sa *= 0.9;
+ if (speed > 20 && !coin.collidedLastFrame) {
+ schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
+ }
+ }
+
+ if (
+ (gameState.perks.golden_goose && typeof hitBrick !== "undefined") ||
+ (gameState.perks.golden_goose > 1 && hitBorder)
+ ) {
+ const closestBall = getClosestBall(gameState, coin.x, coin.y);
+ if (closestBall) {
+ spawnParticlesExplosion(gameState, 3, coin.x, coin.y, "#6262EA");
+ spawnParticlesImplosion(
+ gameState,
+ 3,
+ closestBall.x,
+ closestBall.y,
+ "#6262EA",
+ );
+ coin.x = closestBall.x;
+ coin.y = closestBall.y;
+ }
+ }
+
+ // remember collision
+ coin.collidedLastFrame = !!(typeof hitBrick !== "undefined" || hitBorder);
+ });
+
+ gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
+
+ if (gameState.perks.shocks) {
+ gameState.balls.forEach((a, ai) =>
+ gameState.balls.forEach((b, bi) => {
+ if (
+ ai < bi &&
+ !a.destroyed &&
+ !b.destroyed &&
+ distance2(a, b) < gameState.ballSize * gameState.ballSize
+ ) {
+ // switch speeds
+ let tempVx = a.vx;
+ let tempVy = a.vy;
+ a.vx = b.vx;
+ a.vy = b.vy;
+ b.vx = tempVx;
+ b.vy = tempVy;
+ // Compute center
+ let x = (a.x + b.x) / 2;
+ let y = (a.y + b.y) / 2;
+ // space out the balls with extra speed
+ if (gameState.perks.shocks > 1) {
+ const limit = (gameState.baseSpeed * gameState.perks.shocks) / 2;
+ a.vx +=
+ clamp(a.x - x, -limit, limit) +
+ ((Math.random() - 0.5) * limit) / 3;
+ a.vy +=
+ clamp(a.y - y, -limit, limit) +
+ ((Math.random() - 0.5) * limit) / 3;
+ b.vx +=
+ clamp(b.x - x, -limit, limit) +
+ ((Math.random() - 0.5) * limit) / 3;
+ b.vy +=
+ clamp(b.y - y, -limit, limit) +
+ ((Math.random() - 0.5) * limit) / 3;
+ }
+ let index = brickIndex(x, y);
+ explosionAt(
+ gameState,
+ index,
+ x,
+ y,
+ a,
+ Math.max(0, gameState.perks.shocks - 1),
+ );
+ }
+ }),
+ );
}
+ if (gameState.perks.wind) {
+ const windD =
+ ((gameState.puckPosition - canvasCenterX(gameState)) /
+ gameState.gameZoneWidth) *
+ 2 *
+ gameState.perks.wind;
+ for (let i = 0; i < gameState.perks.wind; i++) {
+ if (Math.random() * Math.abs(windD) > 0.5) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown +
+ Math.random() * gameState.gameZoneWidthRoundedUp,
+ Math.random() * gameState.gameZoneHeight,
+ windD * 8,
+ 0,
+ rainbowColor(),
+ true,
+ gameState.coinSize / 2,
+ 150,
+ );
+ }
+ }
+ }
+ forEachLiveOne(gameState.particles, (flash, index) => {
+ flash.x += flash.vx * frames;
+ flash.y += flash.vy * frames;
+ if (!flash.ethereal) {
+ flash.vy += 0.5 * frames;
+ if (hasBrick(brickIndex(flash.x, flash.y))) {
+ destroy(gameState.particles, index);
+ }
+ }
+ });
+ }
+
+ if (
+ gameState.combo > baseCombo(gameState) &&
+ !isOptionOn("basic") &&
+ (gameState.combo - baseCombo(gameState)) * Math.random() * frames > 5
+ ) {
+ // The red should still be visible on a white bg
+
+ if (gameState.perks.top_is_lava == 1) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown +
+ Math.random() * gameState.gameZoneWidthRoundedUp,
+ 0,
+ (Math.random() - 0.5) * 10,
+ 5,
+ "#FF0000",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+
+ if (gameState.perks.left_is_lava == 1) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown,
+ Math.random() * gameState.gameZoneHeight,
+ 5,
+ (Math.random() - 0.5) * 10,
+ "#FF0000",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+
+ if (gameState.perks.right_is_lava == 1) {
+ makeParticle(
+ gameState,
+ gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp,
+ Math.random() * gameState.gameZoneHeight,
+ -5,
+ (Math.random() - 0.5) * 10,
+ "#FF0000",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+
+ if (gameState.perks.compound_interest) {
+ let x = gameState.puckPosition,
+ attemps = 0;
+ do {
+ x =
+ gameState.offsetXRoundedDown +
+ gameState.gameZoneWidthRoundedUp * Math.random();
+ attemps++;
+ } while (
+ Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 &&
+ attemps < 10
+ );
+
+ makeParticle(
+ gameState,
+ x,
+ gameState.gameZoneHeight,
+ (Math.random() - 0.5) * 10,
+ -5,
+ "#FF0000",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+ if (
+ gameState.perks.streak_shots &&
+ !isMovingWhilePassiveIncome(gameState)
+ ) {
+ const pos = 0.5 - Math.random();
+ makeParticle(
+ gameState,
+ gameState.puckPosition + gameState.puckWidth * pos,
+ gameState.gameZoneHeight - gameState.puckHeight,
+ pos * 10,
+ -5,
+ "#FF0000",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+ }
+
+ if (
+ gameState.perks.wrap_left &&
+ gameState.perks.left_is_lava < 2 &&
+ Math.random() * frames > 0.1
+ ) {
+ makeParticle(
+ gameState,
+ zoneLeftBorderX(gameState),
+ Math.random() * gameState.gameZoneHeight,
+ 5,
+ (Math.random() - 0.5) * 10,
+ "#6262EA",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+ if (
+ gameState.perks.wrap_right &&
+ gameState.perks.right_is_lava < 2 &&
+ Math.random() * frames > 0.1
+ ) {
+ makeParticle(
+ gameState,
+ zoneRightBorderX(gameState),
+ Math.random() * gameState.gameZoneHeight,
+ -5,
+ (Math.random() - 0.5) * 10,
+ "#6262EA",
+ true,
+ gameState.coinSize / 2,
+ 100 * (Math.random() + 1),
+ );
+ }
+
+ // Respawn what's needed, show particles
+ forEachLiveOne(gameState.respawns, (r, ri) => {
+ if (gameState.bricks[r.index]) {
+ destroy(gameState.respawns, ri);
+ } else if (gameState.levelTime > r.time) {
+ setBrick(gameState, r.index, r.color);
+ destroy(gameState.respawns, ri);
+ } else {
+ const { index, color } = r;
+ const vertical = Math.random() > 0.5;
+ const dx = Math.random() > 0.5 ? 1 : -1;
+ const dy = Math.random() > 0.5 ? 1 : -1;
+
+ makeParticle(
+ gameState,
+ brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2,
+ brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2,
+ vertical ? 0 : -dx * gameState.baseSpeed,
+ vertical ? -dy * gameState.baseSpeed : 0,
+ color,
+ true,
+ gameState.coinSize / 2,
+ 250,
+ );
+ }
+ });
+
+ forEachLiveOne(gameState.particles, (p, pi) => {
+ if (gameState.levelTime > p.time + p.duration) {
+ destroy(gameState.particles, pi);
+ }
+ });
+ forEachLiveOne(gameState.texts, (p, pi) => {
+ if (gameState.levelTime > p.time + p.duration) {
+ destroy(gameState.texts, pi);
+ }
+ });
+ forEachLiveOne(gameState.lights, (p, pi) => {
+ if (gameState.levelTime > p.time + p.duration) {
+ destroy(gameState.lights, pi);
+ }
+ });
+}
+
+export function ballTick(gameState: GameState, ball: Ball, frames: number) {
+ ball.previousVX = ball.vx;
+ ball.previousVY = ball.vy;
+
+ let speedLimitDampener =
+ 1 +
+ gameState.perks.telekinesis +
+ gameState.perks.ball_repulse_ball +
+ gameState.perks.puck_repulse_ball +
+ gameState.perks.ball_attract_ball;
+
+ // Speed changes
+
+ if (telekinesisEffectRate(gameState, ball) > 0) {
+ speedLimitDampener += 3;
+ ball.vx +=
+ ((gameState.puckPosition - ball.x) / 1000) *
+ frames *
+ gameState.perks.telekinesis *
+ telekinesisEffectRate(gameState, ball);
+ }
+ if (yoyoEffectRate(gameState, ball) > 0) {
+ speedLimitDampener += 3;
+
+ ball.vx +=
+ (gameState.puckPosition > ball.x ? 1 : -1) *
+ frames *
+ yoyoEffectRate(gameState, ball);
+ }
+
+ if (ball.hitSinceBounce < gameState.perks.bricks_attract_ball * 3) {
+ goToNearestBrick(
+ gameState,
+ ball,
+ gameState.perks.bricks_attract_ball * frames * 0.2,
+ 2 + gameState.perks.bricks_attract_ball,
+ Math.random() < 0.5 * frames,
+ );
+ }
+
+ if (
+ ball.vx * ball.vx + ball.vy * ball.vy <
+ gameState.baseSpeed * gameState.baseSpeed * 2
+ ) {
+ ball.vx *= 1 + 0.02 / speedLimitDampener;
+ ball.vy *= 1 + 0.02 / speedLimitDampener;
+ } else {
+ ball.vx *= 1 - 0.02 / speedLimitDampener;
+ ball.vy *= 1 - 0.02 / speedLimitDampener;
+ }
+
+ // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract
+ if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) {
+ ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener;
+ }
+
+ if (gameState.perks.ball_repulse_ball) {
+ for (let b2 of gameState.balls) {
+ // avoid computing this twice, and repulsing itself
+ if (b2.x >= ball.x) continue;
+ repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true);
+ }
+ }
+
+ if (gameState.perks.ball_attract_ball) {
+ for (let b2 of gameState.balls) {
+ // avoid computing this twice, and repulsing itself
+ if (b2.x >= ball.x) continue;
+ attract(gameState, ball, b2, gameState.perks.ball_attract_ball);
+ }
+ }
+
+ if (
+ gameState.perks.puck_repulse_ball &&
+ !isMovingWhilePassiveIncome(gameState) &&
+ Math.abs(ball.x - gameState.puckPosition) <
+ gameState.puckWidth / 2 +
+ (gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10
+ ) {
+ repulse(
+ gameState,
+ ball,
+ {
+ x: gameState.puckPosition,
+ y: gameState.gameZoneHeight,
+ },
+ gameState.perks.puck_repulse_ball + 1,
+ false,
+ );
+ }
+
+ if (gameState.perks.steering) {
+ const delta = gameState.puckPosition - gameState.lastPuckPosition;
+ if (Math.abs(delta) > 1) {
+ const angle =
+ Math.atan2(ball.vy, ball.vx) +
+ ((((delta / gameState.gameZoneWidth) * Math.PI) / 2) *
+ gameState.perks.steering *
+ frames) /
+ 2;
+ const d = Math.sqrt(ball.vy * ball.vy + ball.vx * ball.vx);
+ ball.vy = Math.sin(angle) * d;
+ ball.vx = Math.cos(angle) * d;
+ if (Math.random() < frames && !isOptionOn("basic")) {
+ makeParticle(
+ gameState,
+ ball.x,
+ ball.y,
+ -ball.vx / 10,
+ -ball.vy / 10,
+ "#6262EA",
+ true,
+ 8,
+ 500,
+ );
+ }
+ }
+ }
+
+ // Bounces
+
+ const borderHitCode = bordersHitCheck(
+ gameState,
+ ball,
+ gameState.ballSize / 2,
+ frames,
+ );
+ if (borderHitCode) {
+ ball.sidesHitsSinceBounce++;
+ if (ball.sidesHitsSinceBounce <= gameState.perks.three_cushion * 3) {
+ offsetCombo(gameState, 1, ball.x, ball.y);
+ }
+ if (
+ gameState.perks.wrap_left &&
+ borderHitCode % 2 &&
+ // x might be moved by wrap so we rely on previousX
+ ball.previousX < gameState.offsetX + gameState.gameZoneWidth / 2
+ ) {
+ schedulGameSound(gameState, "plouf", ball.x, 1);
+ ball.x = gameState.offsetX + gameState.gameZoneWidth - gameState.ballSize;
+ if (ball.vx > 0) {
+ ball.vx *= -1;
+ }
+
+ spawnParticlesExplosion(gameState, 7, ball.x, ball.y, "#6262EA");
+ spawnParticlesImplosion(
+ gameState,
+ 7,
+ ball.previousX,
+ ball.previousY,
+ "#6262EA",
+ );
+ }
+
+ if (
+ gameState.perks.wrap_right &&
+ borderHitCode % 2 &&
+ // x might be moved by wrap so we rely on previousX
+ ball.previousX > gameState.offsetX + gameState.gameZoneWidth / 2
+ ) {
+ schedulGameSound(gameState, "plouf", ball.x, 1);
+ ball.x = gameState.offsetX + gameState.ballSize;
+
+ if (ball.vx < 0) {
+ ball.vx *= -1;
+ }
+
+ spawnParticlesExplosion(gameState, 7, ball.x, ball.y, "#6262EA");
+ spawnParticlesImplosion(
+ gameState,
+ 7,
+ ball.previousX,
+ ball.previousY,
+ "#6262EA",
+ );
+ }
+
+ if (
+ gameState.perks.left_is_lava &&
+ borderHitCode % 2 &&
+ // x might be moved by wrap so we rely on previousX
+ ball.previousX < gameState.offsetX + gameState.gameZoneWidth / 2
+ ) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+
+ if (
+ gameState.perks.right_is_lava &&
+ borderHitCode % 2 &&
+ // x might be moved by wrap so we rely on previousX
+ ball.previousX > gameState.offsetX + gameState.gameZoneWidth / 2
+ ) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+
+ if (gameState.perks.top_is_lava && borderHitCode >= 2) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+ if (gameState.perks.trampoline) {
+ offsetCombo(gameState, -gameState.perks.trampoline, ball.x, ball.y);
+ }
+
+ schedulGameSound(gameState, "wallBeep", ball.x, 1);
+ gameState.levelWallBounces++;
+ gameState.runStatistics.wall_bounces++;
+ }
+
+ // Puck collision
+ const ylimit =
+ gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2;
+ const ballIsUnderPuck =
+ Math.abs(ball.x - gameState.puckPosition) <
+ gameState.ballSize / 2 + gameState.puckWidth / 2 &&
+ !isMovingWhilePassiveIncome(gameState);
+ if (
+ ball.y > ylimit &&
+ ball.vy > 0 &&
+ (ballIsUnderPuck ||
+ (gameState.balls.length < 2 &&
+ gameState.perks.extra_life &&
+ ball.y > ylimit + gameState.puckHeight / 2))
+ ) {
+ if (ballIsUnderPuck) {
+ const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
+ const angle = Math.atan2(
+ -gameState.puckWidth / 2,
+ (ball.x - gameState.puckPosition) *
+ (gameState.perks.concave_puck
+ ? -1 / (1 + gameState.perks.concave_puck)
+ : 1),
+ );
+ ball.vx = speed * Math.cos(angle);
+ ball.vy = speed * Math.sin(angle);
+ schedulGameSound(gameState, "wallBeep", ball.x, 1);
+ } else {
+ ball.vy *= -1;
+ justLostALife(gameState, ball, ball.x, ball.y);
+ }
+ if (gameState.perks.streak_shots) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+
+ offsetCombo(
+ gameState,
+ gameState.perks.trampoline +
+ gameState.perks.happy_family * Math.max(0, gameState.balls.length - 1),
+ ball.x,
+ ball.y,
+ );
+
+ if (
+ gameState.perks.nbricks &&
+ ball.hitSinceBounce < gameState.perks.nbricks
+ ) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+
+ if (!ball.hitSinceBounce && gameState.bricks.find((i) => i)) {
+ gameState.runStatistics.misses++;
+ if (gameState.perks.forgiving) {
+ const loss = Math.floor(
+ (gameState.levelMisses / 10 / gameState.perks.forgiving) *
+ (gameState.combo - baseCombo(gameState)),
+ );
+ offsetCombo(gameState, -loss, ball.x, ball.y);
+ } else {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+ gameState.levelMisses++;
+ makeText(
+ gameState,
+ gameState.puckPosition,
+ gameState.gameZoneHeight - gameState.puckHeight * 2,
+ "#FF0000",
+ t("play.missed_ball"),
+ gameState.puckHeight,
+ 500,
+ );
+ }
+ gameState.runStatistics.puck_bounces++;
+ ball.hitSinceBounce = 0;
+ ball.brokenSinceBounce = 0;
+ ball.sidesHitsSinceBounce = 0;
+ ball.sapperUses = 0;
+ ball.piercePoints = gameState.perks.pierce * 3;
+ }
+
+ if (
+ ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
+ ball.y < -gameState.gameZoneHeight ||
+ ball.x < -gameState.gameZoneHeight ||
+ ball.x > gameState.canvasWidth + gameState.gameZoneHeight
+ ) {
+ ball.destroyed = true;
+ gameState.runStatistics.balls_lost++;
+ if (gameState.perks.happy_family) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+ // If you loose a ball while waiting to level up, setLevel is called and pauses the game
+ // In that case it's ok to not have any ball, don't game over
+ if (
+ !gameState.balls.find((b) => !b.destroyed) &&
+ gameState.running &&
+ !gameState.winAt
+ ) {
+ if (gameState.startParams.computer_controlled) {
+ startComputerControlledGame(gameState.startParams.stress);
+ } else {
+ gameOver(
+ t("gameOver.lost.title"),
+ t("gameOver.lost.summary", { score: gameState.score }),
+ );
+ }
+ }
+ }
+ const radius = gameState.ballSize / 2;
+ // Make ball/coin bonce, and return bricks that were hit
+ const { x, y, previousX, previousY } = ball;
+
+ const vhit = hitsSomething(previousX, y, radius);
+ const hhit = hitsSomething(x, previousY, radius);
+ const chit =
+ (typeof vhit == "undefined" &&
+ typeof hhit == "undefined" &&
+ hitsSomething(x, y, radius)) ||
+ undefined;
+
+ const hitBrick = vhit ?? hhit ?? chit;
+
+ if (typeof hitBrick !== "undefined") {
+ const initialBrickColor = gameState.bricks[hitBrick];
+ ball.hitSinceBounce++;
+
+ if (!ball.sidesHitsSinceBounce && gameState.perks.three_cushion) {
+ resetCombo(gameState, ball.x, ball.y);
+ }
+ if (gameState.perks.nbricks) {
+ if (ball.hitSinceBounce > gameState.perks.nbricks) {
+ resetCombo(gameState, ball.x, ball.y);
+ } else {
+ offsetCombo(gameState, gameState.perks.nbricks, ball.x, ball.y);
+ }
+ // We need to reset at each hit, otherwise it's just an OP version of single puck hit streak
+ }
+
+ let pierce = false;
+ let damage =
+ 1 +
+ (shouldPierceByColor(gameState, vhit, hhit, chit)
+ ? gameState.perks.pierce_color
+ : 0);
+
+ gameState.brickHP[hitBrick] -= damage;
+
+ const used = Math.min(
+ ball.piercePoints,
+ Math.max(1, gameState.brickHP[hitBrick] + 1),
+ );
+ gameState.brickHP[hitBrick] -= used;
+ ball.piercePoints -= used;
+
+ if (gameState.brickHP[hitBrick] < 0) {
+ gameState.brickHP[hitBrick] = 0;
+ pierce = true;
+ }
+ if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
+ if (!pierce) {
+ ball.y = ball.previousY;
+ ball.vy *= -1;
+ }
+ }
+ if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
+ if (!pierce) {
+ ball.x = ball.previousX;
+ ball.vx *= -1;
+ }
+ }
+
+ if (!gameState.brickHP[hitBrick]) {
+ ball.brokenSinceBounce++;
+ applyOttawaTreatyPerk(gameState, hitBrick, ball);
+ explodeBrick(gameState, hitBrick, ball, false);
+ if (
+ ball.sapperUses < gameState.perks.sapper &&
+ initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
+ !gameState.bricks[hitBrick]
+ ) {
+ setBrick(gameState, hitBrick, "black");
+ ball.sapperUses++;
+ }
+ } else {
+ schedulGameSound(gameState, "wallBeep", x, 1);
+ makeLight(
+ gameState,
+ brickCenterX(gameState, hitBrick),
+ brickCenterY(gameState, hitBrick),
+ "#FFFFFF",
+ gameState.brickWidth + 2,
+ 50 * gameState.brickHP[hitBrick],
+ );
+ }
+ }
+
+ if (
+ !isOptionOn("basic") &&
+ ballTransparency(ball, gameState) < Math.random()
+ ) {
+ const remainingPierce = ball.piercePoints;
+ const remainingSapper = ball.sapperUses < gameState.perks.sapper;
+ const willMiss = ball.vy > 0 && !ball.hitSinceBounce;
+ const extraCombo = gameState.combo - 1;
+
+ if (
+ willMiss ||
+ (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) ||
+ (remainingSapper && Math.random() > 0.1 / (1 + remainingSapper)) ||
+ (extraCombo && Math.random() > 0.1 / (1 + extraCombo))
+ ) {
+ const color =
+ (remainingSapper && (Math.random() > 0.5 ? "#ffb92a" : "#FF0000")) ||
+ (willMiss && "#FF0000") ||
+ gameState.ballsColor;
+
+ makeParticle(
+ gameState,
+ ball.x,
+ ball.y,
+ gameState.perks.pierce_color || remainingPierce
+ ? -ball.vx + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
+ : (Math.random() - 0.5) * gameState.baseSpeed,
+ gameState.perks.pierce_color || remainingPierce
+ ? -ball.vy + ((Math.random() - 0.5) * gameState.baseSpeed) / 3
+ : (Math.random() - 0.5) * gameState.baseSpeed,
+ color,
+ true,
+ gameState.coinSize / 2,
+ 100,
+ );
+ }
+ }
}
function justLostALife(gameState: GameState, ball: Ball, x: number, y: number) {
- gameState.perks.extra_life -= 1;
- if (gameState.perks.extra_life < 0) {
- gameState.perks.extra_life = 0;
- } else if (gameState.perks.sacrifice) {
- gameState.combo *= gameState.perks.sacrifice;
- gameState.bricks.forEach(
- (color, index) => color && explodeBrick(gameState, index, ball, true),
- );
- }
+ gameState.perks.extra_life -= 1;
+ if (gameState.perks.extra_life < 0) {
+ gameState.perks.extra_life = 0;
+ } else if (gameState.perks.sacrifice) {
+ gameState.combo *= gameState.perks.sacrifice;
+ gameState.bricks.forEach(
+ (color, index) => color && explodeBrick(gameState, index, ball, true),
+ );
+ }
- schedulGameSound(gameState, "lifeLost", ball.x, 1);
+ schedulGameSound(gameState, "lifeLost", ball.x, 1);
- if (!isOptionOn("basic")) {
- for (let i = 0; i < 10; i++)
- makeParticle(
- gameState,
- x,
- y,
- Math.random() * gameState.baseSpeed * 3,
- gameState.baseSpeed * 3,
- "#FF0000",
- false,
- gameState.coinSize / 2,
- 150,
- );
- }
+ if (!isOptionOn("basic")) {
+ for (let i = 0; i < 10; i++)
+ makeParticle(
+ gameState,
+ x,
+ y,
+ Math.random() * gameState.baseSpeed * 3,
+ gameState.baseSpeed * 3,
+ "#FF0000",
+ false,
+ gameState.coinSize / 2,
+ 150,
+ );
+ }
}
function makeCoin(
- gameState: GameState,
- x: number,
- y: number,
- vx: number,
- vy: number,
- color = "#ffd300",
- points = 1,
+ gameState: GameState,
+ x: number,
+ y: number,
+ vx: number,
+ vy: number,
+ color = "#ffd300",
+ points = 1,
) {
- let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
- weight *= 5 / (5 + gameState.perks.etherealcoins);
+ let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
+ weight *= 5 / (5 + gameState.perks.etherealcoins);
- if (gameState.perks.trickledown) y = -20;
- if (
- gameState.perks.rainbow &&
- Math.random() > 1 / (1 + gameState.perks.rainbow)
- )
- color = rainbowColor();
+ if (gameState.perks.trickledown) y = -20;
+ if (
+ gameState.perks.rainbow &&
+ Math.random() > 1 / (1 + gameState.perks.rainbow)
+ )
+ color = rainbowColor();
- append(gameState.coins, (p: Partial) => {
- p.x = x;
- p.y = y;
- p.collidedLastFrame = true;
- p.size = gameState.coinSize;
- p.previousX = x;
- p.previousY = y;
- p.vx = vx;
- p.vy = vy;
- // p.sx = 0;
- // p.sy = 0;
- p.color = color;
- p.a = Math.random() * Math.PI * 2;
- p.sa = Math.random() - 0.5;
- p.points = points;
- p.weight = weight;
- p.metamorphosisPoints = gameState.perks.metamorphosis;
- p.floatingTime = 0;
- });
+ append(gameState.coins, (p: Partial) => {
+ p.x = x;
+ p.y = y;
+ p.collidedLastFrame = true;
+ p.size = gameState.coinSize;
+ p.previousX = x;
+ p.previousY = y;
+ p.vx = vx;
+ p.vy = vy;
+ // p.sx = 0;
+ // p.sy = 0;
+ p.color = color;
+ p.a = Math.random() * Math.PI * 2;
+ p.sa = Math.random() - 0.5;
+ p.points = points;
+ p.weight = weight;
+ p.metamorphosisPoints = gameState.perks.metamorphosis;
+ p.floatingTime = 0;
+ });
}
function makeParticle(
- gameState: GameState,
- x: number,
- y: number,
- vx: number,
- vy: number,
- color: colorString,
- ethereal = false,
- size = 8,
- duration = 150,
+ gameState: GameState,
+ x: number,
+ y: number,
+ vx: number,
+ vy: number,
+ color: colorString,
+ ethereal = false,
+ size = 8,
+ duration = 150,
) {
- append(gameState.particles, (p: Partial) => {
- p.time = gameState.levelTime;
- p.x = x;
- p.y = y;
- p.vx = vx;
- p.vy = vy;
- p.color = color;
- p.size = size;
- p.duration = duration;
- p.ethereal = ethereal;
- });
+ append(gameState.particles, (p: Partial) => {
+ p.time = gameState.levelTime;
+ p.x = x;
+ p.y = y;
+ p.vx = vx;
+ p.vy = vy;
+ p.color = color;
+ p.size = size;
+ p.duration = duration;
+ p.ethereal = ethereal;
+ });
}
function makeText(
- gameState: GameState,
- x: number,
- y: number,
- color: colorString,
- text: string,
- size = 20,
- duration = 500,
+ gameState: GameState,
+ x: number,
+ y: number,
+ color: colorString,
+ text: string,
+ size = 20,
+ duration = 500,
) {
- append(gameState.texts, (p: Partial) => {
- p.time = gameState.levelTime;
- p.x = clamp(x, 20, gameState.canvasWidth - 20);
- p.y = clamp(
- y,
- 40,
- gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize,
- );
- p.color = color;
- p.size = size;
- p.duration = clamp(duration, 400, 2000);
- p.text = text;
- });
+ append(gameState.texts, (p: Partial) => {
+ p.time = gameState.levelTime;
+ p.x = clamp(x, 20, gameState.canvasWidth - 20);
+ p.y = clamp(
+ y,
+ 40,
+ gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize,
+ );
+ p.color = color;
+ p.size = size;
+ p.duration = clamp(duration, 400, 2000);
+ p.text = text;
+ });
}
function makeLight(
- gameState: GameState,
- x: number,
- y: number,
- color: colorString,
- size = 8,
- duration = 150,
+ gameState: GameState,
+ x: number,
+ y: number,
+ color: colorString,
+ size = 8,
+ duration = 150,
) {
- append(gameState.lights, (p: Partial) => {
- p.time = gameState.levelTime;
- p.x = x;
- p.y = y;
- p.color = color;
- p.size = size;
- p.duration = duration;
- });
+ append(gameState.lights, (p: Partial) => {
+ p.time = gameState.levelTime;
+ p.x = x;
+ p.y = y;
+ p.color = color;
+ p.size = size;
+ p.duration = duration;
+ });
}
export function append(
- where: ReusableArray,
- makeItem: (match: Partial) => void,
+ where: ReusableArray,
+ makeItem: (match: Partial) => void,
) {
- while (
- where.list[where.indexMin] &&
- !where.list[where.indexMin].destroyed &&
- where.indexMin < where.list.length
- ) {
- where.indexMin++;
- }
- if (where.indexMin < where.list.length) {
- where.list[where.indexMin].destroyed = false;
- makeItem(where.list[where.indexMin]);
- where.indexMin++;
- } else {
- const p = {destroyed: false};
- makeItem(p);
- where.list.push(p);
- }
- where.total++;
+ while (
+ where.list[where.indexMin] &&
+ !where.list[where.indexMin].destroyed &&
+ where.indexMin < where.list.length
+ ) {
+ where.indexMin++;
+ }
+ if (where.indexMin < where.list.length) {
+ where.list[where.indexMin].destroyed = false;
+ makeItem(where.list[where.indexMin]);
+ where.indexMin++;
+ } else {
+ const p = { destroyed: false };
+ makeItem(p);
+ where.list.push(p);
+ }
+ where.total++;
}
export function destroy(where: ReusableArray, index: number) {
- if (where.list[index].destroyed) return;
- where.list[index].destroyed = true;
- where.indexMin = Math.min(where.indexMin, index);
- where.total--;
+ if (where.list[index].destroyed) return;
+ where.list[index].destroyed = true;
+ where.indexMin = Math.min(where.indexMin, index);
+ where.total--;
}
export function liveCount(where: ReusableArray) {
- return where.total;
+ return where.total;
}
export function empty(where: ReusableArray) {
- let destroyed = 0;
- where.total = 0;
- where.indexMin = 0;
- where.list.forEach((i) => {
- if (!i.destroyed) {
- i.destroyed = true;
- destroyed++;
- }
- });
- return destroyed;
+ let destroyed = 0;
+ where.total = 0;
+ where.indexMin = 0;
+ where.list.forEach((i) => {
+ if (!i.destroyed) {
+ i.destroyed = true;
+ destroyed++;
+ }
+ });
+ return destroyed;
}
export function forEachLiveOne(
- where: ReusableArray,
- cb: (t: T, index: number) => void,
+ where: ReusableArray,
+ cb: (t: T, index: number) => void,
) {
- where.list.forEach((item: T, index: number) => {
- if (item && !item.destroyed) {
- cb(item, index);
- }
- });
+ where.list.forEach((item: T, index: number) => {
+ if (item && !item.destroyed) {
+ cb(item, index);
+ }
+ });
}
function goToNearestBrick(
- gameState: GameState,
- coin: Ball | Coin,
- strength,
- size = 2,
- particle = false,
+ gameState: GameState,
+ coin: Ball | Coin,
+ strength,
+ size = 2,
+ particle = false,
) {
- const row = Math.floor(coin.y / gameState.brickWidth);
- const col = Math.floor((coin.x - gameState.offsetX) / gameState.brickWidth);
- let vx = 0,
- vy = 0;
- for (let dcol = -size; dcol < size; dcol++) {
- for (let drow = -size; drow < size; drow++) {
- const index = getRowColIndex(gameState, row + drow, col + dcol);
- if (gameState.bricks[index]) {
- const dx =
- brickCenterX(gameState, index) +
- (clamp(-dcol, -1, 1) * gameState.brickWidth) / 2 -
- coin.x;
- const dy =
- brickCenterY(gameState, index) +
- (clamp(-drow, -1, 1) * gameState.brickWidth) / 2 -
- coin.y;
- const d2 = dx * dx + dy * dy;
- vx += (dx / d2) * 20;
- vy += (dy / d2) * 20;
- }
- }
+ const row = Math.floor(coin.y / gameState.brickWidth);
+ const col = Math.floor((coin.x - gameState.offsetX) / gameState.brickWidth);
+ let vx = 0,
+ vy = 0;
+ for (let dcol = -size; dcol < size; dcol++) {
+ for (let drow = -size; drow < size; drow++) {
+ const index = getRowColIndex(gameState, row + drow, col + dcol);
+ if (gameState.bricks[index]) {
+ const dx =
+ brickCenterX(gameState, index) +
+ (clamp(-dcol, -1, 1) * gameState.brickWidth) / 2 -
+ coin.x;
+ const dy =
+ brickCenterY(gameState, index) +
+ (clamp(-drow, -1, 1) * gameState.brickWidth) / 2 -
+ coin.y;
+ const d2 = dx * dx + dy * dy;
+ vx += (dx / d2) * 20;
+ vy += (dy / d2) * 20;
+ }
}
+ }
- coin.vx += vx * strength;
- coin.vy += vy * strength;
- const s2 = coin.vx * coin.vx + coin.vy * coin.vy;
- if (s2 > gameState.baseSpeed * gameState.baseSpeed * 2) {
- coin.vx *= 0.95;
- coin.vy *= 0.95;
- }
+ coin.vx += vx * strength;
+ coin.vy += vy * strength;
+ const s2 = coin.vx * coin.vx + coin.vy * coin.vy;
+ if (s2 > gameState.baseSpeed * gameState.baseSpeed * 2) {
+ coin.vx *= 0.95;
+ coin.vy *= 0.95;
+ }
- if ((vx || vy) && particle) {
- makeParticle(
- gameState,
- coin.x,
- coin.y,
- -vx * 2,
- -vy * 2,
- rainbowColor(),
- true,
- );
- }
+ if ((vx || vy) && particle) {
+ makeParticle(
+ gameState,
+ coin.x,
+ coin.y,
+ -vx * 2,
+ -vy * 2,
+ rainbowColor(),
+ true,
+ );
+ }
}
function applyOttawaTreatyPerk(
- gameState: GameState,
- index: number,
- ball: Ball,
+ gameState: GameState,
+ index: number,
+ ball: Ball,
) {
- if (!gameState.perks.ottawa_treaty) return;
- if (ball.sapperUses) return;
+ if (!gameState.perks.ottawa_treaty) return;
+ if (ball.sapperUses) return;
- const originalColor = gameState.bricks[index];
- if (originalColor == "black") return;
- const x = index % gameState.gridSize;
- const y = Math.floor(index / gameState.gridSize);
- let converted = 0;
- for (let dx = -1; dx <= 1; dx++)
- for (let dy = -1; dy <= 1; dy++)
- if (dx || dy) {
- const nIndex = getRowColIndex(gameState, y + dy, x + dx);
- if (gameState.bricks[nIndex] && gameState.bricks[nIndex] === "black") {
- setBrick(gameState, nIndex, originalColor);
- schedulGameSound(
- gameState,
- "colorChange",
- brickCenterX(gameState, index),
- 1,
- );
- // Avoid infinite bricks generation hack
- ball.sapperUses = Infinity;
- converted++;
- // Don't convert more than one brick per hit normally
- if (converted >= gameState.perks.ottawa_treaty) return;
- }
- }
- return;
+ const originalColor = gameState.bricks[index];
+ if (originalColor == "black") return;
+ const x = index % gameState.gridSize;
+ const y = Math.floor(index / gameState.gridSize);
+ let converted = 0;
+ for (let dx = -1; dx <= 1; dx++)
+ for (let dy = -1; dy <= 1; dy++)
+ if (dx || dy) {
+ const nIndex = getRowColIndex(gameState, y + dy, x + dx);
+ if (gameState.bricks[nIndex] && gameState.bricks[nIndex] === "black") {
+ setBrick(gameState, nIndex, originalColor);
+ schedulGameSound(
+ gameState,
+ "colorChange",
+ brickCenterX(gameState, index),
+ 1,
+ );
+ // Avoid infinite bricks generation hack
+ ball.sapperUses = Infinity;
+ converted++;
+ // Don't convert more than one brick per hit normally
+ if (converted >= gameState.perks.ottawa_treaty) return;
+ }
+ }
+ return;
}
export function zenTick(gameState: GameState) {
- if (!gameState.perks.zen) return;
- if (gameState.levelTime > gameState.lastZenComboIncrease + 3000) {
- gameState.lastZenComboIncrease = gameState.levelTime;
- offsetCombo(
- gameState,
- gameState.perks.zen,
- gameState.puckPosition,
- gameState.gameZoneHeight - gameState.puckHeight,
- );
- }
+ if (!gameState.perks.zen) return;
+ if (gameState.levelTime > gameState.lastZenComboIncrease + 3000) {
+ gameState.lastZenComboIncrease = gameState.levelTime;
+ offsetCombo(
+ gameState,
+ gameState.perks.zen,
+ gameState.puckPosition,
+ gameState.gameZoneHeight - gameState.puckHeight,
+ );
+ }
}
diff --git a/src/game_utils.ts b/src/game_utils.ts
index 21c7178..3863a0e 100644
--- a/src/game_utils.ts
+++ b/src/game_utils.ts
@@ -12,7 +12,6 @@ import { t } from "./i18n/i18n";
import { clamp } from "./pure_functions";
import { getSettingValue, getTotalScore } from "./settings";
import { isOptionOn } from "./options";
-import {gameCanvas} from "./render";
export function describeLevel(level: Level) {
let bricks = 0,
@@ -347,13 +346,12 @@ export function escapeAttribute(str: String) {
.replace(/'/gi, "'");
}
-export function canvasCenterX(gameState:GameState){
- return gameState.canvasWidth/2
+export function canvasCenterX(gameState: GameState) {
+ return gameState.canvasWidth / 2;
}
-export function zoneLeftBorderX(gameState:GameState){
-
- return gameState.offsetXRoundedDown - 1
+export function zoneLeftBorderX(gameState: GameState) {
+ return gameState.offsetXRoundedDown - 1;
+}
+export function zoneRightBorderX(gameState: GameState) {
+ return gameState.canvasWidth - gameState.offsetXRoundedDown + 1;
}
-export function zoneRightBorderX(gameState:GameState){
- return gameCanvas.width - gameState.offsetXRoundedDown + 1
-}
\ No newline at end of file
diff --git a/src/help.ts b/src/help.ts
index e39ff94..2c819f0 100644
--- a/src/help.ts
+++ b/src/help.ts
@@ -9,8 +9,6 @@ import {
levelTimeGood,
missesBest,
missesGood,
- wallBouncedBest,
- wallBouncedGood,
} from "./pure_functions";
export function helpMenuEntry() {
diff --git a/src/pure_functions.ts b/src/pure_functions.ts
index 31dbc14..5cec895 100644
--- a/src/pure_functions.ts
+++ b/src/pure_functions.ts
@@ -95,9 +95,7 @@ export function firstWhere(
}
}
-export const wallBouncedBest = 2,
- wallBouncedGood = 7,
- levelTimeBest = 25,
+export const levelTimeBest = 25,
levelTimeGood = 45,
catchRateBest = 98,
catchRateGood = 90,
diff --git a/src/render.ts b/src/render.ts
index 9d2aeed..e13b34d 100644
--- a/src/render.ts
+++ b/src/render.ts
@@ -10,7 +10,9 @@ import {
max_levels,
reachRedRowIndex,
telekinesisEffectRate,
- yoyoEffectRate, zoneLeftBorderX, zoneRightBorderX,
+ yoyoEffectRate,
+ zoneLeftBorderX,
+ zoneRightBorderX,
} from "./game_utils";
import { colorString, GameState } from "./types";
import { t } from "./i18n/i18n";
@@ -26,8 +28,6 @@ import {
levelTimeGood,
missesBest,
missesGood,
- wallBouncedBest,
- wallBouncedGood,
} from "./pure_functions";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
@@ -642,21 +642,36 @@ export function render(gameState: GameState) {
);
}
-
startWork("render:timeout");
- if(gameState.winAt || gameState.startCountDown){
- const remaining = gameState.startCountDown || Math.ceil((gameState.winAt-gameState.levelTime)/1000)
- if(remaining>0 && remaining<5){
- ctx.globalAlpha=1
- ctx.globalCompositeOperation="destination-out";
- drawText(ctx, remaining.toString(), 'white', 65, gameState.canvasWidth/2, gameState.canvasHeight/2)
+ if (gameState.winAt || gameState.startCountDown) {
+ const remaining =
+ gameState.startCountDown ||
+ Math.ceil((gameState.winAt - gameState.levelTime) / 1000);
+ if (remaining > 0 && remaining < 5) {
+ ctx.globalAlpha = 1;
+ ctx.globalCompositeOperation = "destination-out";
+ drawText(
+ ctx,
+ remaining.toString(),
+ "white",
+ 65,
+ gameState.canvasWidth / 2,
+ gameState.canvasHeight / 2,
+ );
- ctx.globalCompositeOperation="screen";
- ctx.globalAlpha=1/remaining
- drawText(ctx, remaining.toString(), 'white', 60, gameState.canvasWidth/2, gameState.canvasHeight/2)
+ ctx.globalCompositeOperation = "screen";
+ ctx.globalAlpha = 1 / remaining;
+ drawText(
+ ctx,
+ remaining.toString(),
+ "white",
+ 60,
+ gameState.canvasWidth / 2,
+ gameState.canvasHeight / 2,
+ );
}
}
-ctx.globalAlpha=1
+ ctx.globalAlpha = 1;
startWork("render:askForWakeLock");
askForWakeLock(gameState);
diff --git a/src/toast.ts b/src/toast.ts
index 05866bb..6df4027 100644
--- a/src/toast.ts
+++ b/src/toast.ts
@@ -3,16 +3,16 @@ div.classList = "hidden toast";
document.body.appendChild(div);
let timeout: NodeJS.Timeout | undefined;
export function toast(html: string, className = "") {
- clearToasts()
+ clearToasts();
div.classList = "toast visible " + className;
div.innerHTML = html;
timeout = setTimeout(clearToasts, 1500);
}
-export function clearToasts(){
+export function clearToasts() {
if (timeout) {
clearTimeout(timeout);
- timeout = undefined
+ timeout = undefined;
}
- div.classList = "hidden toast";
-}
\ No newline at end of file
+ div.classList = "hidden toast";
+}
diff --git a/src/types.d.ts b/src/types.d.ts
index 5e73823..5647d99 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -283,7 +283,7 @@ export type GameState = {
rerolls: number;
creative: boolean;
startParams: RunParams;
- startCountDown:number;
+ startCountDown: number;
};
export type RunParams = {