Initial commit

I cleared the project history to start fresh on the public version
This commit is contained in:
Renan LE CARO 2025-02-15 19:21:00 +01:00
commit d2cfce2a0e
34 changed files with 11578 additions and 0 deletions

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
node_modules

84
Readme.md Normal file
View file

@ -0,0 +1,84 @@
# Breakout 71
A simple, single player, challenging arcade breakout game.
The goal is to break all the bricks of 7 levels, which catching as many coins as you can.
You have only one life, if you lose your ball you'll go back to the start
At the end of each level, you get to select an upgrade.
[Play now](https://breakout.lecaro.me/) -
[Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout) -
[itch.io](https://renanlecaro.itch.io/breakout71) -
[GitLab](https://gitlab.com/lecarore/breakout71)
## TODO
- show total score on end screen (score added to total)
- show stats on end screen compared to other runs
- handle back bouton in menu
- more levels : famous simple games, letters, fruits, animals
- perk : elastic between balls
- perk : wrap left / right
- perk : twice as many coins after a wall bounce, twice as little otherwise
- perk : fusion reactor (gather coins in one spot to triple their value)
- perk : missing makes you loose all score of level, but otherwise multiplier goes up after each breaking
- perk : n/10 of the broken bricks respawn when the ball comes back
- perk : bricks take twice as many hits but drop 50% more coins
- perk : wind (puck positions adds force to coins and balls)
## maybe
- Make a small mp4 of game which can be shown on gameover and shared. https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder
- perk : soft reset, cut combo in half instead of zero
- perk : missile goes when you catch coin
- perk : missile goes when you break a brick
- when game resumes near bottom, be unvulnerable for .5s ? , once per level
- accelerometer controls coins and balls
- mouvement relatif du puck
- balls should collide with each other
- randomize coins gravity a bit, to make fall more appealing
- apply global curve / brightness to canvas when things blow, or just always to make neon effect better
- perk: bricks attract coins
- perk : puck bounce +1 combo, hit nothing resets
- manifest for PWA (android and apple)
- publish on fdroid
- nerf the hot start a bit
- brick parts fly around with trailing effect ?
- trailing white lines behind ball
- some 3d ish effect ?
- shrink brick at beaking time ?
- perk : multiple hits on the same brick (show remaining resistance as number)
- particle effect around ball when loosing some combo (looks bad)
- Make bricks shadow the light ? using a "fill path" in screen mode, with a gradient background...would get very laggy, maybe just for the ball
- keyboard support
- perk : bricks attract ball
- perk : replay last level (remove score, restores lives if any, and rebuild )
- perk: breaking bricks stains neighbours
- perk: extra kick after bouncing on puck
- perk: transparent coins
- perk: coins of different colors repulse
- 2x coins when ball goes downward ?
- engine: Offline mode web for iphone
- engine: webgl rendering (not with sdf though, that's super slow)
## Credits
I pulled many background patterns from https://pattern.monster/
They are displayed in [patterns.html](patterns.html) for easy inclusion.
Some of the sound generating code was written by ChatGPT, and heavily
adapted to my usage over time. Some of the pixel art is taken from google
image search. I hope to replace it by my own over time.
[Heart](https://www.youtube.com/watch?v=gdWiTfzXb1g)
[Sonic](https://www.deviantart.com/graystripe2000/art/Pixel-art-16x16-Sonic-936384096)
[Finn](https://at.pinterest.com/pin/finn-the-human-pixel-art--140806230775275/)
[Mushroom](https://pixelartmaker.com/art/cce4295a92035ea)
## APK version
The web app is around 50kb, compressed down to 10kb with gzip
I wanted an APK to start in fullscreen and be able to list it on fdroid and the play store.
I stated with an empty view and went to work trimming it down, with the help of that tutorial
https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

83
app/build.gradle.kts Normal file
View file

@ -0,0 +1,83 @@
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.ZoneId
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
}
android {
namespace = "me.lecaro.breakout"
compileSdk = 34
defaultConfig {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
// versionCode = 7
// versionName = "7.0"
// Get the current Unix timestamp in seconds
versionCode = (System.currentTimeMillis() / 1000/60).toInt()
// Get the current date as a string
versionName = ZonedDateTime.now(ZoneId.of("CET"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
// compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
//dependencies {
//
// implementation(libs.androidx.core.ktx)
// implementation(libs.androidx.lifecycle.runtime.ktx)
// implementation(libs.androidx.activity.compose)
// implementation(platform(libs.androidx.compose.bom))
// implementation(libs.androidx.ui)
// implementation(libs.androidx.ui.graphics)
// implementation(libs.androidx.ui.tooling.preview)
// implementation(libs.androidx.material3)
// testImplementation(libs.junit)
// androidTestImplementation(libs.androidx.junit)
// androidTestImplementation(libs.androidx.espresso.core)
// androidTestImplementation(platform(libs.androidx.compose.bom))
// androidTestImplementation(libs.androidx.ui.test.junit4)
// debugImplementation(libs.androidx.ui.tooling)
// debugImplementation(libs.androidx.ui.test.manifest)
//}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding ="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/icon"
android:roundIcon="@drawable/icon"
android:label="@string/app_name"
android:supportsRtl="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true"
android:label="Breakout 71">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

2200
app/src/main/assets/game.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 16.933333 16.933333"
version="1.1"
id="svg5"
xml:space="preserve"
sodipodi:docname="icon.svg"
inkscape:export-filename="icon.png"
inkscape:export-xdpi="768"
inkscape:export-ydpi="768"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview212"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="5.84375"
inkscape:cx="30.802139"
inkscape:cy="32.085561"
inkscape:window-width="1920"
inkscape:window-height="1080"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
id="layer1"><rect
style="fill:#030b1f;fill-opacity:1;stroke-width:0.819666;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect234"
width="16.933332"
height="16.933332"
x="0"
y="0" /><rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.730758;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect1706"
width="4.2333331"
height="2.1166666"
x="2.5952761"
y="14.816667" /><path
style="fill:#ffffff;fill-opacity:1;stroke-width:0.840585;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760"
d="M 8.6672224,9.1329346 A 1.0583333,1.0583333 0 0 1 7.6113975,10.191265 1.0583333,1.0583333 0 0 1 6.5505677,9.1379514 1.0583333,1.0583333 0 0 1 7.6013639,8.074628 1.0583333,1.0583333 0 0 1 8.6671748,9.122901" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2"
d="M 7.0868055,6.1665268 A 0.34226292,0.34226292 0 0 1 6.7453538,6.5087888 0.34226292,0.34226292 0 0 1 6.4022835,6.1681492 0.34226292,0.34226292 0 0 1 6.7421089,5.8242725 0.34226292,0.34226292 0 0 1 7.0867901,6.1632819" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-7"
d="M 9.9609095,4.6433425 A 0.34226292,0.34226292 0 0 1 9.6194578,4.9856045 0.34226292,0.34226292 0 0 1 9.2763875,4.6449649 0.34226292,0.34226292 0 0 1 9.616213,4.3010882 0.34226292,0.34226292 0 0 1 9.9608942,4.6400976" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-0"
d="M 9.4754292,6.0612068 A 0.34226292,0.34226292 0 0 1 9.1339775,6.4034688 0.34226292,0.34226292 0 0 1 8.7909072,6.0628293 0.34226292,0.34226292 0 0 1 9.1307327,5.7189525 0.34226292,0.34226292 0 0 1 9.4754139,6.057962" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-9"
d="M 8.7935426,7.682621 A 0.34226292,0.34226292 0 0 1 8.4520909,8.024883 0.34226292,0.34226292 0 0 1 8.1090206,7.6842434 0.34226292,0.34226292 0 0 1 8.448846,7.3403667 0.34226292,0.34226292 0 0 1 8.7935272,7.6793762" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-3"
d="M 10.550572,7.5443454 A 0.34226292,0.34226292 0 0 1 10.20912,7.8866073 0.34226292,0.34226292 0 0 1 9.8660501,7.5459678 0.34226292,0.34226292 0 0 1 10.205876,7.2020911 0.34226292,0.34226292 0 0 1 10.550557,7.5411005" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-6"
d="m 11.171486,10.027683 a 0.34226292,0.34226292 0 0 1 -0.341452,0.342262 0.34226292,0.34226292 0 0 1 -0.34307,-0.340639 0.34226292,0.34226292 0 0 1 0.339825,-0.343877 0.34226292,0.34226292 0 0 1 0.344681,0.339009" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-06"
d="M 9.6700341,9.248889 A 0.34226292,0.34226292 0 0 1 9.3285824,9.5911509 0.34226292,0.34226292 0 0 1 8.9855121,9.2505114 0.34226292,0.34226292 0 0 1 9.3253375,8.9066347 0.34226292,0.34226292 0 0 1 9.6700187,9.2456441" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-2"
d="m 9.8461224,10.381763 a 0.34226292,0.34226292 0 0 1 -0.3414517,0.342262 0.34226292,0.34226292 0 0 1 -0.3430703,-0.340639 0.34226292,0.34226292 0 0 1 0.3398255,-0.343877 0.34226292,0.34226292 0 0 1 0.3446812,0.33901" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-61"
d="m 10.677446,12.346528 a 0.34226292,0.34226292 0 0 1 -0.341452,0.342262 0.34226292,0.34226292 0 0 1 -0.3430699,-0.34064 0.34226292,0.34226292 0 0 1 0.3398249,-0.343876 0.34226292,0.34226292 0 0 1 0.344682,0.339009" /><path
style="fill:#8953e5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8"
d="m 12.174136,14.576557 a 0.34226292,0.34226292 0 0 1 -0.341452,0.342262 0.34226292,0.34226292 0 0 1 -0.34307,-0.340639 0.34226292,0.34226292 0 0 1 0.339825,-0.343877 0.34226292,0.34226292 0 0 1 0.344681,0.339009" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7"
d="m 12.155115,8.4203005 a 0.34226292,0.34226292 0 0 1 -0.341452,0.3422619 0.34226292,0.34226292 0 0 1 -0.34307,-0.3406395 0.34226292,0.34226292 0 0 1 0.339825,-0.3438767 0.34226292,0.34226292 0 0 1 0.344681,0.3390094" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7-9"
d="M 13.807412,9.5352793 A 0.34226292,0.34226292 0 0 1 13.46596,9.8775412 0.34226292,0.34226292 0 0 1 13.12289,9.5369017 0.34226292,0.34226292 0 0 1 13.462715,9.193025 0.34226292,0.34226292 0 0 1 13.807396,9.5320344" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7-2"
d="M 10.550572,8.4820604 A 0.34226292,0.34226292 0 0 1 10.20912,8.8243224 0.34226292,0.34226292 0 0 1 9.8660501,8.4836829 0.34226292,0.34226292 0 0 1 10.205876,8.1398062 0.34226292,0.34226292 0 0 1 10.550557,8.4788156" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7-0"
d="m 11.829275,6.7558122 a 0.34226292,0.34226292 0 0 1 -0.341452,0.3422619 0.34226292,0.34226292 0 0 1 -0.34307,-0.3406395 0.34226292,0.34226292 0 0 1 0.339825,-0.3438767 0.34226292,0.34226292 0 0 1 0.344681,0.3390094" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7-23"
d="m 10.76369,5.8607206 a 0.34226292,0.34226292 0 0 1 -0.341452,0.342262 0.34226292,0.34226292 0 0 1 -0.34307,-0.3406395 0.34226292,0.34226292 0 0 1 0.339825,-0.3438767 0.34226292,0.34226292 0 0 1 0.344681,0.3390094" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7-7"
d="M 8.3128425,4.9443169 A 0.34226292,0.34226292 0 0 1 7.9713908,5.2865788 0.34226292,0.34226292 0 0 1 7.6283205,4.9459393 0.34226292,0.34226292 0 0 1 7.968146,4.6020626 0.34226292,0.34226292 0 0 1 8.3128272,4.941072" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7-5"
d="M 8.5685832,6.3082666 A 0.34226292,0.34226292 0 0 1 8.2271315,6.6505286 0.34226292,0.34226292 0 0 1 7.8840612,6.3098891 0.34226292,0.34226292 0 0 1 8.2238866,5.9660124 0.34226292,0.34226292 0 0 1 8.5685678,6.3050218" /><path
style="fill:#4aaae5;fill-opacity:1;stroke-width:0.271844;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="path1760-2-8-7-92"
d="M 9.7620389,7.0115528 A 0.34226292,0.34226292 0 0 1 9.4205872,7.3538148 0.34226292,0.34226292 0 0 1 9.0775169,7.0131753 0.34226292,0.34226292 0 0 1 9.4173423,6.6692985 0.34226292,0.34226292 0 0 1 9.7620235,7.008308" /><rect
style="fill:#4aaae5;fill-opacity:1;stroke-width:1.46181;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect1762"
width="4.2333331"
height="4.2333331"
x="0"
y="-6.9388939e-18" /><rect
style="fill:#8953e5;fill-opacity:1;stroke-width:1.46181;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect1762-3"
width="4.2333331"
height="4.2333331"
x="4.2333331"
y="0" /><rect
style="fill:#4aaae5;fill-opacity:1;stroke-width:1.46181;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect1762-5"
width="4.2333331"
height="4.2333331"
x="8.4666662"
y="3.469447e-18" /><rect
style="fill:#8953e5;fill-opacity:1;stroke-width:1.46181;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect1762-3-3"
width="4.2333331"
height="4.2333331"
x="12.699999"
y="3.469447e-18" /><rect
style="fill:#4aaae5;fill-opacity:1;stroke-width:1.46181;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect1762-56"
width="4.2333331"
height="4.2333331"
x="-16.933332"
y="4.2333331"
transform="scale(-1,1)" /><rect
style="fill:#8953e5;fill-opacity:1;stroke-width:1.46181;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill;stop-color:#000000"
id="rect1762-3-3-1"
width="4.2333331"
height="4.2333331"
x="-4.2333331"
y="4.2333331"
transform="scale(-1,1)" /></g></svg>

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Breakout 𝟕𝟏</title>
<link rel="stylesheet" href="style.css?v=5388" />
<link rel="icon" href="./icon.svg" />
</head>
<body>
<button id="menu"><span> menu</span></button>
<button id="score"></button>
<canvas id="game"></canvas>
<script src="levels.js?v=5388"></script>
<script src="game.js?v=5388"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Breakout privacy policy</title>
<link rel="icon" href="./icon.svg" />
<style type="text/css">
body {
max-width: 800px;
margin: 40px auto;
font-family: sans-serif;
}
</style>
</head>
<body>
<h1>Privacy policy</h1>
<p>
<a href="https://breakout.lecaro.me">Breakout 71</a> is published
by Renan LE CARO, a French citizen and programmer. You can contact me at
this adress : breakout71@lecaro.me
</p>
<p>
If you access breakout.lecaro.me though a web browser, your IP address
will be logged on my server to prevent abuses.
<a href="https://staging.lecaro.me/">My server</a> is hosted by Hetzner
Online GmbH in germany.
</p>
<p>
If you install the app through google play, no information will
be collected at all by me.
</p>
</body>
</html>

View file

@ -0,0 +1,227 @@
* {
font-family:
Courier New,
Courier,
Lucida Sans Typewriter,
Lucida Typewriter,
monospace;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100vw;
height: 100vh;
color: white;
background-size: 120px 120px;
background-color: var(--background1);
--background1: #030c23;
--background2: #03112a;
}
#game {
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
}
#score,
#menu {
position: absolute;
top: 0;
z-index: 1;
padding: 10px;
appearance: none;
background: none;
border: none;
font: inherit;
color: white;
min-width: 40px;
min-height: 40px;
line-height: 20px;
}
body.black_puck #score,
body.black_puck #menu {
color:black;
}
#score:hover,
#score:focus,
#menu:hover,
#menu:focus {
background: rgba(0,0,0,0.3);
cursor: pointer;
}
#score {
right: 0;
}
#menu {
left: 0;
}
@media screen and (orientation: portrait) {
#menu > span {
display: none;
}
}
.popup {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.95);
z-index: 10;
display: flex;
overflow: auto;
}
.popup > div {
margin: auto;
padding: 20px;
/*border: 1px solid white;*/
transform-origin: center;
display: flex;
flex-direction: column;
align-items: stretch;
width: 90%;
max-width: 450px;
}
.popup > div > * {
padding: 0;
margin: 0;
}
.popup > div > h2,
.popup > div > p {
margin-bottom: 20px;
}
.popup > div > button {
font:inherit;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px;
cursor: pointer;
border: 1px solid white;
text-align: left;
display: flex;
gap: 10px;
margin-top: -1px;
}
.popup > div > button:not([disabled]):hover,
.popup > div > button:not([disabled]):focus {
border-color: #f1d33b;
position: relative;
z-index: 1;
}
.popup button.close-modale {
color:white;
position: absolute;
top:0;
right:0;
width: 60px;
height: 60px;
background:none;
border: none;
cursor: pointer;
background: rgba(0,0,0,0.2);
overflow: hidden;
}
.popup button.close-modale:before {
content: "+";
position: absolute;
transform: translate(-50%, -50%) rotate(45deg) ;
font-size: 80px;
display: inline-block;
}
.popup button.close-modale:hover {
font-weight: bold;
}
.popup > div > button[disabled] {
/*border: 1px solid #666;*/
opacity: 0.5;
}
.popup > div > button > div {
flex-grow: 1;
}
.popup > div > button > div > em {
display: block;
opacity: 0.8;
}
.popup > div > button > span.checks {
width: 40px;
height: 40px;
display: inline-flex;
gap:5px;
flex-grow: 0;
flex-shrink: 0;
}
.popup > div > button > span.checks>span {
flex-basis: 10px;
flex-grow: 1;
flex-shrink: 1;
/*border: 1px solid white;*/
background: white;
opacity: 0.1;
border-radius: 4px;
align-self: stretch;
}
.popup > div > button > span.checks>span.checked {
opacity: 1;
}
.popup .textAfterButtons{
color: rgba(255, 255, 255, 0.58);
}
.popup a[href]{
color:inherit;
}
.popup a[href]:hover,
.popup a[href]:focus
{
color:white;
}
/*Unlocks progress bar*/
.progress {
display: block;
padding: 5px 10px;
background: #1c1c2f;
color:#fff;
box-shadow: inset 3px 3px 5px rgba(0,0,0,0.5);
border-radius: 5px;
text-align: center;
position: relative;
overflow: hidden;
}
.progress >.progress_bar_part{
display: block;
background: #4049ca;
box-shadow: inset 3px 3px 5px rgba(0,0,0,0.5);
left: 0;
position: absolute;
right: 0;
top: 0;
bottom: 0;
transform-origin: top left;
animation: grow 1s both ease-out;
z-index: 1;
}
.progress> span {
display: block;
position: relative;
z-index: 2;
}
@keyframes grow {
0%{
transform: scale(0,1);
}
}

View file

@ -0,0 +1,33 @@
package me.lecaro.breakout
import android.os.Bundle
import android.util.Log
import android.view.Window
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
import android.webkit.WebView
class MainActivity : android.app.Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE);
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
// WebView.setWebContentsDebuggingEnabled(true)
val webView = WebView(this)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.loadUrl("file:///android_asset/index.html")
webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d(
"WebView", "${consoleMessage.message()} -- From line " +
"${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}"
)
return true
}
}
setContentView(webView)
}
}

