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 = {