Automatic deploy 29000794

This commit is contained in:
Renan LE CARO 2025-02-20 11:34:11 +01:00
parent 80fa8e6329
commit 54512644ab
10 changed files with 876 additions and 730 deletions

View file

@ -27,7 +27,8 @@ There's also an easy mode for kids (slower ball) and a color-blind mode (no colo
## Doing ## Doing
- publish on Fdroid - publish on Fdroid
- enable gif export of gameplay capture
- enable export of gameplay capture in webview
## Perk ideas ## Perk ideas
- wrap left / right - wrap left / right
- n% of the broken bricks respawn when the ball touches the puck - n% of the broken bricks respawn when the ball touches the puck
@ -67,7 +68,6 @@ There's also an easy mode for kids (slower ball) and a color-blind mode (no colo
- show total score on end screen (score added to total) - show total score on end screen (score added to total)
- show stats on end screen compared to other runs - show stats on end screen compared to other runs
- handle back bouton in menu - handle back bouton in menu
- Make a small mp4/gif of game which can be shown on gameover and shared. https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder
- mouvement relatif du puck - mouvement relatif du puck
- balls should collide with each other - balls should collide with each other
- when game resumes near bottom, be unvulnerable for .5s ? , once per level - when game resumes near bottom, be unvulnerable for .5s ? , once per level

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 28999986 versionCode = 29000794
versionName = "28999986" versionName = "29000794"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View file