View file

@ -0,0 +1,195 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="16.933"
android:viewportHeight="16.933">
<path
android:pathData="M0,0h16.933v16.933h-16.933z"
android:strokeLineJoin="round"
android:strokeWidth="0.819666"
android:fillColor="#030b1f"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M0,14.817h4.233v2.117h-4.233z"
android:strokeLineJoin="round"
android:strokeWidth="0.730758"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M8.667,9.133A1.058,1.058 0,0 1,7.611 10.191,1.058 1.058,0 0,1 6.551,9.138 1.058,1.058 0,0 1,7.601 8.075,1.058 1.058,0 0,1 8.667,9.123"
android:strokeLineJoin="round"
android:strokeWidth="0.840585"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M7.087,6.167A0.342,0.342 0,0 1,6.745 6.509,0.342 0.342,0 0,1 6.402,6.168 0.342,0.342 0,0 1,6.742 5.824,0.342 0.342,0 0,1 7.087,6.163"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M9.961,4.643A0.342,0.342 0,0 1,9.619 4.986,0.342 0.342,0 0,1 9.276,4.645 0.342,0.342 0,0 1,9.616 4.301,0.342 0.342,0 0,1 9.961,4.64"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M9.475,6.061A0.342,0.342 0,0 1,9.134 6.403,0.342 0.342,0 0,1 8.791,6.063 0.342,0.342 0,0 1,9.131 5.719,0.342 0.342,0 0,1 9.475,6.058"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M8.794,7.683A0.342,0.342 0,0 1,8.452 8.025,0.342 0.342,0 0,1 8.109,7.684 0.342,0.342 0,0 1,8.449 7.34,0.342 0.342,0 0,1 8.794,7.679"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M10.551,7.544A0.342,0.342 0,0 1,10.209 7.887,0.342 0.342,0 0,1 9.866,7.546 0.342,0.342 0,0 1,10.206 7.202,0.342 0.342,0 0,1 10.551,7.541"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m11.171,10.028a0.342,0.342 0,0 1,-0.341 0.342,0.342 0.342,0 0,1 -0.343,-0.341 0.342,0.342 0,0 1,0.34 -0.344,0.342 0.342,0 0,1 0.345,0.339"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M9.67,9.249A0.342,0.342 0,0 1,9.329 9.591,0.342 0.342,0 0,1 8.986,9.251 0.342,0.342 0,0 1,9.325 8.907,0.342 0.342,0 0,1 9.67,9.246"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m9.846,10.382a0.342,0.342 0,0 1,-0.341 0.342,0.342 0.342,0 0,1 -0.343,-0.341 0.342,0.342 0,0 1,0.34 -0.344,0.342 0.342,0 0,1 0.345,0.339"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m10.677,12.347a0.342,0.342 0,0 1,-0.341 0.342,0.342 0.342,0 0,1 -0.343,-0.341 0.342,0.342 0,0 1,0.34 -0.344,0.342 0.342,0 0,1 0.345,0.339"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.174,14.577a0.342,0.342 0,0 1,-0.341 0.342,0.342 0.342,0 0,1 -0.343,-0.341 0.342,0.342 0,0 1,0.34 -0.344,0.342 0.342,0 0,1 0.345,0.339"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m12.155,8.42a0.342,0.342 0,0 1,-0.341 0.342,0.342 0.342,0 0,1 -0.343,-0.341 0.342,0.342 0,0 1,0.34 -0.344,0.342 0.342,0 0,1 0.345,0.339"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M13.807,9.535A0.342,0.342 0,0 1,13.466 9.878,0.342 0.342,0 0,1 13.123,9.537 0.342,0.342 0,0 1,13.463 9.193,0.342 0.342,0 0,1 13.807,9.532"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M10.551,8.482A0.342,0.342 0,0 1,10.209 8.824,0.342 0.342,0 0,1 9.866,8.484 0.342,0.342 0,0 1,10.206 8.14,0.342 0.342,0 0,1 10.551,8.479"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m11.829,6.756a0.342,0.342 0,0 1,-0.341 0.342,0.342 0.342,0 0,1 -0.343,-0.341 0.342,0.342 0,0 1,0.34 -0.344,0.342 0.342,0 0,1 0.345,0.339"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="m10.764,5.861a0.342,0.342 0,0 1,-0.341 0.342,0.342 0.342,0 0,1 -0.343,-0.341 0.342,0.342 0,0 1,0.34 -0.344,0.342 0.342,0 0,1 0.345,0.339"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M8.313,4.944A0.342,0.342 0,0 1,7.971 5.287,0.342 0.342,0 0,1 7.628,4.946 0.342,0.342 0,0 1,7.968 4.602,0.342 0.342,0 0,1 8.313,4.941"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M8.569,6.308A0.342,0.342 0,0 1,8.227 6.651,0.342 0.342,0 0,1 7.884,6.31 0.342,0.342 0,0 1,8.224 5.966,0.342 0.342,0 0,1 8.569,6.305"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M9.762,7.012A0.342,0.342 0,0 1,9.421 7.354,0.342 0.342,0 0,1 9.078,7.013 0.342,0.342 0,0 1,9.417 6.669,0.342 0.342,0 0,1 9.762,7.008"
android:strokeLineJoin="round"
android:strokeWidth="0.271844"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M0,-0h4.233v4.233h-4.233z"
android:strokeLineJoin="round"
android:strokeWidth="1.46181"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M4.233,0h4.233v4.233h-4.233z"
android:strokeLineJoin="round"
android:strokeWidth="1.46181"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M8.467,0h4.233v4.233h-4.233z"
android:strokeLineJoin="round"
android:strokeWidth="1.46181"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M12.7,0h4.233v4.233h-4.233z"
android:strokeLineJoin="round"
android:strokeWidth="1.46181"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M16.933,4.233l-4.233,0l-0,4.233l4.233,0z"
android:strokeLineJoin="round"
android:strokeWidth="1.46181"
android:fillColor="#4aaae5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M4.233,4.233l-4.233,0l-0,4.233l4.233,0z"
android:strokeLineJoin="round"
android:strokeWidth="1.46181"
android:fillColor="#8953e5"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Breakout</string>
</resources>

