mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-21 20:46:14 -04:00
Automatic deploy 29000794
This commit is contained in:
parent
80fa8e6329
commit
54512644ab
10 changed files with 876 additions and 730 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
3
app/src/main/assets/gif.js
Normal file
3
app/src/main/assets/gif.js
Normal file
File diff suppressed because one or more lines are too long
3
app/src/main/assets/gif.worker.js
Normal file
3
app/src/main/assets/gif.worker.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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
966
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue