diff --git a/Readme.md b/Readme.md index a201851..f5c5f52 100644 --- a/Readme.md +++ b/Readme.md @@ -3,6 +3,8 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades ! - [Play now](https://breakout.lecaro.me/) +- [Donate](https://paypal.me/renanlecaro) +- [Discord](https://discord.gg/DZSPqyJkwP) - [Post your comments on itch.io](https://renanlecaro.itch.io/breakout71) - [Help and tips about the game](./Help.md) - [Credits](./Credits.md) @@ -10,7 +12,6 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades ! - [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout) - [GitLab](https://gitlab.com/lecarore/breakout71) - [HackerNews thread](https://news.ycombinator.com/item?id=43183131) -- [Donate](https://github.com/sponsors/renanlecaro) # System requirements @@ -22,8 +23,7 @@ There's also an easy mode for kids (slower ball). # Next - -- separate particles by type, reuse coins and particles +- check which color you get if picking a color related perk - sturdy bricks map of remaining hits # bugs @@ -159,6 +159,9 @@ There's also an easy mode for kids (slower ball). - [colin] perk: analyzer - permet de voir les caractéristiques cachées des blocs (sturdy…) - [colin] perk: roulette - gagne instantanément 2 perks aléatoires - let coins go out of bounds left and right, where they'll get lost, but +1 combo per brick +- more combo if no coin catch +- combo climbs every time a ball bounces on puck (but bounce is random?) +- # extra levels diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f8e0ace..3b60b54 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "me.lecaro.breakout" minSdk = 21 targetSdk = 34 - versionCode = 29036807 - versionName = "29036807" + versionCode = 29038230 + versionName = "29038230" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f2a54af..0864465 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,12 @@ - + + Breakout 71 \ No newline at end of file +Breakout 71 \ No newline at end of file diff --git a/app/src/main/java/me/lecaro/breakout/MainActivity.kt b/app/src/main/java/me/lecaro/breakout/MainActivity.kt index 7baecf5..04456e2 100644 --- a/app/src/main/java/me/lecaro/breakout/MainActivity.kt +++ b/app/src/main/java/me/lecaro/breakout/MainActivity.kt @@ -1,8 +1,10 @@ package me.lecaro.breakout + import android.app.Activity import android.app.DownloadManager import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.os.Environment @@ -18,8 +20,11 @@ import android.widget.Toast import java.io.File import java.text.SimpleDateFormat import java.util.Date +import java.util.jar.Manifest const val CHOOSE_FILE_REQUEST_CODE = 548459 +const val PERM_REQUEST_CODE = 66622635 + class MainActivity : android.app.Activity() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -28,13 +33,73 @@ class MainActivity : android.app.Activity() { when (requestCode) { CHOOSE_FILE_REQUEST_CODE -> { if (resultCode == RESULT_OK) { - filePathCallback?.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data)) + filePathCallback?.onReceiveValue( + WebChromeClient.FileChooserParams.parseResult( + resultCode, + data + ) + ) filePathCallback = null } } } } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + when (requestCode) { + PERM_REQUEST_CODE -> { + if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + downloadFile() + } else { + Toast.makeText(this, "We cant make a save file without that permission", Toast.LENGTH_SHORT).show() + } + return + } + } + } var filePathCallback: ValueCallback>? = null + var fileToDownload:String? = null + + fun downloadFile(){ + val url = fileToDownload ?: return + try{ + if (!url.startsWith("data:")) { + Log.w("DL", "url ignored because it does not start with data:") + return + } + val sdf = SimpleDateFormat("yyyy-M-dd-hh-mm") + val currentDate = sdf.format(Date()) + // Extract filename from contentDisposition if available + + + if (url.startsWith("data:application/json;base64,")) { + Log.d("DL", "saving application/json ") + val base64Data = url.substringAfterLast(',') + val decodedBytes = + android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT) + val jsonData = String(decodedBytes); + val dir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val fileName = "breakout-71-save-$currentDate.b71" + val file = File(dir, fileName) + file.writeText(jsonData) + Toast.makeText(this, "Saved in $dir", Toast.LENGTH_LONG).show() + Log.d("DL", "finished saving application/json ") + + } else if (url.startsWith("data:video/webm;base64,")) { + Log.d("DL", "saving video/webm ") + // TODO + Log.d("DL", "finished savign video/webm ") + } else { + Log.w("DL", "unexpected type " + url) + } + }catch (e:Exception){ + Log.e("DL", "Error ${e.message}") + Toast.makeText(this, "Error ${e.message}", Toast.LENGTH_LONG).show() + + } + + + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -46,8 +111,9 @@ class MainActivity : android.app.Activity() { webView.settings.javaScriptEnabled = true webView.settings.domStorageEnabled = true webView.settings.setSupportZoom(false) - webView.loadUrl("file:///android_asset/index.html?isInWebView=true") + webView.loadUrl("file:///android_asset/index.html?isInWebView=true") + val activity=this; webView.webChromeClient = object : WebChromeClient() { override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { @@ -57,43 +123,35 @@ class MainActivity : android.app.Activity() { ) return true } - override fun onShowFileChooser(webView: WebView?, filePathCallback: ValueCallback>?, fileChooserParams: FileChooserParams?): Boolean { - startActivityForResult(fileChooserParams?.createIntent(), CHOOSE_FILE_REQUEST_CODE) - this@MainActivity.filePathCallback = filePathCallback - return true + + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>?, + fileChooserParams: FileChooserParams? + ): Boolean { + try{ + + startActivityForResult(fileChooserParams?.createIntent(), CHOOSE_FILE_REQUEST_CODE) + this@MainActivity.filePathCallback = filePathCallback + return true + }catch (e:Exception){ + Log.e("DL", "Error ${e.message}") + Toast.makeText(activity, "Error ${e.message}", Toast.LENGTH_LONG).show() + + return false + } } } + webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> - if (!url.startsWith("data:")) { - Log.w("DL","url ignored because it does not start with data:") - return@DownloadListener - } - val sdf = SimpleDateFormat("yyyy-M-dd-hh-mm") - val currentDate = sdf.format(Date()) - // Extract filename from contentDisposition if available - - if (url.startsWith("data:application/json;base64,")) { - Log.d("DL","saving application/json ") - val base64Data = url.substringAfterLast(',') - val decodedBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT) - val jsonData = String(decodedBytes) - ; - val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - val fileName = "breakout-71-save-$currentDate.b71" - val file = File(dir, fileName) - file.writeText(jsonData) - Toast.makeText(this, "Saved in $dir", Toast. LENGTH_LONG).show() - Log.d("DL","finished saving application/json ") - - }else if (url.startsWith("data:video/webm;base64,")){ - Log.d("DL","saving video/webm ") - // TODO - Log.d("DL","finished savign video/webm ") + fileToDownload = url + if (activity.checkSelfPermission( android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(arrayOf( android.Manifest.permission.WRITE_EXTERNAL_STORAGE), PERM_REQUEST_CODE) }else{ - Log.w("DL","unexpected type "+url) + downloadFile() } - }) diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index fa0f996..a483a1c 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -1,13 +1,5 @@ - + - \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index 9ee9997..5188608 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -1,19 +1,9 @@ - + - + - \ No newline at end of file diff --git a/build.sh b/build.sh index 226aba9..6fe429c 100755 --- a/build.sh +++ b/build.sh @@ -5,7 +5,6 @@ defaultVersionCode=$(($(date +%s) / 60)) versionCode=${1:-$defaultVersionCode} -# TODO crash without app version source ~/.nvm/nvm.sh; @@ -17,13 +16,6 @@ if [[ $(node --version) != v21* ]]; then exit 1 fi - -if grep -rE "T[O]DO|F[I]XME|console\.log" src -then - echo "You have left some TO""DO or logs" - exit 1 -fi - set -e set -x diff --git a/deploy.sh b/deploy.sh index 9c57a78..407b125 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,5 +1,11 @@ #!/bin/bash +if grep -rE "T[O]DO|F[I]XME|console\.log" src +then + echo "You have left some TO""DO or logs" + exit 1 +fi + set -e set -x diff --git a/dist/PWA/sw-b71.js b/dist/PWA/sw-b71.js index 536cb9e..f4f4e03 100644 --- a/dist/PWA/sw-b71.js +++ b/dist/PWA/sw-b71.js @@ -1,33 +1,2 @@ -// The version of the cache. -const VERSION = "29036807"; -// The name of the cache -const CACHE_NAME = `breakout-71-${VERSION}`; -// The static resources that the app needs to function. -const APP_STATIC_RESOURCES = [ - "/" -]; -// On install, cache the static resources -self.addEventListener("install", (event)=>{ - event.waitUntil((async ()=>{ - const cache = await caches.open(CACHE_NAME); - cache.addAll(APP_STATIC_RESOURCES); - })()); -}); -// delete old caches on activate -self.addEventListener("activate", (event)=>{ - event.waitUntil((async ()=>{ - const names = await caches.keys(); - await Promise.all(names.map((name)=>{ - if (name !== CACHE_NAME) return caches.delete(name); - })); - await clients.claim(); - })()); -}); -self.addEventListener("fetch", (event)=>{ - if (event.request.mode === "navigate" && event.request.url.endsWith("/index.html?isPWA=true")) { - event.respondWith(caches.match("/")); - return; - } -}); - +function e(e,t,n,r,a,i,c){try{var o=e[i](c),u=o.value}catch(e){n(e);return}o.done?t(u):Promise.resolve(u).then(r,a)}function t(t){return function(){var n=this,r=arguments;return new Promise(function(a,i){var c=t.apply(n,r);function o(t){e(c,a,i,o,u,"next",t)}function u(t){e(c,a,i,o,u,"throw",t)}o(void 0)})}}function n(e,t){var n,r,a,i,c={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return i={next:o(0),throw:o(1),return:o(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function o(i){return function(o){return function(i){if(n)throw TypeError("Generator is already executing.");for(;c;)try{if(n=1,r&&(a=2&i[0]?r.return:i[0]?r.throw||((a=r.return)&&a.call(r),0):r.next)&&!(a=a.call(r,i[1])).done)return a;switch(r=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return c.label++,{value:i[1],done:!1};case 5:c.label++,r=i[1],i=[0];continue;case 7:i=c.ops.pop(),c.trys.pop();continue;default:if(!(a=(a=c.trys).length>0&&a[a.length-1])&&(6===i[0]||2===i[0])){c=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1] {\n event.waitUntil(\n (async () => {\n const cache = await caches.open(CACHE_NAME);\n cache.addAll(APP_STATIC_RESOURCES);\n })(),\n );\n});\n\n// delete old caches on activate\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const names = await caches.keys();\n await Promise.all(\n names.map((name) => {\n if (name !== CACHE_NAME) {\n return caches.delete(name);\n }\n }),\n );\n await clients.claim();\n })(),\n );\n});\n\nself.addEventListener(\"fetch\", (event) => {\n if (\n event.request.mode === \"navigate\" &&\n event.request.url.endsWith(\"/index.html?isPWA=true\")\n ) {\n event.respondWith(caches.match(\"/\"));\n return;\n }\n});\n"],"names":[],"version":3,"file":"sw-b71.js.map","sourceRoot":"/__parcel_source_root/"} \ No newline at end of file +{"mappings":"A,S,E,C,C,C,C,C,C,C,C,C,C,C,C,C,E,G,C,I,E,C,C,E,C,G,E,E,K,A,C,M,E,C,E,G,M,C,E,I,C,E,G,Q,O,C,G,I,C,E,E,C,S,E,C,E,O,W,I,E,I,C,E,U,O,I,Q,S,C,C,C,E,I,E,E,K,C,E,G,S,E,C,E,E,E,E,E,E,E,O,E,C,S,E,C,E,E,E,E,E,E,E,Q,E,C,E,K,E,E,C,C,S,E,C,C,C,E,I,E,E,E,E,E,C,M,E,K,W,G,A,E,C,C,E,C,M,C,C,E,C,O,C,C,E,A,E,K,E,C,I,E,A,E,O,E,C,K,E,G,M,E,G,O,E,E,E,A,Y,O,Q,C,C,C,O,Q,C,C,W,O,I,A,C,E,E,S,E,C,E,O,S,C,E,O,A,S,C,E,G,E,M,A,U,mC,K,G,G,C,G,E,E,G,C,E,A,E,C,C,E,C,E,M,C,C,C,E,C,E,K,E,C,A,C,E,E,M,A,G,E,I,C,G,C,E,E,I,A,G,C,A,C,E,E,I,C,E,C,C,E,C,E,I,C,O,E,O,E,E,A,G,C,E,C,A,E,C,C,E,C,E,K,C,A,E,C,C,E,E,K,E,K,E,E,E,K,M,E,O,E,K,G,C,M,C,C,E,C,K,C,C,C,M,E,E,K,G,E,C,C,E,C,E,C,E,C,Q,M,E,E,E,G,C,G,G,E,I,C,G,G,Q,S,G,C,C,E,A,C,E,E,I,A,E,M,C,G,C,C,E,M,C,E,A,G,C,A,I,C,C,E,E,A,I,C,C,E,A,E,C,E,E,Q,C,G,A,I,C,C,E,E,C,C,G,C,C,E,C,C,C,E,E,C,C,E,C,C,C,E,A,E,C,E,K,C,C,C,E,C,K,C,G,A,I,C,C,E,E,E,K,C,C,C,E,C,C,E,K,C,C,C,E,C,E,E,K,C,G,G,E,K,C,C,C,E,C,C,E,K,C,C,C,E,C,E,G,C,I,C,G,K,C,C,C,E,E,E,G,C,G,G,E,I,C,G,G,Q,C,E,E,I,C,E,E,C,M,E,C,E,C,E,E,C,E,C,Q,C,E,E,C,C,G,A,E,C,C,E,C,M,C,C,E,C,M,C,M,C,C,E,C,C,C,E,C,K,E,K,C,C,C,E,C,E,E,C,C,C,CCIA,IAAM,EAAc,eAAsB,MAAA,CAH1B,YAMV,EAAuB,CAAC,IAAI,CAGlC,KAAK,gBAAgB,CAAC,UAAW,SAAC,CAAlC,EACE,EAAM,SAAS,CACb,EAAC,W,O,E,I,C,S,C,E,O,E,K,E,K,EACe,MAAA,C,EAAM,OAAO,IAAI,CAAC,G,A,M,E,OAChC,AADc,EAAR,IAAA,GACA,MAAM,CAAC,G,C,E,A,C,EACf,KAEJ,GAGA,KAAK,gBAAgB,CAAC,WAAY,SAAC,CAAnC,EACE,EAAM,SAAS,CACb,EAAC,W,O,E,I,C,S,C,E,O,E,K,E,K,EACe,MAAA,C,EAAM,OAAO,IAAI,G,A,M,EAC/B,MAAA,C,EAAM,QAAQ,GAAG,CACf,AAFY,EAAR,IAAA,GAEE,GAAG,CAAC,SAAC,CADP,EAEF,GAAI,IAAS,EACX,OAAO,OAAO,MAAM,CAAC,EAEzB,I,A,M,EAEF,OAPA,EAAA,IAAA,GAOA,C,EAAM,QAAQ,KAAK,G,A,M,E,OAAnB,EAAA,IAAA,G,C,E,A,C,EACF,KAEJ,GAEA,KAAK,gBAAgB,CAAC,QAAS,SAAC,CAAhC,EACE,GACE,AAAuB,aAAvB,EAAM,OAAO,CAAC,IAAI,EAClB,EAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,0BAC3B,CACA,EAAM,WAAW,CAAC,OAAO,KAAK,CAAC,MAC/B,MACF,CACF","sources":["","src/PWA/sw-b71.js"],"sourcesContent":["// The version of the cache.\nfunction asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {\n try {\n var info = gen[key](arg);\n var value = info.value;\n } catch (error) {\n reject(error);\n return;\n }\n if (info.done) {\n resolve(value);\n } else {\n Promise.resolve(value).then(_next, _throw);\n }\n}\nfunction _async_to_generator(fn) {\n return function() {\n var self1 = this, args = arguments;\n return new Promise(function(resolve, reject) {\n var gen = fn.apply(self1, args);\n function _next(value) {\n asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"next\", value);\n }\n function _throw(err) {\n asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"throw\", err);\n }\n _next(undefined);\n });\n };\n}\nfunction _ts_generator(thisArg, body) {\n var f, y, t, g, _ = {\n label: 0,\n sent: function() {\n if (t[0] & 1) throw t[1];\n return t[1];\n },\n trys: [],\n ops: []\n };\n return g = {\n next: verb(0),\n \"throw\": verb(1),\n \"return\": verb(2)\n }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() {\n return this;\n }), g;\n function verb(n) {\n return function(v) {\n return step([\n n,\n v\n ]);\n };\n }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while(_)try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [\n op[0] & 2,\n t.value\n ];\n switch(op[0]){\n case 0:\n case 1:\n t = op;\n break;\n case 4:\n _.label++;\n return {\n value: op[1],\n done: false\n };\n case 5:\n _.label++;\n y = op[1];\n op = [\n 0\n ];\n continue;\n case 7:\n op = _.ops.pop();\n _.trys.pop();\n continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {\n _ = 0;\n continue;\n }\n if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {\n _.label = op[1];\n break;\n }\n if (op[0] === 6 && _.label < t[1]) {\n _.label = t[1];\n t = op;\n break;\n }\n if (t && _.label < t[2]) {\n _.label = t[2];\n _.ops.push(op);\n break;\n }\n if (t[2]) _.ops.pop();\n _.trys.pop();\n continue;\n }\n op = body.call(thisArg, _);\n } catch (e) {\n op = [\n 6,\n e\n ];\n y = 0;\n } finally{\n f = t = 0;\n }\n if (op[0] & 5) throw op[1];\n return {\n value: op[0] ? op[1] : void 0,\n done: true\n };\n }\n}\nvar VERSION = \"29038230\";\n// The name of the cache\nvar CACHE_NAME = \"breakout-71-\".concat(VERSION);\n// The static resources that the app needs to function.\nvar APP_STATIC_RESOURCES = [\n \"/\"\n];\n// On install, cache the static resources\nself.addEventListener(\"install\", function(event) {\n event.waitUntil(_async_to_generator(function() {\n var cache;\n return _ts_generator(this, function(_state) {\n switch(_state.label){\n case 0:\n return [\n 4,\n caches.open(CACHE_NAME)\n ];\n case 1:\n cache = _state.sent();\n cache.addAll(APP_STATIC_RESOURCES);\n return [\n 2\n ];\n }\n });\n })());\n});\n// delete old caches on activate\nself.addEventListener(\"activate\", function(event) {\n event.waitUntil(_async_to_generator(function() {\n var names;\n return _ts_generator(this, function(_state) {\n switch(_state.label){\n case 0:\n return [\n 4,\n caches.keys()\n ];\n case 1:\n names = _state.sent();\n return [\n 4,\n Promise.all(names.map(function(name) {\n if (name !== CACHE_NAME) return caches[\"delete\"](name);\n }))\n ];\n case 2:\n _state.sent();\n return [\n 4,\n clients.claim()\n ];\n case 3:\n _state.sent();\n return [\n 2\n ];\n }\n });\n })());\n});\nself.addEventListener(\"fetch\", function(event) {\n if (event.request.mode === \"navigate\" && event.request.url.endsWith(\"/index.html?isPWA=true\")) {\n event.respondWith(caches.match(\"/\"));\n return;\n }\n});\n\n//# sourceMappingURL=sw-b71.js.map\n","// The version of the cache.\nconst VERSION = \"29038230\";\n\n// The name of the cache\nconst CACHE_NAME = `breakout-71-${VERSION}`;\n\n// The static resources that the app needs to function.\nconst APP_STATIC_RESOURCES = [\"/\"];\n\n// On install, cache the static resources\nself.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const cache = await caches.open(CACHE_NAME);\n cache.addAll(APP_STATIC_RESOURCES);\n })(),\n );\n});\n\n// delete old caches on activate\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const names = await caches.keys();\n await Promise.all(\n names.map((name) => {\n if (name !== CACHE_NAME) {\n return caches.delete(name);\n }\n }),\n );\n await clients.claim();\n })(),\n );\n});\n\nself.addEventListener(\"fetch\", (event) => {\n if (\n event.request.mode === \"navigate\" &&\n event.request.url.endsWith(\"/index.html?isPWA=true\")\n ) {\n event.respondWith(caches.match(\"/\"));\n return;\n }\n});\n"],"names":["asyncGeneratorStep","gen","resolve","reject","_next","_throw","key","arg","info","value","error","done","Promise","then","_async_to_generator","fn","self1","args","arguments","apply","err","undefined","_ts_generator","thisArg","body","f","y","t","g","_","label","sent","trys","ops","next","verb","Symbol","iterator","n","v","step","op","TypeError","call","pop","length","push","e","CACHE_NAME","concat","APP_STATIC_RESOURCES","self","addEventListener","event","waitUntil","_state","caches","open","cache","addAll","keys","all","names","map","name","clients","claim","request","mode","url","endsWith","respondWith","match"],"version":3,"file":"sw-b71.js.map"} \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index 0647c31..8df477a 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,3848 +1 @@ - - - - - - - Breakout 71 - - - - - - - - - - - - - +Breakout 71 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 22a9366..c430919 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ }, "devDependencies": { "@parcel/optimizer-data-url": "^2.13.3", + "@parcel/packager-raw-url": "^2.13.3", "@parcel/transformer-inline-string": "^2.13.3", "@parcel/transformer-less": "^2.13.3", "@parcel/transformer-webmanifest": "^2.13.3", @@ -1669,6 +1670,24 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/packager-raw-url": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw-url/-/packager-raw-url-2.13.3.tgz", + "integrity": "sha512-Dc8WeVagLGEUzVP4FqJBljXN59XSkvLoZaHeysvN9P33eznocrhIvc9T/OAQjOmsCj18X8jwxm0dIE7LNJbVCA==", + "dev": true, + "dependencies": { + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.13.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/packager-svg": { "version": "2.13.3", "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.13.3.tgz", diff --git a/package.json b/package.json index f230cc9..5b94114 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "devDependencies": { "@parcel/optimizer-data-url": "^2.13.3", + "@parcel/packager-raw-url": "^2.13.3", "@parcel/transformer-inline-string": "^2.13.3", "@parcel/transformer-less": "^2.13.3", "@parcel/transformer-webmanifest": "^2.13.3", diff --git a/src/PWA/sw-b71.js b/src/PWA/sw-b71.js index bb7c6e5..e4620df 100644 --- a/src/PWA/sw-b71.js +++ b/src/PWA/sw-b71.js @@ -1,5 +1,5 @@ // The version of the cache. -const VERSION = "29036807"; +const VERSION = "29038230"; // The name of the cache const CACHE_NAME = `breakout-71-${VERSION}`; diff --git a/src/data/levels.json b/src/data/levels.json index 8fe26ed..7883b59 100644 --- a/src/data/levels.json +++ b/src/data/levels.json @@ -842,4 +842,4 @@ "svg": null, "color": "" } -] \ No newline at end of file +] diff --git a/src/data/version.json b/src/data/version.json index 692db8e..7acf91a 100644 --- a/src/data/version.json +++ b/src/data/version.json @@ -1 +1 @@ -"29036807" +"29038230" diff --git a/src/game.ts b/src/game.ts index 9125c9b..14ac717 100644 --- a/src/game.ts +++ b/src/game.ts @@ -3,12 +3,15 @@ import { Ball, Coin, GameState, + LightFlash, OptionId, + ParticleFlash, PerkId, RunParams, + TextFlash, Upgrade, } from "./types"; -import {getAudioContext, playPendingSounds} from "./sounds"; +import { getAudioContext, playPendingSounds } from "./sounds"; import { currentLevelInfo, getRowColIndex, @@ -20,6 +23,8 @@ import "./PWA/sw_loader"; import { getCurrentLang, t } from "./i18n/i18n"; import { getSettingValue, getTotalScore, setSettingValue } from "./settings"; import { + empty, + forEachLiveOne, gameStateTick, normalizeGameState, pickRandomUpgrades, @@ -51,11 +56,12 @@ import { closeModal, } from "./asyncAlert"; import { isOptionOn, options, toggleOption } from "./options"; -import {hashCode} from "./getLevelBackground"; +import { hashCode } from "./getLevelBackground"; export function play() { if (gameState.running) return; gameState.running = true; + gameState.ballStickToPuck = false; startRecordingGame(gameState); getAudioContext()?.resume(); @@ -95,6 +101,10 @@ export function pause(playerAskedForPause: boolean) { } export const fitSize = () => { + const past_off = gameState.offsetXRoundedDown, + past_width = gameState.gameZoneWidthRoundedUp, + past_heigh = gameState.gameZoneHeight; + const { width, height } = gameCanvas.getBoundingClientRect(); gameState.canvasWidth = width; gameState.canvasHeight = height; @@ -123,10 +133,27 @@ export const fitSize = () => { backgroundCanvas.title = "resized"; // Ensure puck stays within bounds setMousePos(gameState, gameState.puckPosition); - gameState.coins = []; - gameState.flashes = []; + + function mapXY(item: ParticleFlash | TextFlash | LightFlash) { + item.x = + gameState.offsetXRoundedDown + + ((item.x - past_off) / past_width) * gameState.gameZoneWidthRoundedUp; + item.y = (item.y / past_heigh) * gameState.gameZoneHeight; + } + function mapXYPastCoord(coin: Coin | Ball) { + coin.x = + gameState.offsetXRoundedDown + + ((coin.x - past_off) / past_width) * gameState.gameZoneWidthRoundedUp; + coin.y = (coin.y / past_heigh) * gameState.gameZoneHeight; + coin.previousX = coin.x; + coin.previousY = coin.y; + } + gameState.balls.forEach(mapXYPastCoord); + forEachLiveOne(gameState.coins, mapXYPastCoord); + forEachLiveOne(gameState.particles, mapXY); + forEachLiveOne(gameState.texts, mapXY); + forEachLiveOne(gameState.lights, mapXY); pause(true); - putBallsAtPuck(gameState); // For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ document.documentElement.style.setProperty( "--vh", @@ -273,7 +300,9 @@ gameCanvas.addEventListener("mousemove", (e) => { gameCanvas.addEventListener("touchstart", (e) => { e.preventDefault(); if (!e.touches?.length) return; + setMousePos(gameState, e.touches[0].pageX); + normalizeGameState(gameState); play(); }); gameCanvas.addEventListener("touchend", (e) => { @@ -442,7 +471,6 @@ export function tick() { gameState.puckPosition + gameState.keyboardPuckSpeed, ); } - normalizeGameState(gameState); if (gameState.running) { @@ -457,8 +485,8 @@ export function tick() { if (gameState.running) { recordOneFrame(gameState); } - if(isOptionOn('sound') ){ - playPendingSounds(gameState) + if (isOptionOn("sound")) { + playPendingSounds(gameState); } requestAnimationFrame(tick); } @@ -514,10 +542,13 @@ async function openScorePanel() { } } -document.getElementById("menu")?.addEventListener("click", (e) => { - e.preventDefault(); - openSettingsPanel(); -}); +(document.getElementById("menu") as HTMLButtonElement).addEventListener( + "click", + (e) => { + e.preventDefault(); + openSettingsPanel(); + }, +); async function openSettingsPanel() { pause(true); @@ -665,7 +696,7 @@ async function openSettingsPanel() { localStorageContent[key] = value; } - const signedPayload=JSON.stringify(localStorageContent) + const signedPayload = JSON.stringify(localStorageContent); const dlLink = document.createElement("a"); dlLink.setAttribute( @@ -676,7 +707,10 @@ async function openSettingsPanel() { fileType: "B71-save-file", appVersion, signedPayload, - key: hashCode('Security by obscurity, but really the game is oss so eh'+signedPayload) + key: hashCode( + "Security by obscurity, but really the game is oss so eh" + + signedPayload, + ), }), ), ); @@ -727,7 +761,8 @@ async function openSettingsPanel() { const { fileType, appVersion: fileVersion, - signedPayload,key + signedPayload, + key, } = JSON.parse(content); if (fileType !== "B71-save-file") throw new Error("Not a B71 save file"); @@ -738,11 +773,17 @@ async function openSettingsPanel() { " or newer.", ); - if(key!== hashCode('Security by obscurity, but really the game is oss so eh'+signedPayload)){ - throw new Error("Key does not match content.") + if ( + key !== + hashCode( + "Security by obscurity, but really the game is oss so eh" + + signedPayload, + ) + ) { + throw new Error("Key does not match content."); } - const localStorageContent=JSON.parse(signedPayload) + const localStorageContent = JSON.parse(signedPayload); localStorage.clear(); for (let key in localStorageContent) { localStorage.setItem(key, localStorageContent[key]); @@ -982,4 +1023,4 @@ tick(); // @ts-ignore // window.stressTest= ()=>restart({level:'Shark',perks:{base_combo:100, pierce:10, multiball:8}}) window.stressTest = () => - restart({ level: "Shark", perks: { sapper: 2, pierce: 10, multiball: 3 } }); + restart({ level: "Bird", perks: { sapper: 2, pierce: 10, multiball: 3 } }); diff --git a/src/gameStateMutators.ts b/src/gameStateMutators.ts index 199cbff..ae4124e 100644 --- a/src/gameStateMutators.ts +++ b/src/gameStateMutators.ts @@ -1,1266 +1,1325 @@ -import {Ball, BallLike, Coin, colorString, GameState, PerkId, ReusableArray} from "./types"; +import { + Ball, + BallLike, + Coin, + colorString, + GameState, + LightFlash, + ParticleFlash, + PerkId, + ReusableArray, + TextFlash, +} from "./types"; import { - brickCenterX, - brickCenterY, - currentLevelInfo, - distanceBetween, - getMajorityValue, - getPossibleUpgrades, - getRowColIndex, - isTelekinesisActive, - max_levels, + brickCenterX, + brickCenterY, + currentLevelInfo, + distanceBetween, + getMajorityValue, + getPossibleUpgrades, + getRowColIndex, + isTelekinesisActive, + max_levels, } from "./game_utils"; -import {t} from "./i18n/i18n"; -import {icons} from "./loadGameData"; +import { t } from "./i18n/i18n"; +import { icons } from "./loadGameData"; -import {addToTotalScore} from "./settings"; -import {background} from "./render"; -import {gameOver} from "./gameOver"; +import { addToTotalScore } from "./settings"; +import { background } from "./render"; +import { gameOver } from "./gameOver"; import { - bordersHitCheck, - brickIndex, - coinBrickHitCheck, - fitSize, - gameState, - hasBrick, - hitsSomething, - openUpgradesPicker, - pause, - shouldPierceByColor, + bordersHitCheck, + brickIndex, + coinBrickHitCheck, + fitSize, + gameState, + hasBrick, + hitsSomething, + openUpgradesPicker, + pause, + shouldPierceByColor, } from "./game"; -import {stopRecording} from "./recording"; -import {isOptionOn} from "./options"; +import { stopRecording } from "./recording"; +import { isOptionOn } from "./options"; export function setMousePos(gameState: GameState, x: number) { - // Sets the puck position, and updates the ball position if they are supposed to follow it - gameState.puckPosition = x; - gameState.needsRender = true; + // Sets the puck position, and updates the ball position if they are supposed to follow it + gameState.puckPosition = x; + 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) + ); } export function resetBalls(gameState: GameState) { - const count = 1 + (gameState.perks?.multiball || 0); - const perBall = gameState.puckWidth / (count + 1); - gameState.balls = []; - gameState.ballsColor = "#FFF"; - if (gameState.perks.picky_eater || gameState.perks.pierce_color) { - gameState.ballsColor = - getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF"; - } - for (let i = 0; i < count; i++) { - const x = - gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); - const vx = getBallDefaultVx(gameState); + const count = 1 + (gameState.perks?.multiball || 0); + const perBall = gameState.puckWidth / (count + 1); + gameState.balls = []; + gameState.ballsColor = "#FFF"; + if (gameState.perks.picky_eater || gameState.perks.pierce_color) { + gameState.ballsColor = + getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF"; + } + 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, + 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, - sx: 0, - sy: 0, - sparks: 0, - piercedSinceBounce: 0, - hitSinceBounce: 0, - hitItem: [], - sapperUses: 0, - }); - } + sx: 0, + sy: 0, + sparks: 0, + piercedSinceBounce: 0, + hitSinceBounce: 0, + hitItem: [], + 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.vx = vx; - ball.previousVX = ball.vx; - ball.vy = -gameState.baseSpeed; - ball.previousVY = ball.vy; - ball.sx = 0; - ball.sy = 0; - ball.hitItem = []; - ball.hitSinceBounce = 0; - ball.piercedSinceBounce = 0; - }); + ball.x = x; + ball.previousX = x; + ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize; + ball.previousY = ball.y; + ball.vx = vx; + ball.previousVX = ball.vx; + ball.vy = -gameState.baseSpeed; + ball.previousVY = ball.vy; + ball.sx = 0; + ball.sy = 0; + ball.hitItem = []; + ball.hitSinceBounce = 0; + ball.piercedSinceBounce = 0; + }); } 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 = - (gameState.gameZoneWidth / 12) * - (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck); + gameState.puckWidth = + (gameState.gameZoneWidth / 12) * + (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck); - if ( - gameState.puckPosition < - gameState.offsetXRoundedDown + gameState.puckWidth / 2 - ) { - gameState.puckPosition = - gameState.offsetXRoundedDown + gameState.puckWidth / 2; - } - if ( - gameState.puckPosition > - gameState.offsetXRoundedDown + - gameState.gameZoneWidthRoundedUp - - gameState.puckWidth / 2 - ) { - gameState.puckPosition = - gameState.offsetXRoundedDown + - gameState.gameZoneWidthRoundedUp - - gameState.puckWidth / 2; - } - - if (!gameState.running && !gameState.levelTime) { - putBallsAtPuck(gameState); - } + if ( + gameState.puckPosition < + gameState.offsetXRoundedDown + gameState.puckWidth / 2 + ) { + gameState.puckPosition = + gameState.offsetXRoundedDown + gameState.puckWidth / 2; + } + if ( + gameState.puckPosition > + gameState.offsetXRoundedDown + + gameState.gameZoneWidthRoundedUp - + gameState.puckWidth / 2 + ) { + gameState.puckPosition = + gameState.offsetXRoundedDown + + gameState.gameZoneWidthRoundedUp - + gameState.puckWidth / 2; + } + if (gameState.ballStickToPuck) { + putBallsAtPuck(gameState); + } } export function baseCombo(gameState: GameState) { - return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5; + return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5; } 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); - if (!gameState.levelTime) { - gameState.combo += gameState.perks.hot_start * 15; + const prev = gameState.combo; + gameState.combo = baseCombo(gameState); + if (!gameState.levelTime) { + gameState.combo += gameState.perks.hot_start * 15; + } + if (prev > gameState.combo && gameState.perks.soft_reset) { + gameState.combo += Math.floor( + ((prev - gameState.combo) * (gameState.perks.soft_reset * 10)) / 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 (prev > gameState.combo && gameState.perks.soft_reset) { - gameState.combo += Math.floor( - ((prev - gameState.combo) * (gameState.perks.soft_reset * 10)) / 100, - ); + if (typeof x !== "undefined" && typeof y !== "undefined") { + makeText(gameState, x, y, "red", "-" + lost, 20, 150); } - 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") { - gameState.flashes.push({ - type: "text", - text: "-" + lost, - time: gameState.levelTime, - color: "red", - x: x, - y: y, - duration: 150, - size: gameState.puckHeight, - }); - } - } - return lost; + } + return lost; } export function decreaseCombo( - gameState: GameState, - by: number, - x: number, - y: number, + gameState: GameState, + by: number, + x: number, + y: number, ) { - const prev = gameState.combo; - gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by); - const lost = Math.max(0, prev - gameState.combo); + 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) - if (typeof x !== "undefined" && typeof y !== "undefined") { - gameState.flashes.push({ - type: "text", - text: "-" + lost, - time: gameState.levelTime, - color: "red", - x: x, - y: y, - duration: 300, - size: gameState.puckHeight, - }); - } + if (lost) { + schedulGameSound(gameState, "comboDecrease", x, 1); + if (typeof x !== "undefined" && typeof y !== "undefined") { + makeText(gameState, x, y, "red", "-" + lost, 20, 300); } + } } export function spawnExplosion( - gameState: GameState, - count: number, - x: number, - y: number, - color: string, - duration = 150, - size = gameState.coinSize, + gameState: GameState, + count: number, + x: number, + y: number, + color: string, ) { - if (!!isOptionOn("basic")) return; - if (gameState.flashes.length > gameState.MAX_PARTICLES) { - // Avoid freezing when lots of explosion happen at once - count = 1; - } - for (let i = 0; i < count; i++) { - gameState.flashes.push({ - type: "particle", - time: gameState.levelTime, - size, - x: x + ((Math.random() - 0.5) * gameState.brickWidth) / 2, - y: y + ((Math.random() - 0.5) * gameState.brickWidth) / 2, - vx: (Math.random() - 0.5) * 30, - vy: (Math.random() - 0.5) * 30, - color, - duration, - ethereal: false, - }); - } + if (!!isOptionOn("basic")) return; + + if (liveCount(gameState.particles) > gameState.MAX_PARTICLES) { + // 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, + ); + } } 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; - if (color === "black") { - delete gameState.bricks[index]; - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); -schedulGameSound( - gameState, - 'explode', ball.x, 1) + if (color === "black") { + delete gameState.bricks[index]; + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); + schedulGameSound(gameState, "explode", ball.x, 1); - const col = index % gameState.gridSize; - const row = Math.floor(index / gameState.gridSize); - const size = 1 + gameState.perks.bigger_explosions; - // 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 - if ( - gameState.bricks[i] !== "black" && - gameState.perks.sturdy_bricks > Math.random() * 5 - ) - continue; - explodeBrick(gameState, i, ball, true); - } - } + const col = index % gameState.gridSize; + const row = Math.floor(index / gameState.gridSize); + const size = 1 + gameState.perks.bigger_explosions; + // 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 + if ( + gameState.bricks[i] !== "black" && + gameState.perks.sturdy_bricks > Math.random() * 5 + ) + continue; + explodeBrick(gameState, i, ball, true); } - - // Blow nearby coins - gameState.coins.forEach((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; - c.vy += ((dy / d2) * 10 * size) / c.weight; - }); - gameState.lastExplosion = Date.now(); - - gameState.flashes.push({ - type: "ball", - duration: 150, - time: gameState.levelTime, - size: gameState.brickWidth * 2, - color: "white", - x, - y, - }); - spawnExplosion( - gameState, - 7 * (1 + gameState.perks.bigger_explosions), - x, - y, - "white", - 150, - gameState.coinSize, - ); - ball.hitSinceBounce++; - gameState.runStatistics.bricks_broken++; - } else if (color) { - // Even if it bounces we don't want to count that as a miss - ball.hitSinceBounce++; - - // Flashing is take care of by the tick loop - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); - - gameState.bricks[index] = ""; - - // coins = coins.filter((c) => !c.destroyed); - let coinsToSpawn = gameState.combo; - if (gameState.perks.sturdy_bricks) { - // +10% per level - coinsToSpawn += Math.ceil( - ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn, - ); - } - - gameState.levelSpawnedCoins += coinsToSpawn; - gameState.runStatistics.coins_spawned += coinsToSpawn; - gameState.runStatistics.bricks_broken++; - const maxCoins = gameState.MAX_COINS * (isOptionOn("basic") ? 0.5 : 1); - const spawnableCoins = - gameState.coins.length > gameState.MAX_COINS - ? 1 - : Math.floor(maxCoins - gameState.coins.length) / 3; - - 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; - } - - coinsToSpawn -= points; - - const cx = - x + - (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize), - cy = - y + - (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize); - - gameState.coins.push({ - points, - size: gameState.coinSize, //-Math.floor(Math.log2(points)), - color: gameState.perks.metamorphosis ? color : "gold", - x: cx, - y: cy, - previousX: cx, - previousY: cy, - // Use previous speed because the ball has already bounced - vx: ball.previousVX * (0.5 + Math.random()), - vy: ball.previousVY * (0.5 + Math.random()), - sx: 0, - sy: 0, - a: Math.random() * Math.PI * 2, - sa: Math.random() - 0.5, - weight: 0.8 + Math.random() * 0.2, - }); - } - - gameState.combo += Math.max( - 0, - 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, - ); - - if (!isExplosion) { - // color change - if ( - (gameState.perks.picky_eater || gameState.perks.pierce_color) && - color !== gameState.ballsColor && - color - ) { - if (gameState.perks.picky_eater) { - resetCombo(gameState, ball.x, ball.y); - } - schedulGameSound( - gameState, - 'colorChange', ball.x, 0.8) - gameState.lastExplosion = gameState.levelTime; - gameState.ballsColor = color; - if (!isOptionOn("basic")) { - gameState.balls.forEach((ball) => { - spawnExplosion( - gameState, - 7, - ball.previousX, - ball.previousY, - color, - 150, - 15, - ); - }); - } - } else { - schedulGameSound( - gameState, - 'comboIncreaseMaybe', ball.x, 1) - } - } - - gameState.flashes.push({ - type: "ball", - duration: 40, - time: gameState.levelTime, - size: gameState.brickWidth, - color: color, - x, - y, - }); - spawnExplosion( - gameState, - 5 + Math.min(gameState.combo, 30), - x, - y, - color, - 150, - gameState.coinSize / 2, - ); + } } - if (!gameState.bricks[index] && color !== "black") { - ball.hitItem?.push({ - index, - color, - }); + // 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; + c.vy += ((dy / d2) * 10 * size) / c.weight; + }); + gameState.lastExplosion = Date.now(); + + makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150); + + spawnExplosion( + gameState, + 7 * (1 + gameState.perks.bigger_explosions), + x, + y, + "white", + ); + ball.hitSinceBounce++; + gameState.runStatistics.bricks_broken++; + } else if (color) { + // Even if it bounces we don't want to count that as a miss + ball.hitSinceBounce++; + + // Flashing is take care of by the tick loop + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); + + gameState.bricks[index] = ""; + + let coinsToSpawn = gameState.combo; + if (gameState.perks.sturdy_bricks) { + // +10% per level + coinsToSpawn += Math.ceil( + ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn, + ); } + + gameState.levelSpawnedCoins += coinsToSpawn; + gameState.runStatistics.coins_spawned += coinsToSpawn; + gameState.runStatistics.bricks_broken++; + const maxCoins = gameState.MAX_COINS * (isOptionOn("basic") ? 0.5 : 1); + const spawnableCoins = + liveCount(gameState.coins) > gameState.MAX_COINS + ? 1 + : Math.floor(maxCoins - liveCount(gameState.coins)) / 3; + + 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; + } + + coinsToSpawn -= points; + + 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()), + gameState.perks.metamorphosis ? color : "gold", + points, + ); + } + + gameState.combo += Math.max( + 0, + 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, + ); + + if (!isExplosion) { + // color change + if ( + (gameState.perks.picky_eater || gameState.perks.pierce_color) && + color !== gameState.ballsColor && + color + ) { + if (gameState.perks.picky_eater) { + resetCombo(gameState, ball.x, ball.y); + } + schedulGameSound(gameState, "colorChange", ball.x, 0.8); + gameState.lastExplosion = gameState.levelTime; + gameState.ballsColor = color; + if (!isOptionOn("basic")) { + gameState.balls.forEach((ball) => { + spawnExplosion(gameState, 7, ball.previousX, ball.previousY, color); + }); + } + } else { + schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1); + } + } + makeLight(gameState, x, y, color, gameState.brickWidth, 40); + + spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color); + } + + if (!gameState.bricks[index] && color !== "black") { + ball.hitItem?.push({ + index, + color, + }); + } } export function dontOfferTooSoon(gameState: GameState, id: PerkId) { - gameState.lastOffered[id] = Math.round(Date.now() / 1000); + gameState.lastOffered[id] = Math.round(Date.now() / 1000); } export function pickRandomUpgrades(gameState: GameState, count: number) { - let list = getPossibleUpgrades(gameState) - .map((u) => ({ - ...u, - score: Math.random() + (gameState.lastOffered[u.id] || 0), - })) - .sort((a, b) => a.score - b.score) - .filter((u) => gameState.perks[u.id] < u.max) - .slice(0, count) - .sort((a, b) => (a.id > b.id ? 1 : -1)); + let list = getPossibleUpgrades(gameState) + .map((u) => ({ + ...u, + score: Math.random() + (gameState.lastOffered[u.id] || 0), + })) + .sort((a, b) => a.score - b.score) + .filter((u) => gameState.perks[u.id] < u.max) + .slice(0, count) + .sort((a, b) => (a.id > b.id ? 1 : -1)); - list.forEach((u) => { - dontOfferTooSoon(gameState, u.id); - }); + list.forEach((u) => { + dontOfferTooSoon(gameState, u.id); + }); - return list.map((u) => ({ - text: - u.name + - (gameState.perks[u.id] - ? t("level_up.upgrade_perk_to_level", { - level: gameState.perks[u.id] + 1, - }) - : ""), - icon: icons["icon:" + u.id], - value: u.id as PerkId, - help: u.help(gameState.perks[u.id] + 1), - })); + return list.map((u) => ({ + text: + u.name + + (gameState.perks[u.id] + ? t("level_up.upgrade_perk_to_level", { + level: gameState.perks[u.id] + 1, + }) + : ""), + icon: icons["icon:" + u.id], + value: u.id as PerkId, + help: u.help(gameState.perks[u.id] + 1), + })); } -export function schedulGameSound(gameState: GameState, - sound: keyof GameState["aboutToPlaySound"], - x: number|void, - vol: number) { - if (!vol) return - x??=gameState.offsetX+gameState.gameZoneWidth/2 - const ex = gameState.aboutToPlaySound[sound] as { vol: number, x: number } - if(ex.vol){ - console.log('Combined sounds for '+sound) - } - ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol) - ex.vol += vol +export function schedulGameSound( + gameState: GameState, + sound: keyof GameState["aboutToPlaySound"], + x: number | void, + vol: number, +) { + if (!vol) return; + 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; } export function addToScore(gameState: GameState, coin: Coin) { - coin.destroyed = true; - gameState.score += coin.points; - gameState.lastScoreIncrease = gameState.levelTime; + gameState.score += coin.points; + gameState.lastScoreIncrease = gameState.levelTime; - addToTotalScore(gameState, coin.points); - if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) { - gameState.highScore = gameState.score; - localStorage.setItem("breakout-3-hs", gameState.score.toString()); - } - if (!isOptionOn("basic")) { - gameState.flashes.push({ - type: "particle", - duration: 100 + Math.random() * 50, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: coin.color, - x: coin.previousX, - y: coin.previousY, - vx: (gameState.canvasWidth - coin.x) / 100, - vy: -coin.y / 100, - ethereal: true, - }); - } + addToTotalScore(gameState, coin.points); + if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) { + gameState.highScore = gameState.score; + localStorage.setItem("breakout-3-hs", gameState.score.toString()); + } + if (!isOptionOn("basic")) { + makeParticle( + gameState, + coin.previousX, + coin.previousY, + (gameState.canvasWidth - coin.x) / 100, + -coin.y / 100, + coin.color, + true, + gameState.coinSize / 2, + 100 + Math.random() * 50, + ); + } - if (Date.now() - gameState.lastPlayedCoinGrab > 16) { - gameState.lastPlayedCoinGrab = Date.now(); + if (Date.now() - gameState.lastPlayedCoinGrab > 16) { + gameState.lastPlayedCoinGrab = Date.now(); - schedulGameSound( - gameState, - 'coinCatch', coin.x, 1) - } - gameState.runStatistics.score += coin.points; + schedulGameSound(gameState, "coinCatch", coin.x, 1); + } + gameState.runStatistics.score += coin.points; } export function setLevel(gameState: GameState, l: number) { - stopRecording(); - pause(false); - if (l > 0) { - openUpgradesPicker(gameState); - } - gameState.currentLevel = l; + stopRecording(); + pause(false); + if (l > 0) { + openUpgradesPicker(gameState); + } + gameState.currentLevel = l; - gameState.levelTime = 0; - gameState.levelWallBounces = 0; - gameState.autoCleanUses = 0; - gameState.lastTickDown = gameState.levelTime; - gameState.levelStartScore = gameState.score; - gameState.levelSpawnedCoins = 0; - gameState.levelMisses = 0; - gameState.runStatistics.levelsPlayed++; + gameState.levelTime = 0; + gameState.levelWallBounces = 0; + gameState.autoCleanUses = 0; + gameState.lastTickDown = gameState.levelTime; + gameState.levelStartScore = gameState.score; + gameState.levelSpawnedCoins = 0; + gameState.levelMisses = 0; + gameState.runStatistics.levelsPlayed++; - resetCombo(gameState, undefined, undefined); - resetBalls(gameState); + resetCombo(gameState, undefined, undefined); + resetBalls(gameState); - const lvl = currentLevelInfo(gameState); - if (lvl.size !== gameState.gridSize) { - gameState.gridSize = lvl.size; - fitSize(); - } - gameState.coins = []; - gameState.bricks = [...lvl.bricks]; - gameState.flashes = []; + const lvl = currentLevelInfo(gameState); + if (lvl.size !== gameState.gridSize) { + gameState.gridSize = lvl.size; + fitSize(); + } + empty(gameState.coins); + empty(gameState.particles); + empty(gameState.lights); + empty(gameState.texts); + gameState.bricks = [...lvl.bricks]; - // 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; + // 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; } export function rainbowColor(): colorString { - return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`; + return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`; } 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 / 2; - 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 / 2; + 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; - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: a.x, - y: a.y, - vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand, - vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand, - }); - if ( - impactsBToo && - typeof b.vx !== "undefined" && - typeof b.vy !== "undefined" - ) { - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: b.x, - y: b.y, - vx: dx * speed + b.vx + (Math.random() - 0.5) * rand, - vy: dy * speed + b.vy + (Math.random() - 0.5) * rand, - }); - } + 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, + 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 * 0.5; - 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 * 0.5; + 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; - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: a.x, - y: a.y, - vx: dx * speed + a.vx + (Math.random() - 0.5) * rand, - vy: dy * speed + a.vy + (Math.random() - 0.5) * rand, - }); - gameState.flashes.push({ - type: "particle", - duration: 100, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - ethereal: true, - x: b.x, - y: b.y, - vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand, - vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand, - }); + 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, + ); } 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, ) { - 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.balls = gameState.balls.filter((ball) => !ball.destroyed); + + const remainingBricks = gameState.bricks.filter( + (b) => b && b !== "black", + ).length; + + if ( + gameState.levelTime > gameState.lastTickDown + 1000 && + gameState.perks.hot_start + ) { + gameState.lastTickDown = gameState.levelTime; + decreaseCombo( + gameState, + gameState.perks.hot_start, + gameState.puckPosition, + gameState.gameZoneHeight - 2 * gameState.puckHeight, ); + } - gameState.coins = gameState.coins.filter((coin) => !coin.destroyed); - gameState.balls = gameState.balls.filter((ball) => !ball.destroyed); - - const remainingBricks = gameState.bricks.filter( - (b) => b && b !== "black", - ).length; - - if ( - gameState.levelTime > gameState.lastTickDown + 1000 && - gameState.perks.hot_start - ) { - gameState.lastTickDown = gameState.levelTime; - decreaseCombo( - gameState, - gameState.perks.hot_start, - gameState.puckPosition, - gameState.gameZoneHeight - 2 * gameState.puckHeight, - ); + if ( + remainingBricks <= gameState.perks.skip_last && + !gameState.autoCleanUses + ) { + gameState.bricks.forEach((type, index) => { + if (type) { + explodeBrick(gameState, index, gameState.balls[0], true); + } + }); + gameState.autoCleanUses++; + } + if (!remainingBricks && !liveCount(gameState.coins)) { + 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 if (gameState.running || gameState.levelTime) { + const coinRadius = Math.round(gameState.coinSize / 2); - if ( - remainingBricks <= gameState.perks.skip_last && - !gameState.autoCleanUses - ) { - gameState.bricks.forEach((type, index) => { - if (type) { - explodeBrick(gameState, index, gameState.balls[0], true); - } - }); - gameState.autoCleanUses++; - } - if (!remainingBricks && !gameState.coins.length) { - 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 if (gameState.running || gameState.levelTime) { - const coinRadius = Math.round(gameState.coinSize / 2); + forEachLiveOne(gameState.coins, (coin, coinIndex) => { + if (gameState.perks.coin_magnet) { + const attractionX = + ((frames * (gameState.puckPosition - coin.x)) / + (100 + + Math.pow(coin.y - gameState.gameZoneHeight, 2) + + Math.pow(coin.x - gameState.puckPosition, 2))) * + gameState.perks.coin_magnet * + 100; + coin.vx += attractionX; + coin.sa -= attractionX / 10; + } - gameState.coins.forEach((coin) => { - if (coin.destroyed) return; - if (gameState.perks.coin_magnet) { - const attractionX = - ((frames * (gameState.puckPosition - coin.x)) / - (100 + - Math.pow(coin.y - gameState.gameZoneHeight, 2) + - Math.pow(coin.x - gameState.puckPosition, 2))) * - gameState.perks.coin_magnet * - 100; - coin.vx += attractionX; - coin.sa -= attractionX / 10; - } + const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames; - const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames; + coin.vy *= ratio; + coin.vx *= ratio; + 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; - coin.vy *= ratio; - coin.vx *= ratio; - 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 + coin.vy += frames * coin.weight * 0.8; - // Gravity - coin.vy += frames * coin.weight * 0.8; - - const speed = Math.abs(coin.sx) + Math.abs(coin.sx); - const hitBorder = bordersHitCheck(coin, coin.size / 2, frames); - - 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 - gameState.puckHeight - ) { - addToScore(gameState, coin); - } else if (coin.y > gameState.canvasHeight + coinRadius) { - coin.destroyed = true; - if (gameState.perks.compound_interest) { - resetCombo(gameState, coin.x, coin.y); - } - } - - const hitBrick = coinBrickHitCheck(coin); - - if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { - if ( - gameState.bricks[hitBrick] && - coin.color !== gameState.bricks[hitBrick] && - gameState.bricks[hitBrick] !== "black" && - !coin.coloredABrick - ) { - gameState.bricks[hitBrick] = coin.color; - coin.coloredABrick = true; - - schedulGameSound( - gameState, - 'colorChange', coin.x, 0.3) - } - } - if (typeof hitBrick !== "undefined" || hitBorder) { - coin.vx *= 0.8; - coin.vy *= 0.8; - coin.sa *= 0.9; - if (speed > 20 ) { - schedulGameSound( - gameState, - 'coinBounce', coin.x, 0.2) - } - - if (Math.abs(coin.vy) < 3) { - coin.vy = 0; - } - } - }); - - gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); - - if (gameState.perks.wind) { - const windD = - ((gameState.puckPosition - - (gameState.offsetX + gameState.gameZoneWidth / 2)) / - gameState.gameZoneWidth) * - 2 * - gameState.perks.wind; - for (let i = 0; i < gameState.perks.wind; i++) { - if (Math.random() * Math.abs(windD) > 0.5) { - gameState.flashes.push({ - type: "particle", - duration: 150, - ethereal: true, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: rainbowColor(), - x: - gameState.offsetXRoundedDown + - Math.random() * gameState.gameZoneWidthRoundedUp, - y: Math.random() * gameState.gameZoneHeight, - vx: windD * 8, - vy: 0, - }); - } - } - } - - gameState.flashes.forEach((flash) => { - if (flash.type === "particle") { - flash.x += flash.vx * frames; - flash.y += flash.vy * frames; - if (!flash.ethereal) { - flash.vy += 0.5; - if (hasBrick(brickIndex(flash.x, flash.y))) { - flash.destroyed = true; - } - } - } - }); - } - - if (gameState.combo > baseCombo(gameState)) { - // The red should still be visible on a white bg - const baseParticle = !isOptionOn("basic") && - (gameState.combo - baseCombo(gameState)) * Math.random() > 5 && - gameState.running && { - type: "particle" as const, - duration: 100 * (Math.random() + 1), - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: "red", - ethereal: true, - }; - - if (gameState.perks.top_is_lava) { - baseParticle && - gameState.flashes.push({ - ...baseParticle, - x: - gameState.offsetXRoundedDown + - Math.random() * gameState.gameZoneWidthRoundedUp, - y: 0, - vx: (Math.random() - 0.5) * 10, - vy: 5, - }); - } - - if (gameState.perks.left_is_lava && baseParticle) { - gameState.flashes.push({ - ...baseParticle, - x: gameState.offsetXRoundedDown, - y: Math.random() * gameState.gameZoneHeight, - vx: 5, - vy: (Math.random() - 0.5) * 10, - }); - } - - if (gameState.perks.right_is_lava && baseParticle) { - gameState.flashes.push({ - ...baseParticle, - x: gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp, - y: Math.random() * gameState.gameZoneHeight, - vx: -5, - vy: (Math.random() - 0.5) * 10, - }); - } + const speed = Math.abs(coin.sx) + Math.abs(coin.sx); + const hitBorder = bordersHitCheck(coin, coin.size / 2, frames); + 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 + gameState.puckHeight + ) { + addToScore(gameState, coin); + destroy(gameState.coins, coinIndex); + } else if (coin.y > gameState.canvasHeight + coinRadius) { + destroy(gameState.coins, coinIndex); 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 - ); - baseParticle && - gameState.flashes.push({ - ...baseParticle, - x, - y: gameState.gameZoneHeight, - vx: (Math.random() - 0.5) * 10, - vy: -5, - }); + resetCombo(gameState, coin.x, coin.y); } - if (gameState.perks.streak_shots) { - const pos = 0.5 - Math.random(); - baseParticle && - gameState.flashes.push({ - ...baseParticle, - duration: 100, - x: gameState.puckPosition + gameState.puckWidth * pos, - y: gameState.gameZoneHeight - gameState.puckHeight, - vx: pos * 10, - vy: -5, - }); + } + + const hitBrick = coinBrickHitCheck(coin); + + if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { + if ( + gameState.bricks[hitBrick] && + coin.color !== gameState.bricks[hitBrick] && + gameState.bricks[hitBrick] !== "black" && + !coin.coloredABrick + ) { + gameState.bricks[hitBrick] = coin.color; + coin.coloredABrick = true; + + schedulGameSound(gameState, "colorChange", coin.x, 0.3); } + } + if (typeof hitBrick !== "undefined" || hitBorder) { + coin.vx *= 0.8; + coin.vy *= 0.8; + coin.sa *= 0.9; + if (speed > 20) { + schedulGameSound(gameState, "coinBounce", coin.x, 0.2); + } + + if (Math.abs(coin.vy) < 3) { + coin.vy = 0; + } + } + }); + + gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); + + if (gameState.perks.wind) { + const windD = + ((gameState.puckPosition - + (gameState.offsetX + gameState.gameZoneWidth / 2)) / + 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; + if (hasBrick(brickIndex(flash.x, flash.y))) { + destroy(gameState.particles, index); + } + } + }); + } + + if ( + gameState.combo > baseCombo(gameState) && + !isOptionOn("basic") && + (gameState.combo - baseCombo(gameState)) * Math.random() > 5 + ) { + // The red should still be visible on a white bg + + if (gameState.perks.top_is_lava) { + makeParticle( + gameState, + gameState.offsetXRoundedDown + + Math.random() * gameState.gameZoneWidthRoundedUp, + 0, + (Math.random() - 0.5) * 10, + 5, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + + if (gameState.perks.left_is_lava) { + makeParticle( + gameState, + gameState.offsetXRoundedDown, + Math.random() * gameState.gameZoneHeight, + 5, + (Math.random() - 0.5) * 10, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + + if (gameState.perks.right_is_lava) { + makeParticle( + gameState, + gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp, + Math.random() * gameState.gameZoneHeight, + -5, + (Math.random() - 0.5) * 10, + "red", + 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, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + if (gameState.perks.streak_shots) { + const pos = 0.5 - Math.random(); + makeParticle( + gameState, + gameState.puckPosition + gameState.puckWidth * pos, + gameState.gameZoneHeight - gameState.puckHeight, + pos * 10, + -5, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + } + + 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, delta: number) { - ball.previousVX = ball.vx; - ball.previousVY = ball.vy; + 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; - if (isTelekinesisActive(gameState, ball)) { - speedLimitDampener += 3; - ball.vx += - ((gameState.puckPosition - ball.x) / 1000) * - delta * - gameState.perks.telekinesis; - } + let speedLimitDampener = + 1 + + gameState.perks.telekinesis + + gameState.perks.ball_repulse_ball + + gameState.perks.puck_repulse_ball + + gameState.perks.ball_attract_ball; + if (isTelekinesisActive(gameState, ball)) { + speedLimitDampener += 3; + ball.vx += + ((gameState.puckPosition - ball.x) / 1000) * + delta * + gameState.perks.telekinesis; + } - 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 ( + 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_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.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 && - Math.abs(ball.x - gameState.puckPosition) < - gameState.puckWidth / 2 + + } + if ( + gameState.perks.puck_repulse_ball && + 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.respawn && + ball.hitItem?.length > 1 && + !isOptionOn("basic") + ) { + for ( + let i = 0; + i < ball.hitItem?.length - 1 && i < gameState.perks.respawn; + i++ ) { - repulse( + const { index, color } = ball.hitItem[i]; + if (gameState.bricks[index] || color === "black") continue; + 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, + ); + } + } + + const borderHitCode = bordersHitCheck(ball, gameState.ballSize / 2, delta); + if (borderHitCode) { + if ( + gameState.perks.left_is_lava && + borderHitCode % 2 && + ball.x < gameState.offsetX + gameState.gameZoneWidth / 2 + ) { + resetCombo(gameState, ball.x, ball.y); + } + + if ( + gameState.perks.right_is_lava && + borderHitCode % 2 && + ball.x > 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 + gameState.ballSize); + } + + 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; + if ( + ball.y > ylimit && + ball.vy > 0 && + (ballIsUnderPuck || + (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 ? -0.5 : 1), + ); + ball.vx = speed * Math.cos(angle); + ball.vy = speed * Math.sin(angle); + schedulGameSound(gameState, "wallBeep", ball.x, 1); + } else { + ball.vy *= -1; + gameState.perks.extra_life = Math.max(0, gameState.perks.extra_life - 1); + + schedulGameSound(gameState, "lifeLost", ball.x, 1); + if (!isOptionOn("basic")) { + for (let i = 0; i < 10; i++) + makeParticle( gameState, - ball, - { - x: gameState.puckPosition, - y: gameState.gameZoneHeight, - }, - gameState.perks.puck_repulse_ball + 1, + ball.x, + ball.y, + Math.random() * gameState.baseSpeed * 3, + gameState.baseSpeed * 3, + "red", false, - ); + gameState.coinSize / 2, + 150, + ); + } } + if (gameState.perks.streak_shots) { + resetCombo(gameState, ball.x, ball.y); + } + + if (gameState.perks.respawn) { + ball.hitItem + .slice(0, -1) + .slice(0, gameState.perks.respawn) + .forEach(({ index, color }) => { + if (!gameState.bricks[index] && color !== "black") + gameState.bricks[index] = color; + }); + } + ball.hitItem = []; + if (!ball.hitSinceBounce) { + gameState.runStatistics.misses++; + gameState.levelMisses++; + resetCombo(gameState, ball.x, ball.y); + makeText( + gameState, + gameState.puckPosition, + gameState.gameZoneHeight - gameState.puckHeight * 2, + "red", + t("play.missed_ball"), + gameState.puckHeight, + 500, + ); + } + gameState.runStatistics.puck_bounces++; + ball.hitSinceBounce = 0; + ball.sapperUses = 0; + ball.piercedSinceBounce = 0; + } + + if ( + ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 && + gameState.running + ) { + ball.destroyed = true; + gameState.runStatistics.balls_lost++; + if (!gameState.balls.find((b) => !b.destroyed)) { + 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; + let sturdyBounce = + hitBrick && + gameState.bricks[hitBrick] !== "black" && + gameState.perks.sturdy_bricks && + gameState.perks.sturdy_bricks > Math.random() * 5; + + let pierce = false; + if (sturdyBounce || typeof hitBrick === "undefined") { + // cannot pierce + } else if (shouldPierceByColor(vhit, hhit, chit)) { + pierce = true; + } else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) { + pierce = true; + ball.piercedSinceBounce++; + } + + 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 (sturdyBounce) { + schedulGameSound(gameState, "wallBeep", x, 1); + return; + } + if (typeof hitBrick !== "undefined") { + const initialBrickColor = gameState.bricks[hitBrick]; + + explodeBrick(gameState, hitBrick, ball, false); if ( - gameState.perks.respawn && - ball.hitItem?.length > 1 && - !isOptionOn("basic") + ball.sapperUses < gameState.perks.sapper && + initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks + !gameState.bricks[hitBrick] ) { - for ( - let i = 0; - i < ball.hitItem?.length - 1 && i < gameState.perks.respawn; - i++ - ) { - const {index, color} = ball.hitItem[i]; - if (gameState.bricks[index] || color === "black") continue; - const vertical = Math.random() > 0.5; - const dx = Math.random() > 0.5 ? 1 : -1; - const dy = Math.random() > 0.5 ? 1 : -1; - - gameState.flashes.push({ - type: "particle", - duration: 250, - ethereal: true, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color, - x: brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2, - y: brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2, - vx: vertical ? 0 : -dx * gameState.baseSpeed, - vy: vertical ? -dy * gameState.baseSpeed : 0, - }); - } + gameState.bricks[hitBrick] = "black"; + ball.sapperUses++; } + } - const borderHitCode = bordersHitCheck(ball, gameState.ballSize / 2, delta); - if (borderHitCode) { - if ( - gameState.perks.left_is_lava && - borderHitCode % 2 && - ball.x < gameState.offsetX + gameState.gameZoneWidth / 2 - ) { - resetCombo(gameState, ball.x, ball.y); - } + if (!isOptionOn("basic")) { + ball.sparks += (delta * (gameState.combo - 1)) / 30; + if (ball.sparks > 1) { + makeParticle( + gameState, + ball.x, + ball.y, + (Math.random() - 0.5) * gameState.baseSpeed, + (Math.random() - 0.5) * gameState.baseSpeed, + gameState.ballsColor, + false, + gameState.coinSize / 2, + 100 * ball.sparks, + ); - if ( - gameState.perks.right_is_lava && - borderHitCode % 2 && - ball.x > 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 + gameState.ballSize); - } - - 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; - if ( - ball.y > ylimit && - ball.vy > 0 && - (ballIsUnderPuck || - (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 ? -0.5 : 1), - ); - ball.vx = speed * Math.cos(angle); - ball.vy = speed * Math.sin(angle); - schedulGameSound( - gameState, - 'wallBeep', ball.x, 1) - } else { - ball.vy *= -1; - gameState.perks.extra_life = Math.max(0, gameState.perks.extra_life - 1); - - schedulGameSound( - gameState, - 'lifeLost', ball.x, 1) - if (!isOptionOn("basic")) { - for (let i = 0; i < 10; i++) - gameState.flashes.push({ - type: "particle", - ethereal: false, - color: "red", - destroyed: false, - duration: 150, - size: gameState.coinSize / 2, - time: gameState.levelTime, - x: ball.x, - y: ball.y, - vx: Math.random() * gameState.baseSpeed * 3, - vy: gameState.baseSpeed * 3, - }); - } - } - if (gameState.perks.streak_shots) { - resetCombo(gameState, ball.x, ball.y); - } - - if (gameState.perks.respawn) { - ball.hitItem - .slice(0, -1) - .slice(0, gameState.perks.respawn) - .forEach(({index, color}) => { - if (!gameState.bricks[index] && color !== "black") - gameState.bricks[index] = color; - }); - } - ball.hitItem = []; - if (!ball.hitSinceBounce) { - gameState.runStatistics.misses++; - gameState.levelMisses++; - resetCombo(gameState, ball.x, ball.y); - gameState.flashes.push({ - type: "text", - text: t("play.missed_ball"), - duration: 500, - time: gameState.levelTime, - size: gameState.puckHeight * 1.5, - color: "red", - x: gameState.puckPosition, - y: gameState.gameZoneHeight - gameState.puckHeight * 2, - }); - } - gameState.runStatistics.puck_bounces++; - ball.hitSinceBounce = 0; - ball.sapperUses = 0; - ball.piercedSinceBounce = 0; - } - - if ( - ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 && - gameState.running - ) { - ball.destroyed = true; - gameState.runStatistics.balls_lost++; - if (!gameState.balls.find((b) => !b.destroyed)) { - 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; - let sturdyBounce = - hitBrick && - gameState.bricks[hitBrick] !== "black" && - gameState.perks.sturdy_bricks && - gameState.perks.sturdy_bricks > Math.random() * 5; - - let pierce = false; - if (sturdyBounce || typeof hitBrick === "undefined") { - // cannot pierce - } else if (shouldPierceByColor(vhit, hhit, chit)) { - pierce = true; - } else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) { - pierce = true; - ball.piercedSinceBounce++; - } - - 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 (sturdyBounce) { - schedulGameSound( - gameState, - 'wallBeep', x, 1) - return; - } - if (typeof hitBrick !== "undefined") { - const initialBrickColor = gameState.bricks[hitBrick]; - - 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] - ) { - gameState.bricks[hitBrick] = "black"; - ball.sapperUses++; - } - } - - if (!isOptionOn("basic")) { - ball.sparks += (delta * (gameState.combo - 1)) / 30; - if (ball.sparks > 1) { - gameState.flashes.push({ - type: "particle", - duration: 100 * ball.sparks, - time: gameState.levelTime, - size: gameState.coinSize / 2, - color: gameState.ballsColor, - x: ball.x, - y: ball.y, - vx: (Math.random() - 0.5) * gameState.baseSpeed, - vy: (Math.random() - 0.5) * gameState.baseSpeed, - ethereal: false, - }); - ball.sparks = 0; - } + ball.sparks = 0; } + } } -function append(makeItem:(match:T|null)=>T,where:ReusableArray){ - while(where.list[where.indexMin] && !where.list[where.indexMin].destroyed - && where.indexMin) => { + p.x = x; + p.y = y; + 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.weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01); + p.points = points; + }); } -function destroy(where:ReusableArray, index:number){ - where.list[index].destroyed=true - where.indexMin = Math.min(where.indexMin, index) +function makeParticle( + 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; + }); +} -} \ No newline at end of file +function makeText( + gameState: GameState, + x: number, + y: number, + color: colorString, + text: string, + size = 20, + duration = 150, +) { + append(gameState.texts, (p: Partial) => { + p.time = gameState.levelTime; + p.x = x; + p.y = y; + p.color = color; + p.size = size; + p.duration = duration; + p.text = text; + }); +} + +function makeLight( + 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; + }); +} + +export function append( + 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++; + console.log("Reused item " + where.indexMin); + } else { + console.log("Created item " + where.indexMin); + 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--; +} + +export function liveCount(where: ReusableArray) { + return where.total; +} + +export function empty(where: ReusableArray) { + where.total = 0; + where.indexMin = 0; + where.list.forEach((i) => (i.destroyed = true)); +} + +export function forEachLiveOne( + where: ReusableArray, + cb: (t: T, index: number) => void, +) { + where.list.forEach((item: T, index: number) => { + if (item && !item.destroyed) { + cb(item, index); + } + }); +} + +//TODO check destroyed usage in code diff --git a/src/game_utils.ts b/src/game_utils.ts index afe30da..bde47c4 100644 --- a/src/game_utils.ts +++ b/src/game_utils.ts @@ -1,5 +1,5 @@ -import {Ball, GameState, PerkId, PerksMap} from "./types"; -import {icons, upgrades} from "./loadGameData"; +import { Ball, GameState, PerkId, PerksMap } from "./types"; +import { icons, upgrades } from "./loadGameData"; export function getMajorityValue(arr: string[]): string { const count: { [k: string]: number } = {}; @@ -101,16 +101,16 @@ export function distanceBetween( } export function defaultSounds() { - return { - aboutToPlaySound: { - wallBeep: {vol: 0, x: 0}, - comboIncreaseMaybe: {vol: 0, x: 0}, - comboDecrease: {vol: 0, x: 0}, - coinBounce: {vol: 0, x: 0}, - explode: {vol: 0, x: 0}, - lifeLost: {vol: 0, x: 0}, - coinCatch: {vol: 0, x: 0}, - colorChange: {vol: 0, x: 0}, - } - } -} \ No newline at end of file + return { + aboutToPlaySound: { + wallBeep: { vol: 0, x: 0 }, + comboIncreaseMaybe: { vol: 0, x: 0 }, + comboDecrease: { vol: 0, x: 0 }, + coinBounce: { vol: 0, x: 0 }, + explode: { vol: 0, x: 0 }, + lifeLost: { vol: 0, x: 0 }, + coinCatch: { vol: 0, x: 0 }, + colorChange: { vol: 0, x: 0 }, + }, + }; +} diff --git a/src/i18n/b71.babel b/src/i18n/b71.babel index d58380e..a536406 100644 --- a/src/i18n/b71.babel +++ b/src/i18n/b71.babel @@ -1,5 +1,5 @@ - +