mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-20 04:05:06 -04:00
Trying to get file dowload to work
This commit is contained in:
parent
5ca2d58c9d
commit
ffdbd71a88
28 changed files with 1525 additions and 5236 deletions
|
@ -3,6 +3,8 @@
|
||||||
Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
||||||
|
|
||||||
- [Play now](https://breakout.lecaro.me/)
|
- [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)
|
- [Post your comments on itch.io](https://renanlecaro.itch.io/breakout71)
|
||||||
- [Help and tips about the game](./Help.md)
|
- [Help and tips about the game](./Help.md)
|
||||||
- [Credits](./Credits.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)
|
- [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout)
|
||||||
- [GitLab](https://gitlab.com/lecarore/breakout71)
|
- [GitLab](https://gitlab.com/lecarore/breakout71)
|
||||||
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
|
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
|
||||||
- [Donate](https://github.com/sponsors/renanlecaro)
|
|
||||||
|
|
||||||
|
|
||||||
# System requirements
|
# System requirements
|
||||||
|
@ -22,8 +23,7 @@ There's also an easy mode for kids (slower ball).
|
||||||
|
|
||||||
|
|
||||||
# Next
|
# Next
|
||||||
|
- check which color you get if picking a color related perk
|
||||||
- separate particles by type, reuse coins and particles
|
|
||||||
- sturdy bricks map of remaining hits
|
- sturdy bricks map of remaining hits
|
||||||
|
|
||||||
# bugs
|
# 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: analyzer - permet de voir les caractéristiques cachées des blocs (sturdy…)
|
||||||
- [colin] perk: roulette - gagne instantanément 2 perks aléatoires
|
- [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
|
- 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
|
# extra levels
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ android {
|
||||||
applicationId = "me.lecaro.breakout"
|
applicationId = "me.lecaro.breakout"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 29036807
|
versionCode = 29038230
|
||||||
versionName = "29036807"
|
versionName = "29038230"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
<?xml version="1.0" encoding ="utf-8"?>
|
<?xml version="1.0" encoding ="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
<application
|
<application
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,8 +1,10 @@
|
||||||
package me.lecaro.breakout
|
package me.lecaro.breakout
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
@ -18,8 +20,11 @@ import android.widget.Toast
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.jar.Manifest
|
||||||
|
|
||||||
const val CHOOSE_FILE_REQUEST_CODE = 548459
|
const val CHOOSE_FILE_REQUEST_CODE = 548459
|
||||||
|
const val PERM_REQUEST_CODE = 66622635
|
||||||
|
|
||||||
class MainActivity : android.app.Activity() {
|
class MainActivity : android.app.Activity() {
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
@ -28,58 +33,52 @@ class MainActivity : android.app.Activity() {
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
CHOOSE_FILE_REQUEST_CODE -> {
|
CHOOSE_FILE_REQUEST_CODE -> {
|
||||||
if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
filePathCallback?.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data))
|
filePathCallback?.onReceiveValue(
|
||||||
|
WebChromeClient.FileChooserParams.parseResult(
|
||||||
|
resultCode,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
)
|
||||||
filePathCallback = null
|
filePathCallback = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, 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<Array<Uri>>? = null
|
var filePathCallback: ValueCallback<Array<Uri>>? = null
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
var fileToDownload:String? = null
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
||||||
window.setFlags(
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
||||||
);
|
|
||||||
val webView = WebView(this)
|
|
||||||
webView.settings.javaScriptEnabled = true
|
|
||||||
webView.settings.domStorageEnabled = true
|
|
||||||
webView.settings.setSupportZoom(false)
|
|
||||||
webView.loadUrl("file:///android_asset/index.html?isInWebView=true")
|
|
||||||
|
|
||||||
|
|
||||||
webView.webChromeClient = object : WebChromeClient() {
|
|
||||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
|
||||||
Log.d(
|
|
||||||
"WebView", "${consoleMessage.message()} -- From line " +
|
|
||||||
"${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}"
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
override fun onShowFileChooser(webView: WebView?, filePathCallback: ValueCallback<Array<Uri>>?, fileChooserParams: FileChooserParams?): Boolean {
|
|
||||||
startActivityForResult(fileChooserParams?.createIntent(), CHOOSE_FILE_REQUEST_CODE)
|
|
||||||
this@MainActivity.filePathCallback = filePathCallback
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
|
|
||||||
|
|
||||||
|
fun downloadFile(){
|
||||||
|
val url = fileToDownload ?: return
|
||||||
|
try{
|
||||||
if (!url.startsWith("data:")) {
|
if (!url.startsWith("data:")) {
|
||||||
Log.w("DL", "url ignored because it does not start with data:")
|
Log.w("DL", "url ignored because it does not start with data:")
|
||||||
return@DownloadListener
|
return
|
||||||
}
|
}
|
||||||
val sdf = SimpleDateFormat("yyyy-M-dd-hh-mm")
|
val sdf = SimpleDateFormat("yyyy-M-dd-hh-mm")
|
||||||
val currentDate = sdf.format(Date())
|
val currentDate = sdf.format(Date())
|
||||||
// Extract filename from contentDisposition if available
|
// Extract filename from contentDisposition if available
|
||||||
|
|
||||||
|
|
||||||
if (url.startsWith("data:application/json;base64,")) {
|
if (url.startsWith("data:application/json;base64,")) {
|
||||||
Log.d("DL", "saving application/json ")
|
Log.d("DL", "saving application/json ")
|
||||||
val base64Data = url.substringAfterLast(',')
|
val base64Data = url.substringAfterLast(',')
|
||||||
val decodedBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT)
|
val decodedBytes =
|
||||||
val jsonData = String(decodedBytes)
|
android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT)
|
||||||
;
|
val jsonData = String(decodedBytes);
|
||||||
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
val dir =
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
val fileName = "breakout-71-save-$currentDate.b71"
|
val fileName = "breakout-71-save-$currentDate.b71"
|
||||||
val file = File(dir, fileName)
|
val file = File(dir, fileName)
|
||||||
file.writeText(jsonData)
|
file.writeText(jsonData)
|
||||||
|
@ -93,7 +92,66 @@ class MainActivity : android.app.Activity() {
|
||||||
} else {
|
} else {
|
||||||
Log.w("DL", "unexpected type " + url)
|
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);
|
||||||
|
window.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
);
|
||||||
|
val webView = WebView(this)
|
||||||
|
webView.settings.javaScriptEnabled = true
|
||||||
|
webView.settings.domStorageEnabled = true
|
||||||
|
webView.settings.setSupportZoom(false)
|
||||||
|
|
||||||
|
webView.loadUrl("file:///android_asset/index.html?isInWebView=true")
|
||||||
|
val activity=this;
|
||||||
|
|
||||||
|
webView.webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||||
|
Log.d(
|
||||||
|
"WebView", "${consoleMessage.message()} -- From line " +
|
||||||
|
"${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}"
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowFileChooser(
|
||||||
|
webView: WebView?,
|
||||||
|
filePathCallback: ValueCallback<Array<Uri>>?,
|
||||||
|
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 ->
|
||||||
|
|
||||||
|
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{
|
||||||
|
downloadFile()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
Sample backup rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/guide/topics/data/autobackup
|
|
||||||
for details.
|
|
||||||
Note: This file is ignored for devices older that API 31
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore
|
|
||||||
-->
|
|
||||||
<full-backup-content>
|
<full-backup-content>
|
||||||
<!--
|
|
||||||
<include domain="sharedpref" path="."/>
|
<include domain="sharedpref" path="."/>
|
||||||
<exclude domain="sharedpref" path="device.xml"/>
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
-->
|
|
||||||
</full-backup-content>
|
</full-backup-content>
|
|
@ -1,19 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
Sample data extraction rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
|
||||||
for details.
|
|
||||||
-->
|
|
||||||
<data-extraction-rules>
|
<data-extraction-rules>
|
||||||
<cloud-backup>
|
<cloud-backup>
|
||||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
<include domain="sharedpref"/>
|
||||||
<include .../>
|
|
||||||
<exclude .../>
|
|
||||||
-->
|
|
||||||
</cloud-backup>
|
</cloud-backup>
|
||||||
<!--
|
|
||||||
<device-transfer>
|
<device-transfer>
|
||||||
<include .../>
|
<include domain="sharedpref"/>
|
||||||
<exclude .../>
|
|
||||||
</device-transfer>
|
</device-transfer>
|
||||||
-->
|
|
||||||
</data-extraction-rules>
|
</data-extraction-rules>
|
8
build.sh
8
build.sh
|
@ -5,7 +5,6 @@
|
||||||
defaultVersionCode=$(($(date +%s) / 60))
|
defaultVersionCode=$(($(date +%s) / 60))
|
||||||
versionCode=${1:-$defaultVersionCode}
|
versionCode=${1:-$defaultVersionCode}
|
||||||
|
|
||||||
# TODO crash without app version
|
|
||||||
|
|
||||||
source ~/.nvm/nvm.sh;
|
source ~/.nvm/nvm.sh;
|
||||||
|
|
||||||
|
@ -17,13 +16,6 @@ if [[ $(node --version) != v21* ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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 -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
#!/bin/bash
|
#!/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 -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
|
|
33
dist/PWA/sw-b71.js
vendored
33
dist/PWA/sw-b71.js
vendored
|
@ -1,33 +1,2 @@
|
||||||
// The version of the cache.
|
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]<a[3])){c.label=i[1];break}if(6===i[0]&&c.label<a[1]){c.label=a[1],a=i;break}if(a&&c.label<a[2]){c.label=a[2],c.ops.push(i);break}a[2]&&c.ops.pop(),c.trys.pop();continue}i=t.call(e,c)}catch(e){i=[6,e],r=0}finally{n=a=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,o])}}}var r="breakout-71-".concat("29038230"),a=["/"];self.addEventListener("install",function(e){e.waitUntil(t(function(){return n(this,function(e){switch(e.label){case 0:return[4,caches.open(r)];case 1:return e.sent().addAll(a),[2]}})})())}),self.addEventListener("activate",function(e){e.waitUntil(t(function(){return n(this,function(e){switch(e.label){case 0:return[4,caches.keys()];case 1:return[4,Promise.all(e.sent().map(function(e){if(e!==r)return caches.delete(e)}))];case 2:return e.sent(),[4,clients.claim()];case 3:return e.sent(),[2]}})})())}),self.addEventListener("fetch",function(e){if("navigate"===e.request.mode&&e.request.url.endsWith("/index.html?isPWA=true")){e.respondWith(caches.match("/"));return}});
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//# sourceMappingURL=sw-b71.js.map
|
//# sourceMappingURL=sw-b71.js.map
|
||||||
|
|
2
dist/PWA/sw-b71.js.map
vendored
2
dist/PWA/sw-b71.js.map
vendored
File diff suppressed because one or more lines are too long
3849
dist/index.html
vendored
3849
dist/index.html
vendored
File diff suppressed because one or more lines are too long
19
package-lock.json
generated
19
package-lock.json
generated
|
@ -25,6 +25,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/optimizer-data-url": "^2.13.3",
|
"@parcel/optimizer-data-url": "^2.13.3",
|
||||||
|
"@parcel/packager-raw-url": "^2.13.3",
|
||||||
"@parcel/transformer-inline-string": "^2.13.3",
|
"@parcel/transformer-inline-string": "^2.13.3",
|
||||||
"@parcel/transformer-less": "^2.13.3",
|
"@parcel/transformer-less": "^2.13.3",
|
||||||
"@parcel/transformer-webmanifest": "^2.13.3",
|
"@parcel/transformer-webmanifest": "^2.13.3",
|
||||||
|
@ -1669,6 +1670,24 @@
|
||||||
"url": "https://opencollective.com/parcel"
|
"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": {
|
"node_modules/@parcel/packager-svg": {
|
||||||
"version": "2.13.3",
|
"version": "2.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.13.3.tgz",
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/optimizer-data-url": "^2.13.3",
|
"@parcel/optimizer-data-url": "^2.13.3",
|
||||||
|
"@parcel/packager-raw-url": "^2.13.3",
|
||||||
"@parcel/transformer-inline-string": "^2.13.3",
|
"@parcel/transformer-inline-string": "^2.13.3",
|
||||||
"@parcel/transformer-less": "^2.13.3",
|
"@parcel/transformer-less": "^2.13.3",
|
||||||
"@parcel/transformer-webmanifest": "^2.13.3",
|
"@parcel/transformer-webmanifest": "^2.13.3",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// The version of the cache.
|
// The version of the cache.
|
||||||
const VERSION = "29036807";
|
const VERSION = "29038230";
|
||||||
|
|
||||||
// The name of the cache
|
// The name of the cache
|
||||||
const CACHE_NAME = `breakout-71-${VERSION}`;
|
const CACHE_NAME = `breakout-71-${VERSION}`;
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
"29036807"
|
"29038230"
|
||||||
|
|
71
src/game.ts
71
src/game.ts
|
@ -3,9 +3,12 @@ import {
|
||||||
Ball,
|
Ball,
|
||||||
Coin,
|
Coin,
|
||||||
GameState,
|
GameState,
|
||||||
|
LightFlash,
|
||||||
OptionId,
|
OptionId,
|
||||||
|
ParticleFlash,
|
||||||
PerkId,
|
PerkId,
|
||||||
RunParams,
|
RunParams,
|
||||||
|
TextFlash,
|
||||||
Upgrade,
|
Upgrade,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getAudioContext, playPendingSounds } from "./sounds";
|
import { getAudioContext, playPendingSounds } from "./sounds";
|
||||||
|
@ -20,6 +23,8 @@ import "./PWA/sw_loader";
|
||||||
import { getCurrentLang, t } from "./i18n/i18n";
|
import { getCurrentLang, t } from "./i18n/i18n";
|
||||||
import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
|
import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
|
||||||
import {
|
import {
|
||||||
|
empty,
|
||||||
|
forEachLiveOne,
|
||||||
gameStateTick,
|
gameStateTick,
|
||||||
normalizeGameState,
|
normalizeGameState,
|
||||||
pickRandomUpgrades,
|
pickRandomUpgrades,
|
||||||
|
@ -56,6 +61,7 @@ import {hashCode} from "./getLevelBackground";
|
||||||
export function play() {
|
export function play() {
|
||||||
if (gameState.running) return;
|
if (gameState.running) return;
|
||||||
gameState.running = true;
|
gameState.running = true;
|
||||||
|
gameState.ballStickToPuck = false;
|
||||||
|
|
||||||
startRecordingGame(gameState);
|
startRecordingGame(gameState);
|
||||||
getAudioContext()?.resume();
|
getAudioContext()?.resume();
|
||||||
|
@ -95,6 +101,10 @@ export function pause(playerAskedForPause: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fitSize = () => {
|
export const fitSize = () => {
|
||||||
|
const past_off = gameState.offsetXRoundedDown,
|
||||||
|
past_width = gameState.gameZoneWidthRoundedUp,
|
||||||
|
past_heigh = gameState.gameZoneHeight;
|
||||||
|
|
||||||
const { width, height } = gameCanvas.getBoundingClientRect();
|
const { width, height } = gameCanvas.getBoundingClientRect();
|
||||||
gameState.canvasWidth = width;
|
gameState.canvasWidth = width;
|
||||||
gameState.canvasHeight = height;
|
gameState.canvasHeight = height;
|
||||||
|
@ -123,10 +133,27 @@ export const fitSize = () => {
|
||||||
backgroundCanvas.title = "resized";
|
backgroundCanvas.title = "resized";
|
||||||
// Ensure puck stays within bounds
|
// Ensure puck stays within bounds
|
||||||
setMousePos(gameState, gameState.puckPosition);
|
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);
|
pause(true);
|
||||||
putBallsAtPuck(gameState);
|
|
||||||
// For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
// For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--vh",
|
"--vh",
|
||||||
|
@ -273,7 +300,9 @@ gameCanvas.addEventListener("mousemove", (e) => {
|
||||||
gameCanvas.addEventListener("touchstart", (e) => {
|
gameCanvas.addEventListener("touchstart", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!e.touches?.length) return;
|
if (!e.touches?.length) return;
|
||||||
|
|
||||||
setMousePos(gameState, e.touches[0].pageX);
|
setMousePos(gameState, e.touches[0].pageX);
|
||||||
|
normalizeGameState(gameState);
|
||||||
play();
|
play();
|
||||||
});
|
});
|
||||||
gameCanvas.addEventListener("touchend", (e) => {
|
gameCanvas.addEventListener("touchend", (e) => {
|
||||||
|
@ -442,7 +471,6 @@ export function tick() {
|
||||||
gameState.puckPosition + gameState.keyboardPuckSpeed,
|
gameState.puckPosition + gameState.keyboardPuckSpeed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeGameState(gameState);
|
normalizeGameState(gameState);
|
||||||
|
|
||||||
if (gameState.running) {
|
if (gameState.running) {
|
||||||
|
@ -457,8 +485,8 @@ export function tick() {
|
||||||
if (gameState.running) {
|
if (gameState.running) {
|
||||||
recordOneFrame(gameState);
|
recordOneFrame(gameState);
|
||||||
}
|
}
|
||||||
if(isOptionOn('sound') ){
|
if (isOptionOn("sound")) {
|
||||||
playPendingSounds(gameState)
|
playPendingSounds(gameState);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
|
@ -514,10 +542,13 @@ async function openScorePanel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("menu")?.addEventListener("click", (e) => {
|
(document.getElementById("menu") as HTMLButtonElement).addEventListener(
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openSettingsPanel();
|
openSettingsPanel();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
async function openSettingsPanel() {
|
async function openSettingsPanel() {
|
||||||
pause(true);
|
pause(true);
|
||||||
|
@ -665,7 +696,7 @@ async function openSettingsPanel() {
|
||||||
localStorageContent[key] = value;
|
localStorageContent[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signedPayload=JSON.stringify(localStorageContent)
|
const signedPayload = JSON.stringify(localStorageContent);
|
||||||
const dlLink = document.createElement("a");
|
const dlLink = document.createElement("a");
|
||||||
|
|
||||||
dlLink.setAttribute(
|
dlLink.setAttribute(
|
||||||
|
@ -676,7 +707,10 @@ async function openSettingsPanel() {
|
||||||
fileType: "B71-save-file",
|
fileType: "B71-save-file",
|
||||||
appVersion,
|
appVersion,
|
||||||
signedPayload,
|
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 {
|
const {
|
||||||
fileType,
|
fileType,
|
||||||
appVersion: fileVersion,
|
appVersion: fileVersion,
|
||||||
signedPayload,key
|
signedPayload,
|
||||||
|
key,
|
||||||
} = JSON.parse(content);
|
} = JSON.parse(content);
|
||||||
if (fileType !== "B71-save-file")
|
if (fileType !== "B71-save-file")
|
||||||
throw new Error("Not a B71 save file");
|
throw new Error("Not a B71 save file");
|
||||||
|
@ -738,11 +773,17 @@ async function openSettingsPanel() {
|
||||||
" or newer.",
|
" or newer.",
|
||||||
);
|
);
|
||||||
|
|
||||||
if(key!== hashCode('Security by obscurity, but really the game is oss so eh'+signedPayload)){
|
if (
|
||||||
throw new Error("Key does not match content.")
|
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();
|
localStorage.clear();
|
||||||
for (let key in localStorageContent) {
|
for (let key in localStorageContent) {
|
||||||
localStorage.setItem(key, localStorageContent[key]);
|
localStorage.setItem(key, localStorageContent[key]);
|
||||||
|
@ -982,4 +1023,4 @@ tick();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// window.stressTest= ()=>restart({level:'Shark',perks:{base_combo:100, pierce:10, multiball:8}})
|
// window.stressTest= ()=>restart({level:'Shark',perks:{base_combo:100, pierce:10, multiball:8}})
|
||||||
window.stressTest = () =>
|
window.stressTest = () =>
|
||||||
restart({ level: "Shark", perks: { sapper: 2, pierce: 10, multiball: 3 } });
|
restart({ level: "Bird", perks: { sapper: 2, pierce: 10, multiball: 3 } });
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -111,6 +111,6 @@ export function defaultSounds() {
|
||||||
lifeLost: { vol: 0, x: 0 },
|
lifeLost: { vol: 0, x: 0 },
|
||||||
coinCatch: { vol: 0, x: 0 },
|
coinCatch: { vol: 0, x: 0 },
|
||||||
colorChange: { vol: 0, x: 0 },
|
colorChange: { vol: 0, x: 0 },
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<babeledit_project be_version="5.3.0" version="1.3">
|
<babeledit_project be_version="5.2.0" version="1.3">
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
BabelEdit project file
|
BabelEdit project file
|
||||||
|
@ -3353,7 +3353,6 @@
|
||||||
</package_node>
|
</package_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<embedded_source_texts>false</embedded_source_texts>
|
|
||||||
<isTemplateProject>false</isTemplateProject>
|
<isTemplateProject>false</isTemplateProject>
|
||||||
<languages>
|
<languages>
|
||||||
<language>
|
<language>
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"main_menu.basic_help": "Fewer particles and flashes, better performance.",
|
"main_menu.basic_help": "Fewer particles and flashes, better performance.",
|
||||||
"main_menu.download_save_file": "Download save file",
|
"main_menu.download_save_file": "Download save file",
|
||||||
"main_menu.download_save_file_help": "Get a transferable .b71 file with your score and stats",
|
"main_menu.download_save_file_help": "Get a transferable .b71 file with your score and stats",
|
||||||
"main_menu.footer_html": " <p> <span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n <a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n <a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n <a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n <a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n <a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n <a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n <a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n <span>v.{{appVersion}}</span></p>",
|
"main_menu.footer_html": "<p> \n<span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donate</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n<span>v.{{appVersion}}</span>\n</p>\n",
|
||||||
"main_menu.fullscreen": "Fullscreen",
|
"main_menu.fullscreen": "Fullscreen",
|
||||||
"main_menu.fullscreen_exit": "Exit Fullscreen",
|
"main_menu.fullscreen_exit": "Exit Fullscreen",
|
||||||
"main_menu.fullscreen_exit_help": "Might not work on some machines",
|
"main_menu.fullscreen_exit_help": "Might not work on some machines",
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"main_menu.basic_help": "Moins de particules et effets, meilleures performances.",
|
"main_menu.basic_help": "Moins de particules et effets, meilleures performances.",
|
||||||
"main_menu.download_save_file": "Sauvegarder mes progrès",
|
"main_menu.download_save_file": "Sauvegarder mes progrès",
|
||||||
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde .b71 transférable",
|
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde .b71 transférable",
|
||||||
"main_menu.footer_html": " <p> <span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> <a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> <a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a> <a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a> <a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> <a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a> <a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a> <a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a> <span>v.{{appVersion}}</span></p>",
|
"main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>",
|
||||||
"main_menu.fullscreen": "Plein écran",
|
"main_menu.fullscreen": "Plein écran",
|
||||||
"main_menu.fullscreen_exit": "Quitter le plein écran",
|
"main_menu.fullscreen_exit": "Quitter le plein écran",
|
||||||
"main_menu.fullscreen_exit_help": "Peut ne pas fonctionner sur certaines machines",
|
"main_menu.fullscreen_exit_help": "Peut ne pas fonctionner sur certaines machines",
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { GameState, RunParams } from "./types";
|
import { GameState, RunParams } from "./types";
|
||||||
import { getTotalScore } from "./settings";
|
import { getTotalScore } from "./settings";
|
||||||
import { allLevels, upgrades } from "./loadGameData";
|
import { allLevels, upgrades } from "./loadGameData";
|
||||||
import {defaultSounds, getPossibleUpgrades, makeEmptyPerksMap, sumOfKeys,} from "./game_utils";
|
import {
|
||||||
|
defaultSounds,
|
||||||
|
getPossibleUpgrades,
|
||||||
|
makeEmptyPerksMap,
|
||||||
|
sumOfKeys,
|
||||||
|
} from "./game_utils";
|
||||||
import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
|
import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
|
||||||
import { isOptionOn } from "./options";
|
import { isOptionOn } from "./options";
|
||||||
|
|
||||||
|
|
||||||
export function newGameState(params: RunParams): GameState {
|
export function newGameState(params: RunParams): GameState {
|
||||||
const totalScoreAtRunStart = getTotalScore();
|
const totalScoreAtRunStart = getTotalScore();
|
||||||
const firstLevel = params?.level
|
const firstLevel = params?.level
|
||||||
|
@ -33,6 +37,7 @@ export function newGameState(params: RunParams): GameState {
|
||||||
combo: 1,
|
combo: 1,
|
||||||
gridSize: 12,
|
gridSize: 12,
|
||||||
running: false,
|
running: false,
|
||||||
|
ballStickToPuck: true,
|
||||||
puckPosition: 400,
|
puckPosition: 400,
|
||||||
pauseTimeout: null,
|
pauseTimeout: null,
|
||||||
canvasWidth: 0,
|
canvasWidth: 0,
|
||||||
|
@ -50,10 +55,10 @@ export function newGameState(params: RunParams): GameState {
|
||||||
balls: [],
|
balls: [],
|
||||||
ballsColor: "white",
|
ballsColor: "white",
|
||||||
bricks: [],
|
bricks: [],
|
||||||
lights: {indexMin:0,list:[]},
|
lights: { indexMin: 0, total: 0, list: [] },
|
||||||
particles: {indexMin:0,list:[]},
|
particles: { indexMin: 0, total: 0, list: [] },
|
||||||
texts: {indexMin:0,list:[]},
|
texts: { indexMin: 0, total: 0, list: [] },
|
||||||
coins: {indexMin:0,list:[]},
|
coins: { indexMin: 0, total: 0, list: [] },
|
||||||
levelStartScore: 0,
|
levelStartScore: 0,
|
||||||
levelMisses: 0,
|
levelMisses: 0,
|
||||||
levelSpawnedCoins: 0,
|
levelSpawnedCoins: 0,
|
||||||
|
@ -90,7 +95,7 @@ export function newGameState(params: RunParams): GameState {
|
||||||
levelWallBounces: 0,
|
levelWallBounces: 0,
|
||||||
needsRender: true,
|
needsRender: true,
|
||||||
autoCleanUses: 0,
|
autoCleanUses: 0,
|
||||||
...defaultSounds()
|
...defaultSounds(),
|
||||||
};
|
};
|
||||||
resetBalls(gameState);
|
resetBalls(gameState);
|
||||||
|
|
||||||
|
@ -109,4 +114,3 @@ export function newGameState(params: RunParams): GameState {
|
||||||
}
|
}
|
||||||
return gameState;
|
return gameState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -323,12 +323,12 @@ export const rawUpgrades = [
|
||||||
giftable: false,
|
giftable: false,
|
||||||
id: "sturdy_bricks",
|
id: "sturdy_bricks",
|
||||||
max: 4,
|
max: 4,
|
||||||
name: t("upgrades.telekinesis.name"),
|
name: t("upgrades.sturdy_bricks.name"),
|
||||||
help: (lvl: number) =>
|
help: (lvl: number) =>
|
||||||
lvl == 1
|
lvl == 1
|
||||||
? t("upgrades.telekinesis.help")
|
? t("upgrades.sturdy_bricks.help")
|
||||||
: t("upgrades.telekinesis.help_plural"),
|
: t("upgrades.sturdy_bricks.help_plural"),
|
||||||
fullHelp: t("upgrades.telekinesis.fullHelp"),
|
fullHelp: t("upgrades.sturdy_bricks.fullHelp"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
requires: "",
|
requires: "",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { baseCombo } from "./gameStateMutators";
|
import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
|
||||||
import {
|
import {
|
||||||
brickCenterX,
|
brickCenterX,
|
||||||
brickCenterY,
|
brickCenterY,
|
||||||
|
@ -54,8 +54,7 @@ export function render(gameState: GameState) {
|
||||||
|
|
||||||
ctx.globalCompositeOperation = "screen";
|
ctx.globalCompositeOperation = "screen";
|
||||||
ctx.globalAlpha = 0.6;
|
ctx.globalAlpha = 0.6;
|
||||||
gameState.coins.forEach((coin) => {
|
forEachLiveOne(gameState.coins, (coin) => {
|
||||||
if (!coin.destroyed)
|
|
||||||
drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y);
|
drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y);
|
||||||
});
|
});
|
||||||
gameState.balls.forEach((ball) => {
|
gameState.balls.forEach((ball) => {
|
||||||
|
@ -81,17 +80,19 @@ export function render(gameState: GameState) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
gameState.flashes.forEach((flash) => {
|
forEachLiveOne(gameState.lights, (flash) => {
|
||||||
const { x, y, time, color, size, type, duration } = flash;
|
const { x, y, time, color, size, duration } = flash;
|
||||||
const elapsed = gameState.levelTime - time;
|
const elapsed = gameState.levelTime - time;
|
||||||
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
|
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
|
||||||
if (type === "ball") {
|
|
||||||
drawFuzzyBall(ctx, color, size, x, y);
|
drawFuzzyBall(ctx, color, size, x, y);
|
||||||
}
|
|
||||||
if (type === "particle") {
|
|
||||||
drawFuzzyBall(ctx, color, size * 3, x, y);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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);
|
||||||
|
drawFuzzyBall(ctx, color, size * 3, x, y);
|
||||||
|
});
|
||||||
|
|
||||||
// Decides how brights the bg black parts can get
|
// Decides how brights the bg black parts can get
|
||||||
ctx.globalAlpha = 0.2;
|
ctx.globalAlpha = 0.2;
|
||||||
ctx.globalCompositeOperation = "multiply";
|
ctx.globalCompositeOperation = "multiply";
|
||||||
|
@ -128,14 +129,11 @@ export function render(gameState: GameState) {
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = "source-over";
|
||||||
ctx.fillStyle = level.color || "#000";
|
ctx.fillStyle = level.color || "#000";
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
forEachLiveOne(gameState.particles, (flash) => {
|
||||||
gameState.flashes.forEach((flash) => {
|
const { x, y, time, color, size, duration } = flash;
|
||||||
const { x, y, time, color, size, type, duration } = flash;
|
|
||||||
const elapsed = gameState.levelTime - time;
|
const elapsed = gameState.levelTime - time;
|
||||||
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
|
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
|
||||||
if (type === "particle") {
|
|
||||||
drawBall(ctx, color, size, x, y);
|
drawBall(ctx, color, size, x, y);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,9 +159,7 @@ export function render(gameState: GameState) {
|
||||||
}
|
}
|
||||||
// Coins
|
// Coins
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
|
forEachLiveOne(gameState.coins, (coin) => {
|
||||||
gameState.coins.forEach((coin) => {
|
|
||||||
if (!coin.destroyed) {
|
|
||||||
ctx.globalCompositeOperation =
|
ctx.globalCompositeOperation =
|
||||||
coin.color === "gold" || level.color ? "source-over" : "screen";
|
coin.color === "gold" || level.color ? "source-over" : "screen";
|
||||||
drawCoin(
|
drawCoin(
|
||||||
|
@ -175,13 +171,12 @@ export function render(gameState: GameState) {
|
||||||
level.color || "black",
|
level.color || "black",
|
||||||
coin.a,
|
coin.a,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Black shadow around balls
|
// Black shadow around balls
|
||||||
if (!isOptionOn("basic")) {
|
if (!isOptionOn("basic")) {
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = "source-over";
|
||||||
ctx.globalAlpha = Math.min(0.8, gameState.coins.length / 20);
|
ctx.globalAlpha = Math.min(0.8, liveCount(gameState.coins) / 20);
|
||||||
gameState.balls.forEach((ball) => {
|
gameState.balls.forEach((ball) => {
|
||||||
drawBall(
|
drawBall(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -197,22 +192,21 @@ export function render(gameState: GameState) {
|
||||||
renderAllBricks();
|
renderAllBricks();
|
||||||
|
|
||||||
ctx.globalCompositeOperation = "screen";
|
ctx.globalCompositeOperation = "screen";
|
||||||
gameState.flashes = gameState.flashes.filter(
|
forEachLiveOne(gameState.texts, (flash) => {
|
||||||
(f) => gameState.levelTime - f.time < f.duration && !f.destroyed,
|
const { x, y, time, color, size, duration } = flash;
|
||||||
);
|
|
||||||
|
|
||||||
gameState.flashes.forEach((flash) => {
|
|
||||||
const { x, y, time, color, size, type, duration } = flash;
|
|
||||||
const elapsed = gameState.levelTime - time;
|
const elapsed = gameState.levelTime - time;
|
||||||
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
|
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
|
||||||
if (type === "text") {
|
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = "source-over";
|
||||||
drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
|
drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
|
||||||
} else if (type === "particle") {
|
});
|
||||||
|
|
||||||
|
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";
|
ctx.globalCompositeOperation = "screen";
|
||||||
drawBall(ctx, color, size, x, y);
|
drawBall(ctx, color, size, x, y);
|
||||||
drawFuzzyBall(ctx, color, size, x, y);
|
drawFuzzyBall(ctx, color, size, x, y);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (gameState.perks.extra_life) {
|
if (gameState.perks.extra_life) {
|
||||||
|
|
|
@ -1,23 +1,28 @@
|
||||||
|
|
||||||
import { isOptionOn } from "./options";
|
import { isOptionOn } from "./options";
|
||||||
import { GameState } from "./types";
|
import { GameState } from "./types";
|
||||||
|
|
||||||
let lastPlay = Date.now()
|
let lastPlay = Date.now();
|
||||||
|
|
||||||
export function playPendingSounds(gameState: GameState) {
|
export function playPendingSounds(gameState: GameState) {
|
||||||
if (lastPlay > Date.now() - 60) {
|
if (lastPlay > Date.now() - 60) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
lastPlay=Date.now()
|
lastPlay = Date.now();
|
||||||
for (let key in gameState.aboutToPlaySound) {
|
for (let key in gameState.aboutToPlaySound) {
|
||||||
const soundName = key as keyof GameState["aboutToPlaySound"]
|
const soundName = key as keyof GameState["aboutToPlaySound"];
|
||||||
const ex = gameState.aboutToPlaySound[soundName] as {vol:number, x:number}
|
const ex = gameState.aboutToPlaySound[soundName] as {
|
||||||
|
vol: number;
|
||||||
|
x: number;
|
||||||
|
};
|
||||||
if (ex.vol) {
|
if (ex.vol) {
|
||||||
sounds[soundName](Math.min(2,ex.vol),pixelsToPan(gameState, ex.x), gameState.combo)
|
sounds[soundName](
|
||||||
ex.vol=0
|
Math.min(2, ex.vol),
|
||||||
|
pixelsToPan(gameState, ex.x),
|
||||||
|
gameState.combo,
|
||||||
|
);
|
||||||
|
ex.vol = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
export const sounds = {
|
export const sounds = {
|
||||||
wallBeep: (vol: number, pan: number, combo: number) => {
|
wallBeep: (vol: number, pan: number, combo: number) => {
|
||||||
|
@ -25,7 +30,7 @@ export const sounds = {
|
||||||
createSingleBounceSound(800, pan, vol);
|
createSingleBounceSound(800, pan, vol);
|
||||||
},
|
},
|
||||||
|
|
||||||
comboIncreaseMaybe: ( volume: number,pan: number,combo: number, ) => {
|
comboIncreaseMaybe: (volume: number, pan: number, combo: number) => {
|
||||||
if (!isOptionOn("sound")) return;
|
if (!isOptionOn("sound")) return;
|
||||||
let delta = 0;
|
let delta = 0;
|
||||||
if (!isNaN(lastComboPlayed)) {
|
if (!isNaN(lastComboPlayed)) {
|
||||||
|
@ -55,7 +60,7 @@ export const sounds = {
|
||||||
|
|
||||||
coinCatch(volume: number, pan: number, combo: number) {
|
coinCatch(volume: number, pan: number, combo: number) {
|
||||||
if (!isOptionOn("sound")) return;
|
if (!isOptionOn("sound")) return;
|
||||||
createSingleBounceSound(900, (pan), volume, 0.1, "triangle");
|
createSingleBounceSound(900, pan, volume, 0.1, "triangle");
|
||||||
},
|
},
|
||||||
colorChange(volume: number, pan: number, combo: number) {
|
colorChange(volume: number, pan: number, combo: number) {
|
||||||
createSingleBounceSound(400, pan, volume, 0.5, "sine");
|
createSingleBounceSound(400, pan, volume, 0.5, "sine");
|
||||||
|
|
42
src/types.d.ts
vendored
42
src/types.d.ts
vendored
|
@ -113,22 +113,22 @@ interface BaseFlash {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParticleFlash extends BaseFlash {
|
interface ParticleFlash extends BaseFlash {
|
||||||
type: "particle";
|
// type: "particle";
|
||||||
vx: number;
|
vx: number;
|
||||||
vy: number;
|
vy: number;
|
||||||
ethereal: boolean;
|
ethereal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextFlash extends BaseFlash {
|
interface TextFlash extends BaseFlash {
|
||||||
type: "text";
|
// type: "text";
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BallFlash extends BaseFlash {
|
interface LightFlash extends BaseFlash {
|
||||||
type: "ball";
|
// type: "ball";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Flash = ParticleFlash | TextFlash | BallFlash;
|
export type Flash = ParticleFlash | TextFlash | LightFlash;
|
||||||
|
|
||||||
export type RunStats = {
|
export type RunStats = {
|
||||||
started: number;
|
started: number;
|
||||||
|
@ -154,8 +154,9 @@ export type PerksMap = {
|
||||||
export type ReusableArray<T> = {
|
export type ReusableArray<T> = {
|
||||||
// All items below that index should not be destroyed
|
// All items below that index should not be destroyed
|
||||||
indexMin: number;
|
indexMin: number;
|
||||||
list:T[]
|
total: number;
|
||||||
}
|
list: T[];
|
||||||
|
};
|
||||||
|
|
||||||
export type RunHistoryItem = RunStats & {
|
export type RunHistoryItem = RunStats & {
|
||||||
perks?: PerksMap;
|
perks?: PerksMap;
|
||||||
|
@ -197,6 +198,7 @@ export type GameState = {
|
||||||
combo: number;
|
combo: number;
|
||||||
// Whether the game is running or paused
|
// Whether the game is running or paused
|
||||||
running: boolean;
|
running: boolean;
|
||||||
|
ballStickToPuck: boolean;
|
||||||
// Whether the game should be re-rendered once even if not running
|
// Whether the game should be re-rendered once even if not running
|
||||||
needsRender: boolean;
|
needsRender: boolean;
|
||||||
// Position of the center of the puck on the canvas in pixels, from the left of the canvas.
|
// Position of the center of the puck on the canvas in pixels, from the left of the canvas.
|
||||||
|
@ -220,11 +222,9 @@ export type GameState = {
|
||||||
// Array of bricks to display. 'black' means bomb. '' means no brick.
|
// Array of bricks to display. 'black' means bomb. '' means no brick.
|
||||||
bricks: colorString[];
|
bricks: colorString[];
|
||||||
|
|
||||||
|
particles: ReusableArray<ParticleFlash>;
|
||||||
|
texts: ReusableArray<TextFlash>;
|
||||||
particles: ReusableArray<ParticleFlash>
|
lights: ReusableArray<LightFlash>;
|
||||||
texts: ReusableArray<TextFlash>
|
|
||||||
lights: ReusableArray<BallFlash>
|
|
||||||
coins: ReusableArray<Coin>;
|
coins: ReusableArray<Coin>;
|
||||||
levelStartScore: number;
|
levelStartScore: number;
|
||||||
levelMisses: number;
|
levelMisses: number;
|
||||||
|
@ -249,15 +249,15 @@ export type GameState = {
|
||||||
levelWallBounces: number;
|
levelWallBounces: number;
|
||||||
autoCleanUses: number;
|
autoCleanUses: number;
|
||||||
aboutToPlaySound: {
|
aboutToPlaySound: {
|
||||||
wallBeep:{vol:number, x:number},
|
wallBeep: { vol: number; x: number };
|
||||||
comboIncreaseMaybe:{vol:number, x:number},
|
comboIncreaseMaybe: { vol: number; x: number };
|
||||||
comboDecrease:{vol:number, x:number},
|
comboDecrease: { vol: number; x: number };
|
||||||
coinBounce:{vol:number, x:number},
|
coinBounce: { vol: number; x: number };
|
||||||
explode:{vol:number, x:number},
|
explode: { vol: number; x: number };
|
||||||
lifeLost:{vol:number, x:number},
|
lifeLost: { vol: number; x: number };
|
||||||
coinCatch:{vol:number, x:number},
|
coinCatch: { vol: number; x: number };
|
||||||
colorChange:{vol:number, x:number},
|
colorChange: { vol: number; x: number };
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RunParams = {
|
export type RunParams = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue