diff --git a/Readme.md b/Readme.md index b490a29..8359738 100644 --- a/Readme.md +++ b/Readme.md @@ -13,22 +13,70 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades ! # Current priorities -The goal of this project is to make a game used by many people. The game is already pretty fun. I'm now trying to -translate it to (Lebanese) Arabic, Russian and (Chilean) Spanish. Other translation are very welcome, contact me -if you'd like to submit one. - -While translations are being written, I'll try to avoid adding features that require new translations. That means only -bug fixes and optimisations, maybe adding levels. Once we have a nice stable release available in 4 -languages, I may add features again. +The goal of this project is to make a game used by many people. +The game is already pretty fun. +I'm now trying to translate it to (Lebanese) Arabic, Russian and (Chilean) Spanish. +Other translation are very welcome, contact me if you'd like to submit one. + # Changelog ## To do -- auto-detect device performance at first startup and adjust settings accordingly ## Done +- toast an error if storage is blocked +- toast an error if migration fails +- in apk, video download doesn't work +- ask for permanent storage +- option: reuse past frame's light in new frame lighting computation when there are 150+ coins on screen, to limit the performance impact of rendering lots of lights + +## 29084606 + +- simpler and more readable encoding for save files +- removed check of payload signature on save file, seemed to fail because of the poor encoding of the name of the "côte d'ivoire" level +- automatic detection of the number of steps required for physics +- trial runs detection fix + +## 29083397 + +- highlight last used creative level +- access autoplay mode from the menu +- access stress test mode from the menu, show real time stats +- Render bottom border differently to show how far the puck can go +- Corner Shot: scale like Need Some Space +- grey out irrelevant options in the settings +- Back to Creative Menu at the end of a Creative level + +## 29080170 + +- don't show unlock toast at first startup for levels that are unlocked by default +- Droplet particle color should be gold for gold coins +- added levels: A Very Dangerous High-Five, The Boys + +## 29079818 + +- Imported levels : Mario, Minesweeper and Target +- Fixed an issue with localstorage saving of custom levels + + +## 29079805 + +- combo text on paddle will be grey if we're at the base combo +- transparency now rounds up +- import level up to 21 x 21 +- corrected icon of "padding" + +## 29079087 + +- measured and improve the performance (test here https://breakout.lecaro.me/?stresstest) +- added a few levels +- autoplay mode (with wake lock and computer play https://breakout.lecaro.me/?autoplay ) +- Added particle and sound effect when coin drops below the "waterline" of the puck +- slower coins fall once they are under the paddle +- in game level editor +- allow loading newer save in outdated app (for rollback) - game crashes when reaching level 12 (no level info in runLevels) ## 29074385 @@ -296,6 +344,7 @@ languages, I may add features again. - make stats a clairvoyant thing - [colin]P ocket money — bricks absorb coins that touch them, which are released on brick destruction (with a bonus?) - [colin] turn ball gravity on after a top bar hit, and until bouncing on puck +- fan : paddle motion creates upward draft that lifts coins and balls ## Medium difficulty perks ideas - balls collision split them into 4 smaller balls, lvl times (requires rework) @@ -360,6 +409,12 @@ languages, I may add features again. ## UX / gameplay +- chill game mode, to just relax your mind : + - no 7 levels limit + - no upgrades offered at the end of the level + - get a random perk + - every 7 level it's replaced by another random perk + - every 7 levels, +10 base combo and +1 piece - avoid showing a +1 and -1 at the same time when a combo increase is reset - explain to iOS users how to add the app to home screen to get fullscreen - delayed start on mobile to let users place the puck where they want @@ -370,7 +425,6 @@ languages, I may add features again. ## Game engine features ideas - add a clickable button to allow sound to play in chrome android - save state in localstorage for easy resume of a game in progress -- ask for permanent storage - handle back bouton in menu - Offline mode web for iphone - controller support on web/mobile @@ -379,6 +433,7 @@ languages, I may add features again. ## Maybe one day - https://weblate.org/fr/ quite annoying to have merge conflicts while pushing, i'll enable it later. +- auto-detect device performance at first startup and adjust settings accordingly (hard to do in any sort of useful way) - [jaceys] Move the restart button out of the menu, so that it is more easily accessible (will allow user to choose starting perk instead) - colored coins only (coins should be of the color of the ball to count, otherwise what ? i'd rather avoid negative points) - coins avoid ball of different color (pointless) @@ -472,4 +527,5 @@ Breakout 71 can be installed and work offline in many ways: # System requirements The game should perform well even on low-end devices. It's very lean and does not take much storage space (Roughly 0.1MB). The web version is supposed to work on iOS safari, Firefox ESR and chrome, on desktop and mobile. -If the app stutters, turn on "fast mode" in the settings to render a simplified view that should be faster. You can adjust many aspects of the game there, go have a look ! \ No newline at end of file +If the app stutters, turn on "fast mode" in the settings to render a simplified view that should be faster. You can adjust many aspects of the game there, go have a look ! + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8d1bcab..8058c74 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,8 +29,8 @@ android { applicationId = "me.lecaro.breakout" minSdk = 21 targetSdk = 34 - versionCode = 29074738 - versionName = "29074738" + versionCode = 29085904 + versionName = "29085904" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true @@ -67,3 +67,6 @@ android { } } } +dependencies { + implementation(libs.androidx.core) +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 644eaef..7e787ff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,14 @@ - + + + \ No newline at end of file diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index 8e9eefa..ce5cadd 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -1 +1 @@ -Breakout 71 \ No newline at end of file +Breakout 71 \ No newline at end of file diff --git a/app/src/main/java/me/lecaro/breakout/MainActivity.kt b/app/src/main/java/me/lecaro/breakout/MainActivity.kt index 57648c0..1d9832e 100644 --- a/app/src/main/java/me/lecaro/breakout/MainActivity.kt +++ b/app/src/main/java/me/lecaro/breakout/MainActivity.kt @@ -1,11 +1,7 @@ package me.lecaro.breakout -import android.app.Activity -import android.app.DownloadManager import android.content.ContentValues -import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle @@ -20,10 +16,12 @@ import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebView import android.widget.Toast +import androidx.core.content.FileProvider import java.io.File +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import java.util.Date -import java.util.jar.Manifest const val CHOOSE_FILE_REQUEST_CODE = 548459 @@ -50,71 +48,79 @@ class MainActivity : android.app.Activity() { private fun downloadFile(url: String) { try { - if (!url.startsWith("data:")) { - Log.w("DL", "url ignored because it does not start with data:") - return - } val sdf = SimpleDateFormat("yyyy-M-dd-hh-mm") val currentDate = sdf.format(Date()) - val base64Data = url.substringAfterLast(',') - val decodedBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT) - if (url.startsWith("data:application/json;base64,")) { - writeFile(decodedBytes, "breakout-71-save-$currentDate.json", "application/json") + if (url.startsWith("data:application/json;charset=utf-8,")) { - } else if (url.startsWith("data:video/webm;base64,")) { - writeFile(decodedBytes, "breakout-71-gameplay-capture-$currentDate.webm", "video/webm") - } else { - Log.w("DL", "unexpected type " + url) + val urlEncoded = url.substring("data:application/json;charset=utf-8,".length) + val str = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8.name()) + writeFileAndShare(str.toByteArray(), "breakout-71-save-$currentDate.json", "application/json") } + + if (url.startsWith("data:video/webm;base64,")) { + val base64Data = url.substring("data:video/webm;base64,".length) + val decodedBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT) + writeFileAndShare(decodedBytes, "breakout-71-capture-$currentDate.webm", "video/webm") + } + + } catch (e: Exception) { Log.e("DL", "Error ${e.message}") Toast.makeText(this, "Error ${e.message}", Toast.LENGTH_LONG).show() } } - fun writeFile(decodedBytes:ByteArray,fileName:String, mime:String){ + fun writeFileAndShare(bytes:ByteArray, fileName: String, mime: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // android 10 + val contentValues = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, fileName) + put(MediaStore.Downloads.MIME_TYPE, mime) + put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } - - val jsonData = String(decodedBytes); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - - - val contentValues = ContentValues().apply { - put(MediaStore.Downloads.DISPLAY_NAME, fileName) - put(MediaStore.Downloads.MIME_TYPE,mime ) - put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) - } - - val uri: Uri? = contentResolver.insert( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues - ) - uri?.let { - contentResolver.openOutputStream(it)?.use { outputStream -> - outputStream.write(decodedBytes) - } - } - - val shareIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - // Example: content://com.google.android.apps.photos.contentprovider/... - putExtra(Intent.EXTRA_STREAM, uri) - type = mime - } - startActivity(Intent.createChooser(shareIntent, null)) - - } else { - - - val dir = getExternalFilesDir(null) - val file = File(dir, fileName) - file.writeText(jsonData) - Toast.makeText(this, "Saved in $dir", Toast.LENGTH_LONG).show() - + val uri: Uri? = contentResolver.insert( + MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues + ) + uri?.let { + contentResolver.openOutputStream(it)?.use { outputStream -> + outputStream.write(bytes) } + } + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = mime + } + startActivity(Intent.createChooser(shareIntent, null)) + + } else { + + val file = File(getExternalFilesDir(null), fileName) + file.writeBytes(bytes) + val uri = FileProvider.getUriForFile( + this, + "$packageName.fileprovider", // Adjust if your authority is different + file + ) + + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = mime + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + + startActivity(Intent.createChooser(shareIntent, null)) + + } } + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -155,13 +161,18 @@ class MainActivity : android.app.Activity() { } 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 -> + Log.d("DL", "url: ${url}") + Log.d("DL", "userAgent: ${userAgent}") + Log.d("DL", "contentDisposition: ${contentDisposition}") + Log.d("DL", "mimetype: ${mimetype}") + Log.d("DL", "contentLength: ${contentLength}") + downloadFile(url) }) diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..5e91a74 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index fd3af66..25a406c 100644 --- a/dist/index.html +++ b/dist/index.html @@ -75,8 +75,8 @@ canvas:not(#game) { transition: color 10ms; } -#score.hidden { - display: none; +#score.computer_controlled { + pointer-events: none; } #score span { @@ -110,7 +110,7 @@ body:not(.has-alert-open) #popup { } #popup:before { - z-index: 10; + z-index: 4; content: ""; background: #000000e6; display: block; @@ -119,7 +119,7 @@ body:not(.has-alert-open) #popup { } #popup > div { - z-index: 11; + z-index: 5; transform-origin: center; flex-direction: column; align-items: stretch; @@ -207,7 +207,7 @@ body:not(.has-alert-open) #popup { #popup button#close-modale { color: #fff; cursor: pointer; - z-index: 12; + z-index: 6; background: none; border: none; width: 60px; @@ -256,41 +256,6 @@ body:not(.has-alert-open) #popup { } } -.progress { - color: #fff; - text-align: center; - background: #1c1c2f; - border-radius: 5px; - padding: 5px 10px; - display: block; - position: relative; - overflow: hidden; - box-shadow: inset 3px 3px 5px #00000080; -} - -.progress > .progress_bar_part { - transform-origin: 0 0; - z-index: 1; - background: #4049ca; - animation: 1s ease-out both grow; - display: block; - position: absolute; - inset: 0; - box-shadow: inset 3px 3px 5px #00000080; -} - -.progress > span { - z-index: 2; - display: block; - position: relative; -} - -@keyframes grow { - 0% { - transform: scale(0, 1); - } -} - #level-recording-container { text-align: center; max-width: 400px; @@ -430,7 +395,7 @@ h2.histogram-title strong { #tooltip { color: #fff; - z-index: 11; + z-index: 5; pointer-events: none; user-select: none; opacity: 1; @@ -495,29 +460,105 @@ h2.histogram-title strong { .toast { opacity: .8; pointer-events: none; + z-index: 7; background: #000; border: 1px solid #fff; border-radius: 2px; align-items: center; gap: 10px; padding-right: 10px; - animation: forwards toast; + transition: opacity .2s, transform .2s; display: flex; position: fixed; top: 40px; left: 0; } -@keyframes toast { - 0%, 100% { - opacity: 0; - transform: translate(-20px, -20px)scale(.5); - } +.toast.hidden { + opacity: 0; + transform: translate(-20px, -20px)scale(.5); +} - 10%, 90% { - opacity: .8; - transform: none; - } +.toast.visible { + opacity: .8; + transform: none; +} + +.gridEdit > div > span, .palette > span { + cursor: pointer; + border: 1px solid; + justify-content: center; + align-items: center; + width: 40px; + height: 40px; + display: inline-flex; +} + +.gridEdit > div > span:hover, .palette > span:hover { + z-index: 1; + border-color: gold; + position: relative; + box-shadow: inset 2px 2px 4px #0003; +} + +.gridEdit > div { + display: flex; +} + +.gridEdit > div > span { + width: calc(min(500px, 100vw, 100vh - 200px) / var(--grid-size)); + height: calc(min(500px, 100vw, 100vh - 200px) / var(--grid-size)); +} + +.palette { + flex-wrap: wrap; + display: flex; +} + +.palette > span[data-selected="true"] { + border: 2px solid #fff; +} + +#stats { + color: #fff; + z-index: 3; + pointer-events: none; + opacity: 1; + width: 100vw; + max-width: 400px; + position: fixed; + top: 40px; + left: 0; +} + +#stats > div { + background: #26262680; + position: relative; +} + +#stats > div > div { + transform-origin: 0 0; + background: #6262ea; + position: absolute; + inset: 0; +} + +#stats > div > strong { + padding: 0 5px; + position: relative; +} + +.highlight { + position: relative; +} + +.highlight:before { + content: ""; + mix-blend-mode: screen; + opacity: .3; + background: linear-gradient(-45deg, #6262ea, #0000); + position: absolute; + inset: 0; } @@ -525,6 +566,7 @@ h2.histogram-title strong { +