View file

@ -0,0 +1,13 @@
<?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>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View file

@ -0,0 +1,19 @@
<?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>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

5
build.gradle.kts Normal file
View file

@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
}

BIN
cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

19
deploy.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
#Randomly shuffle the version ids
# Generate a random number between 1000 and 9999
random_number=$(shuf -i 1000-9999 -n 1)
# Use sed to replace the pattern with the random number
sed -i "s/\?v=[0-9]*/\?v=$random_number/g" ./app/src/main/assets/index.html
DOMAIN="breakout.lecaro.me"
PUBLIC_CONTENT="./app/src/main/assets/"
ssh staging "mkdir -p /opt/mup-nginx-proxy/config/html/static_sites/$DOMAIN"
rsync -avz --delete --delete-excluded --exclude="*.sh" --exclude="node_modules" --exclude="android" --exclude=".*" $PUBLIC_CONTENT staging:/opt/mup-nginx-proxy/config/html/static_sites/$DOMAIN
# generate zip for itch
rm -f breakout.zip
zip -j breakout.zip app/src/main/assets/*

29
editclient.css Normal file
View file

@ -0,0 +1,29 @@
body {
background: black;
color: white;
}
#palette button.active {
transform: scale(1.2);
}
.level-bricks-preview {
position: relative;
}
#palette {
position: fixed;
top: 0;
right: 0;
width: 80px;
bottom: 0;
overflow: auto;
}
#levels {
display: flex;
gap: 40px;
flex-wrap: wrap;
margin-right: 80px;
}

274
editclient.js Normal file
View file

@ -0,0 +1,274 @@
let currentColor = ''
const colorsList = [
'white',
'black',
'',
'#F44848',
'#ab0c0c',
'#F29E4A',
'#F0F04C',
'#A1F051',
'#53EE53',
'#59EEA3',
'#5BECEC',
'#5DA3EA',
'#6262EA',
'#A664E8',
'#E869E8',
'#E66BA8',
'#E67070',
"#333",
'#231f20',
'#e32119',
'#ffd300',
'#e1c8b4',
'#618227'
]
const palette = document.getElementById('palette');
colorsList.forEach(color => {
const btn = document.createElement('button')
Object.assign(btn.style, {
background: color,
display: 'inline-block',
width: '40px',
height: '40px',
border: '1px solid black'
})
if (color === currentColor) {
btn.className = 'active'
}
palette.appendChild(btn)
btn.addEventListener('click', (e) => {
currentColor = color
e.preventDefault()
document.querySelector('#palette button.active')?.classList.remove('active');
btn.classList.add('active')
})
})
function renderAllLevels() {
allLevels.forEach((level, levelIndex) => {
addLevelEditorToList(level, levelIndex)
})
}
function addLevelEditorToList(level, levelIndex) {
const {name, bricks, size, svg,color} = level
let div = document.createElement('div')
div.innerHTML = `
<div>
<button data-level="${levelIndex}" data-rename="yep">${name}</button>
<button data-level="${levelIndex}" data-delete="yep">Delete</button>
<button data-offset-level-size="-1" data-level="${levelIndex}">-</button>
<button data-offset-level-size="1" data-level="${levelIndex}">+</button>
<button data-offset-x="-1" data-offset-y="0" data-level="${levelIndex}">L</button>
<button data-offset-x="1" data-offset-y="0" data-level="${levelIndex}">R</button>
<button data-offset-x="0" data-offset-y="-1" data-level="${levelIndex}">U</button>
<button data-offset-x="0" data-offset-y="1" data-level="${levelIndex}">D</button>
<input type="color" value="${level.color || ''}" data-level="${levelIndex}" />
<button data-level="${levelIndex}" data-set-bg-svg="true" >${svg?'replace':'set'}</button>
<label>
<input type="checkbox" data-field="squared" ${level.squared ? 'checked':''} data-level="${levelIndex}" />
sqare
</label>
<label>
<input type="checkbox" data-field="focus" ${level.focus ? 'checked':''} data-level="${levelIndex}" />
focus
</label>
<label>
<input type="checkbox" data-field="black_puck" ${level.black_puck ? 'checked':''} data-level="${levelIndex}" />
black_puck
</label>
<label>
<input type="checkbox" data-field="draft" ${level.draft ? 'checked':''} data-level="${levelIndex}" />
draft
</label>
</div>
<div class="level-bricks-preview" id="bricks-of-${levelIndex}" >
</div>
`;
document.getElementById('levels').appendChild(div)
renderLevelBricks(levelIndex)
updateLevelBackground(levelIndex)
}
function updateLevelBackground(levelIndex){
const div=document.getElementById("bricks-of-"+levelIndex)
const {svg, color}= allLevels[levelIndex]
Object.assign(div.style, svg ?
{backgroundImage:`url('data:image/svg+xml,${encodeURIComponent(svg)}')`, backgroundColor:'transparent'} :
{backgroundImage:'none', backgroundColor:color||'#111'}
)
}
function renderLevelBricks(levelIndex) {
const {size, bricks} = allLevels[levelIndex]
const buttons = []
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
const index = y * size + x
buttons.push(`<button style="background: ${bricks[index] || 'transparent'}; left:${x * 40}px;top:${y * 40
}px;width:40px;height: 40px; position: absolute" data-set-color-of="${index}" data-level="${levelIndex}"></button>`)
}
}
const div = document.getElementById("bricks-of-" + levelIndex)
div.innerHTML = buttons.join('')
Object.assign(div.style, {
width: size * 40 + 'px',
height: size * 40 + 'px'
})
}
document.getElementById('levels').addEventListener('change', e => {
const levelIndexStr = e.target.getAttribute('data-level')
if ( levelIndexStr) {
const levelIndex = parseInt(levelIndexStr)
const level = allLevels[levelIndex]
if( e.target.getAttribute('type') === 'color'){
level.color = e.target.value
level.svg = ''
updateLevelBackground(levelIndex)
}else if( e.target.getAttribute('type') === 'checkbox' && e.target.hasAttribute('data-field')){
level[e.target.getAttribute('data-field')] = !!e.target.checked
}
save()
}
})
document.getElementById('levels').addEventListener('click', e => {
const resize = e.target.getAttribute('data-offset-level-size')
const moveX = e.target.getAttribute('data-offset-x')
const moveY = e.target.getAttribute('data-offset-y')
const levelIndexStr = e.target.getAttribute('data-level')
if (!levelIndexStr) return
if (e.target.tagName!=='BUTTON') return
const levelIndex = parseInt(levelIndexStr)
const level = allLevels[levelIndex]
const {bricks, size} = level;
if (resize) {
const newSize = size + parseInt(resize)
const newBricks = []
for (let x = 0; x < Math.min(size, newSize); x++) {
for (let y = 0; y < Math.min(size, newSize); y++) {
newBricks[y * newSize + x] = bricks[y * size + x] || ''
}
}
level.size = newSize;
level.bricks = newBricks;
} else if (moveX && moveY) {
const dx = parseInt(moveX), dy = parseInt(moveY)
const moved = []
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
moved[(y + dy) * size + (x + dx)] = bricks[y * size + x] || ''
}
}
level.bricks = moved;
} else if (e.target.getAttribute('data-rename')) {
const newName = prompt('Name ? ', level.name)
if (newName) {
level.name = newName
e.target.textContent = newName
}
}else if (e.target.getAttribute('data-delete')) {
if (confirm('Delete level')) {
allLevels=allLevels.filter((l,i)=>i!==levelIndex)
save().then(()=>window.location.reload())
}
}else if(e.target.getAttribute('data-set-bg-svg')){
const newBg = prompt('New svg code',level.svg||'')
if(newBg){
level.svg=newBg
level.color = ''
}
updateLevelBackground(levelIndex)
}
renderLevelBricks(levelIndex)
save()
}, true)
let applying = undefined
function colorPixel(e) {
if (typeof applying === 'undefined') return
const index = e.target.getAttribute('data-set-color-of')
const level = e.target.getAttribute('data-level')
if (index && level) {
const levelIndex = parseInt(level)
e.target.style.background = applying || 'transparent'
allLevels[levelIndex].bricks[index] = applying
}
}
document.getElementById('levels').addEventListener('mousedown', e => {
const index = e.target.getAttribute('data-set-color-of')
const level = e.target.getAttribute('data-level')
if (index && level) {
const before = allLevels[parseInt(level)].bricks[parseInt(index)] || ''
applying = before === currentColor ? '' : currentColor
colorPixel(e)
}
})
document.getElementById('levels').addEventListener('mouseenter', e => {
if (typeof applying !== undefined) {
colorPixel(e)
}
}, true)
document.addEventListener('mouseup', e => {
applying = undefined
save()
})
document.getElementById('new-level').addEventListener('click', e => {
const name = prompt("Name ? ")
if (!name) return;
allLevels.push({
name,
size: 8,
bricks: ['white'],
svg: '',
})
const levelIndex = allLevels.length - 1
addLevelEditorToList(allLevels[levelIndex], levelIndex)
save()
}, true)
renderAllLevels()
function save() {
return fetch('/', {
method: 'POST', headers: {
'Content-Type': 'text/plain'
},
body: 'let allLevels=' + JSON.stringify(allLevels, null, 2)
})
}

50
editserver.js Normal file
View file

@ -0,0 +1,50 @@
const express = require('express')
const bodyParser = require('body-parser');
const fs = require('fs')
const app = express()
const port = 4400
const srcPath = 'app/src/main/assets/levels.js'
app.use(bodyParser.text({
type: 'text/plain',
limit:'1MB'
}));
app.get('/', (req, res) => {
res.end(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Level editor</title>
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎨</text></svg>"
/>
</head>
<body>
<div id="levels"></div>
<div id="palette">
<button id="new-level">new</button>
</div>
<style>
${fs.readFileSync('./editclient.css').toString()}
</style>
<script>${fs.readFileSync(srcPath).toString()}</script>
<script>${fs.readFileSync('./editclient.js').toString()}</script>
</body>
`)
})
app.post('/', (req, res) => {
console.log(req.body)
if(req.body?.trim()) {
fs.writeFileSync(srcPath, req.body)
}
res.end('OK')
})
app.listen(port, () => {
console.log(`Example app listening on port http://localhost:${port}`)
})

23
gradle.properties Normal file
View file

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

31
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,31 @@
[versions]
agp = "8.3.1"
kotlin = "1.9.0"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.7.0"
composeBom = "2023.08.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Sun Jan 05 13:08:47 CET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

1079
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "breakout.lecaro.me",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon editserver.js --watch editserver.js "
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.21.2",
"nodemon": "^3.1.9"
}
}

163
patterns.html Normal file

File diff suppressed because one or more lines are too long

24
settings.gradle.kts Normal file
View file

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Breakout"
include(":app")