@ -107,6 +107,7 @@ function play() {
if (audioContext) { if (audioContext) {
audioContext.resume() audioContext.resume()
} }
resumeRecording()
} }
function pause() { function pause() {
@ -119,6 +120,7 @@ function pause() {
audioContext.suspend() audioContext.suspend()
}, 1000) }, 1000)
} }
pauseRecording()
} }
let offsetX, offsetXRoundedDown, gameZoneWidth, gameZoneWidthRoundedUp, gameZoneHeight, brickWidth, needsRender = true; let offsetX, offsetXRoundedDown, gameZoneWidth, gameZoneWidthRoundedUp, gameZoneHeight, brickWidth, needsRender = true;
@ -353,7 +355,8 @@ async function openUpgradesPicker() {
while (repeats--) { while (repeats--) {
const actions = pickRandomUpgrades(choices); const actions = pickRandomUpgrades(choices);
if (!actions.length) break if (!actions.length) break
let textAfterButtons = `<p>Upgrades picked so far : </p><p>${pickedUpgradesHTMl()}</p>`; let textAfterButtons = `<p>Upgrades picked so far : </p><p>${pickedUpgradesHTMl()}</p>
<div id="level-recording-container"></div>`;
const cb = await asyncAlert({ const cb = await asyncAlert({
title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text, allowClose: false, title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text, allowClose: false,
@ -394,7 +397,8 @@ function setLevel(l) {
flashes = []; flashes = [];
background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg) background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
stopRecording()
startRecordingGame()
} }
function currentLevelInfo() { function currentLevelInfo() {
@ -770,6 +774,8 @@ function restart() {
setLevel(0); setLevel(0);
scoreStory.push(`You started playing with the upgrade "${upgrades.find(u => u.id === randomGift)?.name}" on the level "${runLevels[0].name}". `,); scoreStory.push(`You started playing with the upgrade "${upgrades.find(u => u.id === randomGift)?.name}" on the level "${runLevels[0].name}". `,);
pauseRecording()
} }
function setMousePos(x) { function setMousePos(x) {
@ -1041,7 +1047,7 @@ function tick() {
const windD = (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * 2 * perks.wind const windD = (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * 2 * perks.wind
for (var i = 0; i < perks.wind; i++) { for (var i = 0; i < perks.wind; i++) {
if(Math.random()*Math.abs(windD)>0.5) { if (Math.random() * Math.abs(windD) > 0.5) {
flashes.push({ flashes.push({
type: "particle", type: "particle",
duration: 150, duration: 150,
@ -1049,9 +1055,9 @@ function tick() {
time: levelTime, time: levelTime,
size: coinSize / 2, size: coinSize / 2,
color: rainbowColor(), color: rainbowColor(),
x: offsetXRoundedDown+ Math.random() * gameZoneWidthRoundedUp , x: offsetXRoundedDown + Math.random() * gameZoneWidthRoundedUp,
y: Math.random() * gameZoneHeight, y: Math.random() * gameZoneHeight,
vx: windD*8, vx: windD * 8,
vy: 0, vy: 0,
}); });
} }
@ -1298,6 +1304,7 @@ function addToTotalScore(points) {
function gameOver(title, intro) { function gameOver(title, intro) {
if (!running) return; if (!running) return;
pause() pause()
stopRecording()
runStatistics.ended = Date.now() runStatistics.ended = Date.now()
@ -1371,7 +1378,7 @@ function gameOver(title, intro) {
<p>${intro}</p> <p>${intro}</p>
${unlocksInfo} ${unlocksInfo}
`, textAfterButtons: ` `, textAfterButtons: `
<div id="level-recording-container"></div>
${scoreStory.map((t) => "<p>" + t + "</p>").join("")} ${scoreStory.map((t) => "<p>" + t + "</p>").join("")}
` `
}).then(() => restart()); }).then(() => restart());
@ -1737,8 +1744,9 @@ function render() {
ctx.fillStyle = puckColor; ctx.fillStyle = puckColor;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
if (offsetXRoundedDown) { if (offsetXRoundedDown) {
ctx.fillRect(offsetX, 0, 1, height); // draw outside of gaming area to avoid capturing borders in recordings
ctx.fillRect(width - offsetX - 1, 0, 1, height); ctx.fillRect(offsetX - 1, 0, 1, height);
ctx.fillRect(width - offsetX + 1, 0, 1, height);
} }
if (isSettingOn("mobile-mode")) { if (isSettingOn("mobile-mode")) {
ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1); ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1);
@ -1752,6 +1760,8 @@ function render() {
if (shaked) { if (shaked) {
ctx.resetTransform(); ctx.resetTransform();
} }
recordOneFrame()
} }
let cachedBricksRender = document.createElement("canvas"); let cachedBricksRender = document.createElement("canvas");
@ -2343,6 +2353,11 @@ const options = {
}, "color_blind": { }, "color_blind": {
default: false, name: `Color blind mode`, help: `Removes mechanics about colors.`, restart: true, default: false, name: `Color blind mode`, help: `Removes mechanics about colors.`, restart: true,
}, },
// Could not get the sharing to work without loading androidx and all the modern android things so for now i'll just disable sharing in the android app
"record": !window.location.search.includes('isInWebView=true') && {
default: false, name: `Record games`, help: `Get a video at the end of the run.`, restart: true,
},
}; };
async function openSettingsPanel() { async function openSettingsPanel() {
@ -2350,8 +2365,10 @@ async function openSettingsPanel() {
const optionsList = []; const optionsList = [];
for (const key in options) { for (const key in options) {
if (options[key])
optionsList.push({ optionsList.push({
checked: isSettingOn(key) ? 1 : 0, max: 1, text: options[key].name, help: options[key].help, value: () => { checked: isSettingOn(key) ? 1 : 0,
max: 1, text: options[key].name, help: options[key].help, value: () => {
toggleSetting(key) toggleSetting(key)
if (options[key].restart) { if (options[key].restart) {
restart() restart()
@ -2618,6 +2635,185 @@ function levelIconHTML(level, title) {
upgrades.forEach(u => u.icon = levelIconHTML(perkIconsLevels[u.id], u.name)) upgrades.forEach(u => u.icon = levelIconHTML(perkIconsLevels[u.id], u.name))
let mediaRecorder, captureStream, recordCanvas, recordCanvasCtx, levelGif, gifCanvas, gifCtx
function recordOneFrame() {
if (!isSettingOn('record')) {
return
}
if (!running) return;
drawMainCanvasOnSmallCanvas()
// Start recording after you hit something
if(levelSpawnedCoins && levelGif) {
recordGifFrame()
}
if (captureStream.requestFrame) {
captureStream.requestFrame()
} else {
captureStream.getVideoTracks()[0].requestFrame()
}
}
function drawMainCanvasOnSmallCanvas() {
recordCanvasCtx?.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height)
recordCanvasCtx.fillStyle = currentLevelInfo()?.black_puck ? '#000' : '#FFF'
recordCanvasCtx.textBaseline = "top";
recordCanvasCtx.font = "12px monospace";
recordCanvasCtx.textAlign = "right";
recordCanvasCtx.fillText(score.toString(), recordCanvas.width - 12, 12)
recordCanvasCtx.textAlign = "left";
recordCanvasCtx.fillText((currentLevel + 1) + '/' + max_levels(), 12, 12)
}
let nthFrame = 0, gifFrameReduction = 2
function recordGifFrame(){
gifCtx.globalCompositeOperation = 'screen'
gifCtx.globalAlpha = 1 / gifFrameReduction
gifCtx?.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, gifCanvas.width, gifCanvas.height)
nthFrame++
if (nthFrame === gifFrameReduction) {
levelGif.addFrame(gifCtx, {delay: Math.round(gifFrameReduction * 1000 / 60), copy: true});
gifCtx.globalCompositeOperation = 'source-over'
gifCtx.fillStyle = 'black'
gifCtx.fillRect(0, 0, gifCanvas.width, gifCanvas.height)
nthFrame=0
}
}
function startRecordingGame() {
if (!isSettingOn('record')) {
return
}
if (!recordCanvas) {
// Smaller canvas with less details
recordCanvas = document.createElement("canvas")
recordCanvasCtx = recordCanvas.getContext("2d", {antialias: false, alpha: false})
gifCanvas = document.createElement("canvas")
gifCtx = gifCanvas.getContext("2d", {antialias: false, alpha: false})
}
let scale = 1
while (Math.max(gameZoneWidthRoundedUp, gameZoneHeight) * scale > 400 * 2) {
scale = scale / 2
}
console.log('Recording at scale ' + scale)
recordCanvas.width = gameZoneWidthRoundedUp * scale
recordCanvas.height = gameZoneHeight * scale
gifCanvas.width = Math.floor(gameZoneWidthRoundedUp * scale / 2)
gifCanvas.height = Math.floor(gameZoneHeight * scale / 2)
// if(isSettingOn('basic')){
levelGif = new GIF({
workers: 2,
quality: 10,
repeat: 0,
background: currentLevelInfo()?.color || '#000',
width: gifCanvas.width,
height: gifCanvas.height,
dither: false,
});
// }else{
// levelGif=null
// }
// drawMainCanvasOnSmallCanvas()
const recordedChunks = [];
captureStream = captureStream || recordCanvas.captureStream(0);
const instance = new MediaRecorder(captureStream);
mediaRecorder = instance
instance.start();
mediaRecorder.pause()
instance.ondataavailable = function (event) {
recordedChunks.push(event.data);
}
instance.onstop = async function () {
let targetDiv = document.getElementById("level-recording-container")
if (!targetDiv) return
const video = document.createElement("video")
video.autoplay = true
video.controls = false
video.disablepictureinpicture = true
video.disableremoteplayback = true
video.width = recordCanvas.width
video.height = recordCanvas.height
targetDiv.style.width = recordCanvas.width + 'px'
targetDiv.style.height = recordCanvas.height + 'px'
video.loop = true
video.muted = true
video.playsinline = true
let blob = new Blob(recordedChunks, {type: "video/webm"});
video.src = URL.createObjectURL(blob);
const a = document.createElement("a")
a.download = captureFileName('webm')
a.target = "_blank"
a.href = video.src
a.textContent = `Download video (${(blob.size / 1000000).toFixed(2)}MB)`
targetDiv.appendChild(video)
targetDiv.appendChild(a)
}
levelGif?.on('finished', function (blob) {
let targetDiv = document.getElementById("level-recording-container")
const url = URL.createObjectURL(blob)
const img = document.createElement("img")
img.src = url
targetDiv?.appendChild(img)
const giflink = document.createElement("a")
giflink.textContent = `Download GIF (${(blob.size / 1000000).toFixed(2)}MB)`
giflink.href = url
giflink.download = captureFileName('gif')
targetDiv?.appendChild(giflink)
})
}
function pauseRecording() {
if (!isSettingOn('record')) {
return
}
if (mediaRecorder?.state === 'recording') {
mediaRecorder?.pause()
}
}
function resumeRecording() {
if (!isSettingOn('record')) {
return
}
if (mediaRecorder?.state === 'paused') {
mediaRecorder.resume()
}
}
function stopRecording() {
if (!isSettingOn('record')) {
return
}
if (!mediaRecorder) return;
mediaRecorder?.stop()
levelGif?.render()
mediaRecorder = null
levelGif = null
}
function captureFileName(ext) {
return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, '-') + '.' + ext
}
fitSize() fitSize()
restart() restart()
tick(); tick();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,15 +8,16 @@
/> />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Breakout 71</title> <title>Breakout 71</title>
<link rel="stylesheet" href="style.css?v=28999986" /> <link rel="stylesheet" href="style.css?v=29000794" />
<link rel="icon" href="./icon.svg" /> <link rel="icon" href="./icon.svg" />
</head> </head>
<body> <body>
<button id="menu"><span> menu</span></button> <button id="menu"><span> menu</span></button>
<button id="score"></button> <button id="score"></button>
<canvas id="game"></canvas> <canvas id="game"></canvas>
<script>window.appVersion="?v=28999986".slice(3)</script> <script>window.appVersion="?v=29000794".slice(3)</script>
<script src="levels.js?v=28999986"></script> <script src="gif.js"></script>
<script src="game.js?v=28999986"></script> <script src="levels.js?v=29000794"></script>
<script src="game.js?v=29000794"></script>
</body> </body>
</html> </html>

View file

@ -1,6 +1,5 @@
* { * {
font-family: font-family: Courier New,
Courier New,
Courier, Courier,
Lucida Sans Typewriter, Lucida Sans Typewriter,
Lucida Typewriter, Lucida Typewriter,
@ -28,6 +27,7 @@ body {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
} }
#score, #score,
#menu { #menu {
position: absolute; position: absolute;
@ -43,24 +43,28 @@ body {
min-height: 40px; min-height: 40px;
line-height: 20px; line-height: 20px;
} }
body.black_puck #score, body.black_puck #score,
body.black_puck #menu { body.black_puck #menu {
color:black; color: black;
} }
#score:hover, #score:hover,
#score:focus, #score:focus,
#menu:hover, #menu:hover,
#menu:focus { #menu:focus {
background: rgba(0,0,0,0.3); background: rgba(0, 0, 0, 0.3);
cursor: pointer; cursor: pointer;
} }
#score { #score {
right: 0; right: 0;
} }
#menu { #menu {
left: 0; left: 0;
} }
@media screen and (orientation: portrait) { @media screen and (orientation: portrait) {
#menu > span { #menu > span {
display: none; display: none;
@ -97,8 +101,9 @@ body.black_puck #menu {
.popup > div > p { .popup > div > p {
margin-bottom: 20px; margin-bottom: 20px;
} }
.popup > div > button { .popup > div > button {
font:inherit; font: inherit;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.8);
color: white; color: white;
padding: 10px; padding: 10px;
@ -119,25 +124,27 @@ body.black_puck #menu {
.popup button.close-modale { .popup button.close-modale {
color:white; color: white;
position: absolute; position: absolute;
top:0; top: 0;
right:0; right: 0;
width: 60px; width: 60px;
height: 60px; height: 60px;
background:none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
background: rgba(0,0,0,0.2); background: rgba(0, 0, 0, 0.2);
overflow: hidden; overflow: hidden;
} }
.popup button.close-modale:before { .popup button.close-modale:before {
content: "+"; content: "+";
position: absolute; position: absolute;
transform: translate(-50%, -50%) rotate(45deg) ; transform: translate(-50%, -50%) rotate(45deg);
font-size: 80px; font-size: 80px;
display: inline-block; display: inline-block;
} }
.popup button.close-modale:hover { .popup button.close-modale:hover {
font-weight: bold; font-weight: bold;
} }
@ -152,6 +159,7 @@ body.black_puck #menu {
.popup > div > button > div { .popup > div > button > div {
flex-grow: 1; flex-grow: 1;
} }
.popup > div > button > div > em { .popup > div > button > div > em {
display: block; display: block;
opacity: 0.8; opacity: 0.8;
@ -161,11 +169,12 @@ body.black_puck #menu {
width: 40px; width: 40px;
height: 40px; height: 40px;
display: inline-flex; display: inline-flex;
gap:5px; gap: 5px;
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
} }
.popup > div > button > span.checks>span {
.popup > div > button > span.checks > span {
flex-basis: 10px; flex-basis: 10px;
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-shrink: 1;
@ -175,21 +184,22 @@ body.black_puck #menu {
border-radius: 4px; border-radius: 4px;
align-self: stretch; align-self: stretch;
} }
.popup > div > button > span.checks>span.checked {
.popup > div > button > span.checks > span.checked {
opacity: 1; opacity: 1;
} }
.popup .textAfterButtons{ .popup .textAfterButtons {
color: rgba(255, 255, 255, 0.58); color: rgba(255, 255, 255, 0.58);
} }
.popup a[href]{ .popup a[href] {
color:inherit; color: inherit;
} }
.popup a[href]:hover, .popup a[href]:hover,
.popup a[href]:focus .popup a[href]:focus {
{ color: white;
color:white;
} }
/*Unlocks progress bar*/ /*Unlocks progress bar*/
@ -197,17 +207,18 @@ body.black_puck #menu {
display: block; display: block;
padding: 5px 10px; padding: 5px 10px;
background: #1c1c2f; background: #1c1c2f;
color:#fff; color: #fff;
box-shadow: inset 3px 3px 5px rgba(0,0,0,0.5); box-shadow: inset 3px 3px 5px rgba(0, 0, 0, 0.5);
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.progress >.progress_bar_part{
.progress > .progress_bar_part {
display: block; display: block;
background: #4049ca; background: #4049ca;
box-shadow: inset 3px 3px 5px rgba(0,0,0,0.5); box-shadow: inset 3px 3px 5px rgba(0, 0, 0, 0.5);
left: 0; left: 0;
position: absolute; position: absolute;
right: 0; right: 0;
@ -217,13 +228,48 @@ body.black_puck #menu {
animation: grow 1s both ease-out; animation: grow 1s both ease-out;
z-index: 1; z-index: 1;
} }
.progress> span {
.progress > span {
display: block; display: block;
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
@keyframes grow { @keyframes grow {
0%{ 0% {
transform: scale(0,1); transform: scale(0, 1);
}
}
#level-recording-container {
max-width: 400px;
text-align: center;
margin: 40px;
}
#level-recording-container img,#level-recording-container video{
max-width: 100%;
height: auto;
}
#level-recording-container a {
display: block;
}
#level-recording-container a video {
border-radius: 10px;
display: block;
outline: 1px solid white;
box-shadow: 2px 2px 5px black;
margin: 20px auto;
}
@media (min-width: 1200px) {
#level-recording-container {
position: absolute;
top: 40px;
left: 40px;
} }
} }

View file

@ -1,11 +1,21 @@
package me.lecaro.breakout package me.lecaro.breakout
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Base64
import android.util.Log import android.util.Log
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import android.webkit.ConsoleMessage import android.webkit.ConsoleMessage
import android.webkit.DownloadListener
import android.webkit.JavascriptInterface
import android.webkit.MimeTypeMap
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import java.io.File
import java.io.FileOutputStream
class MainActivity : android.app.Activity() { class MainActivity : android.app.Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -18,7 +28,7 @@ class MainActivity : android.app.Activity() {
val webView = WebView(this) val webView = WebView(this)
webView.settings.javaScriptEnabled = true webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true webView.settings.domStorageEnabled = true
webView.loadUrl("file:///android_asset/index.html") webView.loadUrl("file:///android_asset/index.html?isInWebView=true")
webView.webChromeClient = object : WebChromeClient() { webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d( Log.d(
@ -28,6 +38,7 @@ class MainActivity : android.app.Activity() {
return true return true
} }
} }
setContentView(webView) setContentView(webView)
} }
} }

966
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,13 +4,15 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "nodemon editserver.js --watch editserver.js " "start": "nodemon editserver.js --watch editserver.js ",
"serve": "http-server app/src/main/assets -o"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"express": "^4.21.2", "express": "^4.21.2",
"http-server": "^14.1.1",
"nodemon": "^3.1.9" "nodemon": "^3.1.9"
} }
} }