Trying to get file dowload to work

This commit is contained in:
Renan LE CARO 2025-03-18 14:16:12 +01:00
parent 5ca2d58c9d
commit ffdbd71a88
28 changed files with 1525 additions and 5236 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()
}
}) })

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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
View file

@ -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

File diff suppressed because one or more lines are too long

3849
dist/index.html vendored

File diff suppressed because one or more lines are too long

19
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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}`;

View file

@ -1 +1 @@
"29036807" "29038230"

View file

@ -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

View file

@ -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 },
} },
} };
} }

View file

@ -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>

View file

@ -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",

View file

@ -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",

View file

@ -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;
} }

View file

@ -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: "",

View file

@ -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) {

View file

@ -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
View file

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