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
- publish on Fdroid
- enable gif export of gameplay capture
- enable export of gameplay capture in webview
## Perk ideas
- wrap left / right
- 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 stats on end screen compared to other runs
- 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
- balls should collide with each other
- when game resumes near bottom, be unvulnerable for .5s ? , once per level

View file

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

View file

@ -107,6 +107,7 @@ function play() {
if (audioContext) {
audioContext.resume()
}
resumeRecording()
}
function pause() {
@ -119,6 +120,7 @@ function pause() {
audioContext.suspend()
}, 1000)
}
pauseRecording()
}
let offsetX, offsetXRoundedDown, gameZoneWidth, gameZoneWidthRoundedUp, gameZoneHeight, brickWidth, needsRender = true;
@ -353,7 +355,8 @@ async function openUpgradesPicker() {
while (repeats--) {
const actions = pickRandomUpgrades(choices);
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({
title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text, allowClose: false,
@ -394,7 +397,8 @@ function setLevel(l) {
flashes = [];
background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
stopRecording()
startRecordingGame()
}
function currentLevelInfo() {
@ -770,6 +774,8 @@ function restart() {
setLevel(0);
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) {
@ -1298,6 +1304,7 @@ function addToTotalScore(points) {
function gameOver(title, intro) {
if (!running) return;
pause()
stopRecording()
runStatistics.ended = Date.now()
@ -1371,7 +1378,7 @@ function gameOver(title, intro) {
<p>${intro}</p>
${unlocksInfo}
`, textAfterButtons: `
<div id="level-recording-container"></div>
${scoreStory.map((t) => "<p>" + t + "</p>").join("")}
`
}).then(() => restart());
@ -1737,8 +1744,9 @@ function render() {
ctx.fillStyle = puckColor;
ctx.globalCompositeOperation = "source-over";
if (offsetXRoundedDown) {
ctx.fillRect(offsetX, 0, 1, height);
ctx.fillRect(width - offsetX - 1, 0, 1, height);
// draw outside of gaming area to avoid capturing borders in recordings
ctx.fillRect(offsetX - 1, 0, 1, height);
ctx.fillRect(width - offsetX + 1, 0, 1, height);
}
if (isSettingOn("mobile-mode")) {
ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1);
@ -1752,6 +1760,8 @@ function render() {
if (shaked) {
ctx.resetTransform();
}
recordOneFrame()
}
let cachedBricksRender = document.createElement("canvas");
@ -2343,6 +2353,11 @@ const options = {
}, "color_blind": {
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() {
@ -2350,8 +2365,10 @@ async function openSettingsPanel() {
const optionsList = [];
for (const key in options) {
if (options[key])
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)
if (options[key].restart) {
restart()
@ -2618,6 +2635,185 @@ function levelIconHTML(level, title) {
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()
restart()
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" />
<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" />
</head>
<body>
<button id="menu"><span> menu</span></button>
<button id="score"></button>
<canvas id="game"></canvas>
<script>window.appVersion="?v=28999986".slice(3)</script>
<script src="levels.js?v=28999986"></script>
<script src="game.js?v=28999986"></script>
<script>window.appVersion="?v=29000794".slice(3)</script>
<script src="gif.js"></script>
<script src="levels.js?v=29000794"></script>
<script src="game.js?v=29000794"></script>
</body>
</html>

View file

@ -1,6 +1,5 @@
* {
font-family:
Courier New,
font-family: Courier New,
Courier,
Lucida Sans Typewriter,
Lucida Typewriter,
@ -28,6 +27,7 @@ body {
height: 100vh;
width: 100vw;
}
#score,
#menu {
position: absolute;
@ -43,10 +43,12 @@ body {
min-height: 40px;
line-height: 20px;
}
body.black_puck #score,
body.black_puck #menu {
color: black;
}
#score:hover,
#score:focus,
#menu:hover,
@ -58,9 +60,11 @@ body.black_puck #menu {
#score {
right: 0;
}
#menu {
left: 0;
}
@media screen and (orientation: portrait) {
#menu > span {
display: none;
@ -97,6 +101,7 @@ body.black_puck #menu {
.popup > div > p {
margin-bottom: 20px;
}
.popup > div > button {
font: inherit;
background: rgba(0, 0, 0, 0.8);
@ -131,6 +136,7 @@ body.black_puck #menu {
background: rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.popup button.close-modale:before {
content: "+";
position: absolute;
@ -138,6 +144,7 @@ body.black_puck #menu {
font-size: 80px;
display: inline-block;
}
.popup button.close-modale:hover {
font-weight: bold;
}
@ -152,6 +159,7 @@ body.black_puck #menu {
.popup > div > button > div {
flex-grow: 1;
}
.popup > div > button > div > em {
display: block;
opacity: 0.8;
@ -165,6 +173,7 @@ body.black_puck #menu {
flex-grow: 0;
flex-shrink: 0;
}
.popup > div > button > span.checks > span {
flex-basis: 10px;
flex-grow: 1;
@ -175,6 +184,7 @@ body.black_puck #menu {
border-radius: 4px;
align-self: stretch;
}
.popup > div > button > span.checks > span.checked {
opacity: 1;
}
@ -186,9 +196,9 @@ body.black_puck #menu {
.popup a[href] {
color: inherit;
}
.popup a[href]:hover,
.popup a[href]:focus
{
.popup a[href]:focus {
color: white;
}
@ -204,6 +214,7 @@ body.black_puck #menu {
position: relative;
overflow: hidden;
}
.progress > .progress_bar_part {
display: block;
background: #4049ca;
@ -217,13 +228,48 @@ body.black_puck #menu {
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);
}
}
#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
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import android.util.Log
import android.view.Window
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.DownloadListener
import android.webkit.JavascriptInterface
import android.webkit.MimeTypeMap
import android.webkit.WebChromeClient
import android.webkit.WebView
import java.io.File
import java.io.FileOutputStream
class MainActivity : android.app.Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -18,7 +28,7 @@ class MainActivity : android.app.Activity() {
val webView = WebView(this)
webView.settings.javaScriptEnabled = 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() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d(
@ -28,6 +38,7 @@ class MainActivity : android.app.Activity() {
return true
}
}
setContentView(webView)
}
}

966
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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