Build 29035725

This commit is contained in:
Renan LE CARO 2025-03-16 17:45:29 +01:00
parent a1bf54af71
commit 819197031f
64 changed files with 3494 additions and 6921 deletions

View file

@ -47,6 +47,7 @@ There's also an easy mode for kids (slower ball).
- translation - translation
- when game resumes near bottom, be unvulnerable for .5s ? , once per level - when game resumes near bottom, be unvulnerable for .5s ? , once per level
# Game engine features # Game engine features
- ask for permanent storage - ask for permanent storage
@ -153,6 +154,14 @@ There's also an easy mode for kids (slower ball).
- level flips horizontally every time a ball bounces on puck - level flips horizontally every time a ball bounces on puck
- coins that hit the puck disappear, missed ones are scored - coins that hit the puck disappear, missed ones are scored
- squirell : keep coins on screen to have a higher combo - squirell : keep coins on screen to have a higher combo
- [colin] peaceful combo - le combo monte chaque seconde tant que les 2+ balles ne se touchent pas OU qu'on ne touche pas de bloc explosif.
- [colin] close quarters - balle attirée par tous les blocs/par un bloc aléatoire, actif à portée de bloc (+1bloc au lvlup)/proportionnel à une force (+puissance au lvlup)…
- [colin] shocks - balls can bounce off of each others and produce a shock that destroys a random block at the current combo
- [colin] plusieurs perks qui déclenchent des effets quand une balle est perdue. par ex: +3 combo à chaque balle perdue, 5 blocs transformés en bombe, balle et coins ralentis, blocs régénérés…
- [colin] faster style - augmente le combo en fonction de la vitesse de la balle
- [colin] perk: analyzer - permet de voir les caractéristiques cachées des blocs (sturdy…)
- [colin] perk: roulette - gagne instantanément 2 perks aléatoires
# extra levels # extra levels
@ -195,7 +204,7 @@ This requires recording a bit more info about each run.
I could unlock the "pro stand" at $999 that just holds the play area higher. I could unlock the "pro stand" at $999 that just holds the play area higher.
# increase skill ceiling # increase skill ceiling
- reroll mechanic, rerolls are reward for better play
- make puck smaller as combo increases ? - make puck smaller as combo increases ?
- nerf coin magnet : - nerf coin magnet :
- no effect when too close - no effect when too close
@ -216,8 +225,6 @@ I could unlock the "pro stand" at $999 that just holds the play area higher.
# Colin's feedback (cwpute/obigre) # Colin's feedback (cwpute/obigre)
* reward the player with more choices/perks for breaking a brick while having reached an increasing combo thresholds. 5 combo, then 10, then 20, then 40 etc… once a threshold is reached you aren't rewarded for that threshold again until you start a rew run * reward the player with more choices/perks for breaking a brick while having reached an increasing combo thresholds. 5 combo, then 10, then 20, then 40 etc… once a threshold is reached you aren't rewarded for that threshold again until you start a rew run

View file

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

File diff suppressed because one or more lines are too long

View file

@ -35,10 +35,10 @@ sed -i -e "s/^[[:space:]]*versionCode = .*/ versionCode = $versionCode/"
-e "s/^[[:space:]]*versionName = .*/ versionName = \"$versionCode\"/" \ -e "s/^[[:space:]]*versionName = .*/ versionName = \"$versionCode\"/" \
./app/build.gradle.kts ./app/build.gradle.kts
echo "\"$versionCode\"" > src/version.json echo "\"$versionCode\"" > src/data/version.json
# Update service worker # Update service worker
sed -i -e "s/VERSION = .*/ VERSION = '$versionCode'/" ./src/sw-b71.js sed -i -e "s/VERSION = .*/ VERSION = '$versionCode'/" ./src/PWA/sw-b71.js
@ -48,8 +48,9 @@ find -name '*.jp*g' -o -name '*.png' | xargs exiftool -all=
npx prettier --write src/ npx prettier --write src/
npm run build npx jest
rm -rf dist/*
npx parcel build src/index.html
rm -rf ./app/src/main/assets/* rm -rf ./app/src/main/assets/*
cp public/* dist cp public/* dist
rm -rf ./app/src/main/assets/* rm -rf ./app/src/main/assets/*

2
dist/PWA/sw-b71.js vendored Normal file
View file

@ -0,0 +1,2 @@
function e(e,t,n,r,a,i,c){try{var o=e[i](c),u=o.value}catch(e){n(e);return}o.done?t(u):Promise.resolve(u).then(r,a)}function t(t){return function(){var n=this,r=arguments;return new Promise(function(a,i){var c=t.apply(n,r);function o(t){e(c,a,i,o,u,"next",t)}function u(t){e(c,a,i,o,u,"throw",t)}o(void 0)})}}function n(e,t){var n,r,a,i,c={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return i={next:o(0),throw:o(1),return:o(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function o(i){return function(o){return function(i){if(n)throw TypeError("Generator is already executing.");for(;c;)try{if(n=1,r&&(a=2&i[0]?r.return:i[0]?r.throw||((a=r.return)&&a.call(r),0):r.next)&&!(a=a.call(r,i[1])).done)return a;switch(r=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return c.label++,{value:i[1],done:!1};case 5:c.label++,r=i[1],i=[0];continue;case 7:i=c.ops.pop(),c.trys.pop();continue;default:if(!(a=(a=c.trys).length>0&&a[a.length-1])&&(6===i[0]||2===i[0])){c=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]<a[3])){c.label=i[1];break}if(6===i[0]&&c.label<a[1]){c.label=a[1],a=i;break}if(a&&c.label<a[2]){c.label=a[2],c.ops.push(i);break}a[2]&&c.ops.pop(),c.trys.pop();continue}i=t.call(e,c)}catch(e){i=[6,e],r=0}finally{n=a=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,o])}}}var r="breakout-71-".concat("29035725"),a=["/"];self.addEventListener("install",function(e){e.waitUntil(t(function(){return n(this,function(e){switch(e.label){case 0:return[4,caches.open(r)];case 1:return e.sent().addAll(a),[2]}})})())}),self.addEventListener("activate",function(e){e.waitUntil(t(function(){return n(this,function(e){switch(e.label){case 0:return[4,caches.keys()];case 1:return[4,Promise.all(e.sent().map(function(e){if(e!==r)return caches.delete(e)}))];case 2:return e.sent(),[4,clients.claim()];case 3:return e.sent(),[2]}})})())}),self.addEventListener("fetch",function(e){if("navigate"===e.request.mode&&e.request.url.endsWith("/index.html?isPWA=true")){e.respondWith(caches.match("/"));return}});
//# sourceMappingURL=sw-b71.js.map

1
dist/PWA/sw-b71.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/editor.1350aee5.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -142,10 +142,10 @@
this[globalName] = mainExports; this[globalName] = mainExports;
} }
} }
})({"9zw4T":[function(require,module,exports,__globalThis) { })({"7Iayr":[function(require,module,exports,__globalThis) {
require("6f2db3dd8d20283b")(require("27e61996b32b4a9a").getBundleURL('ouAZg') + "index.c0fd3053.js"); require("4e14309168f23be0")(require("275cab9bde4ab8f8").getBundleURL('jo05F') + "editor.1350aee5.js");
},{"6f2db3dd8d20283b":"61B45","27e61996b32b4a9a":"lgJ39"}],"61B45":[function(require,module,exports,__globalThis) { },{"4e14309168f23be0":"61B45","275cab9bde4ab8f8":"lgJ39"}],"61B45":[function(require,module,exports,__globalThis) {
"use strict"; "use strict";
var cacheLoader = require("ca2a84f7fa4a3bb0"); var cacheLoader = require("ca2a84f7fa4a3bb0");
module.exports = cacheLoader(function(bundle) { module.exports = cacheLoader(function(bundle) {
@ -242,16 +242,16 @@ exports.getBundleURL = getBundleURLCached;
exports.getBaseURL = getBaseURL; exports.getBaseURL = getBaseURL;
exports.getOrigin = getOrigin; exports.getOrigin = getOrigin;
},{}],"iSxqL":[function(require,module,exports,__globalThis) { },{}],"9Ly5x":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
var _jsxDevRuntime = require("react/jsx-dev-runtime"); var _jsxDevRuntime = require("react/jsx-dev-runtime");
var _backgroundsJson = require("./backgrounds.json"); var _backgroundsJson = require("../data/backgrounds.json");
var _backgroundsJsonDefault = parcelHelpers.interopDefault(_backgroundsJson); var _backgroundsJsonDefault = parcelHelpers.interopDefault(_backgroundsJson);
var _paletteJson = require("./palette.json"); var _paletteJson = require("../data/palette.json");
var _paletteJsonDefault = parcelHelpers.interopDefault(_paletteJson); var _paletteJsonDefault = parcelHelpers.interopDefault(_paletteJson);
var _levelsJson = require("./levels.json"); var _levelsJson = require("../data/levels.json");
var _levelsJsonDefault = parcelHelpers.interopDefault(_levelsJson); var _levelsJsonDefault = parcelHelpers.interopDefault(_levelsJson);
var _getLevelBackground = require("./getLevelBackground"); var _getLevelBackground = require("../getLevelBackground");
var _client = require("react-dom/client"); var _client = require("react-dom/client");
var _react = require("react"); var _react = require("react");
var _levelsEditorUtil = require("./levels_editor_util"); var _levelsEditorUtil = require("./levels_editor_util");
@ -273,7 +273,7 @@ function App() {
}, []); }, []);
(0, _react.useEffect)(()=>{ (0, _react.useEffect)(()=>{
const timoutId = setTimeout(()=>{ const timoutId = setTimeout(()=>{
return fetch("http://localhost:4400/src/levels.json", { return fetch("http://localhost:4400/src/data/levels.json", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "text/plain" "Content-Type": "text/plain"
@ -317,7 +317,7 @@ function App() {
position: "absolute" position: "absolute"
} }
}, index, false, { }, index, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 63, lineNumber: 63,
columnNumber: 17 columnNumber: 17
}, this)); }, this));
@ -338,7 +338,7 @@ function App() {
name: e.target.value name: e.target.value
}) })
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 99, lineNumber: 99,
columnNumber: 15 columnNumber: 15
}, this), }, this),
@ -348,7 +348,7 @@ function App() {
onClick: ()=>deleteLevel(li), onClick: ()=>deleteLevel(li),
children: "Delete" children: "Delete"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 105, lineNumber: 105,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -356,7 +356,7 @@ function App() {
onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.resizeLevel)(level, -1)), onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.resizeLevel)(level, -1)),
children: "-" children: "-"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 106, lineNumber: 106,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -364,7 +364,7 @@ function App() {
onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.resizeLevel)(level, 1)), onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.resizeLevel)(level, 1)),
children: "+" children: "+"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 109, lineNumber: 109,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -372,7 +372,7 @@ function App() {
onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, -1, 0)), onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, -1, 0)),
children: "L" children: "L"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 112, lineNumber: 112,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -380,7 +380,7 @@ function App() {
onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, 1, 0)), onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, 1, 0)),
children: "R" children: "R"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 117, lineNumber: 117,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -388,7 +388,7 @@ function App() {
onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, 0, -1)), onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, 0, -1)),
children: "U" children: "U"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 120, lineNumber: 120,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -396,7 +396,7 @@ function App() {
onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, 0, 1)), onClick: ()=>updateLevel(li, (0, _levelsEditorUtil.moveLevel)(level, 0, 1)),
children: "D" children: "D"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 125, lineNumber: 125,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -407,7 +407,7 @@ function App() {
color: e.target.value color: e.target.value
}) })
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 128, lineNumber: 128,
columnNumber: 17 columnNumber: 17
}, this), }, this),
@ -419,13 +419,13 @@ function App() {
svg: parseFloat(e.target.value) svg: parseFloat(e.target.value)
}) })
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 135, lineNumber: 135,
columnNumber: 17 columnNumber: 17
}, this) }, this)
] ]
}, void 0, true, { }, void 0, true, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 104, lineNumber: 104,
columnNumber: 15 columnNumber: 15
}, this), }, this),
@ -438,19 +438,19 @@ function App() {
}, },
children: brickButtons children: brickButtons
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 147, lineNumber: 147,
columnNumber: 15 columnNumber: 15
}, this) }, this)
] ]
}, li, true, { }, li, true, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 98, lineNumber: 98,
columnNumber: 13 columnNumber: 13
}, this); }, this);
}) })
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 54, lineNumber: 54,
columnNumber: 7 columnNumber: 7
}, this), }, this),
@ -467,12 +467,12 @@ function App() {
}, },
onClick: ()=>setSelected(code) onClick: ()=>setSelected(code)
}, code, false, { }, code, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 163, lineNumber: 163,
columnNumber: 11 columnNumber: 11
}, this)) }, this))
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 161, lineNumber: 161,
columnNumber: 7 columnNumber: 7
}, this), }, this),
@ -494,25 +494,25 @@ function App() {
}, },
children: "new" children: "new"
}, void 0, false, { }, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 177, lineNumber: 177,
columnNumber: 7 columnNumber: 7
}, this) }, this)
] ]
}, void 0, true, { }, void 0, true, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 49, lineNumber: 49,
columnNumber: 5 columnNumber: 5
}, this); }, this);
} }
const root = (0, _client.createRoot)(document.getElementById("app")); const root = (0, _client.createRoot)(document.getElementById("app"));
root.render(/*#__PURE__*/ (0, _jsxDevRuntime.jsxDEV)(App, {}, void 0, false, { root.render(/*#__PURE__*/ (0, _jsxDevRuntime.jsxDEV)(App, {}, void 0, false, {
fileName: "src/levels_editor.tsx", fileName: "src/level_editor/levels_editor.tsx",
lineNumber: 203, lineNumber: 203,
columnNumber: 13 columnNumber: 13
}, undefined)); }, undefined));
},{"react/jsx-dev-runtime":"iTorj","./backgrounds.json":"el6Kx","./palette.json":"jhnsJ","./levels.json":"kqnNl","./getLevelBackground":"7OIPf","react-dom/client":"lOjBx","react":"21dqq","./levels_editor_util":"lt8Nt","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iTorj":[function(require,module,exports,__globalThis) { },{"react/jsx-dev-runtime":"iTorj","../data/backgrounds.json":"31wW4","../data/palette.json":"ktRBU","../data/levels.json":"8JSUc","../getLevelBackground":"7OIPf","react-dom/client":"lOjBx","react":"21dqq","./levels_editor_util":"lfafp","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iTorj":[function(require,module,exports,__globalThis) {
'use strict'; 'use strict';
module.exports = require("ee51401569654d91"); module.exports = require("ee51401569654d91");
@ -16252,7 +16252,7 @@ module.exports = require("b0f0e6b9e8349dac");
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error()); "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})(); })();
},{"6f0162e9ab224cd4":"21dqq"}],"lt8Nt":[function(require,module,exports,__globalThis) { },{"6f0162e9ab224cd4":"21dqq"}],"lfafp":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports); parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "resizeLevel", ()=>resizeLevel); parcelHelpers.export(exports, "resizeLevel", ()=>resizeLevel);
@ -16292,6 +16292,6 @@ function setBrick(level, index, colorCode) {
}; };
} }
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["9zw4T","iSxqL"], "iSxqL", "parcelRequire94c2") },{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["7Iayr","9Ly5x"], "9Ly5x", "parcelRequire94c2")
//# sourceMappingURL=levels_editor.ef3c2e1a.js.map //# sourceMappingURL=editor.1ec04b8f.js.map

1
dist/editor.1ec04b8f.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -53,4 +53,4 @@ body {
#levels > div > div:nth-child(3) { #levels > div > div:nth-child(3) {
grid-area: bricks; grid-area: bricks;
} }
/*# sourceMappingURL=levels_editor.de5e7f9b.css.map */ /*# sourceMappingURL=editor.9680328c.css.map */

1
dist/editor.9680328c.css.map vendored Normal file
View file

@ -0,0 +1 @@
{"mappings":"AAAA;;;;;AAKA;;;;;;;;;AAAA;;;;AAaA;;;;;;;;AAAA;;;;AAAA;;;;AAcE;;;;;;;AAKE;;;;AAIA;;;;;;;AAOA","sources":["src/level_editor/levels_editor.less"],"sourcesContent":["body {\n background: black;\n color: white;\n}\n\n#palette {\n position: fixed;\n top: 0;\n right: 0;\n width: 80px;\n bottom: 0;\n overflow: auto;\n\n button.active {\n transform: scale(1.2);\n }\n}\n\n#levels {\n display: flex;\n gap: 40px;\n align-items: flex-start;\n flex-wrap: wrap;\n margin-right: 80px;\n\n .level-bricks-preview {\n position: relative;\n }\n input[type=\"number\"] {\n width: 50px;\n }\n\n & > div {\n display: grid;\n grid-template-columns: auto auto;\n grid-template-areas: \". name\" \"buttons bricks\";\n\n & > *:nth-child(1) {\n grid-area: name;\n }\n\n & > div:nth-child(2) {\n grid-area: buttons;\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n }\n\n & > div:nth-child(3) {\n grid-area: bricks;\n }\n }\n}\n"],"names":[],"version":3,"file":"editor.9680328c.css.map","sourceRoot":"/__parcel_source_root/"}

View file

@ -1,14 +1,14 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head><script src="/index.c0fd3053.js"></script> <head><script src="/editor.1350aee5.js"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Level editor</title> <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>"> <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>">
<link rel="stylesheet" href="/levels_editor.de5e7f9b.css"> <link rel="stylesheet" href="/editor.9680328c.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="/levels_editor.ef3c2e1a.js" defer=""></script> <script src="/editor.1ec04b8f.js" defer=""></script>
</body> </body>
</html> </html>

8
dist/icon.7be7e26e.svg vendored Normal file
View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" height="50" width="50">
<rect x="0" y="0" width="30" height="10" fill="#6262EA"></rect>
<rect x="20" y="10" width="10" height="10" fill="#6262EA"></rect>
<rect x="10" y="20" width="10" height="20" fill="#6262EA"></rect>
<rect x="20" y="20" width="10" height="10" fill="#5DA3EA"></rect>
<rect x="30" y="10" width="10" height="30" fill="#5DA3EA"></rect>
<rect x="20" y="40" width="40" height="30" fill="#5DA3EA"></rect>
</svg>

After

Width:  |  Height:  |  Size: 464 B

1
dist/icon.939d1a8f.svg vendored Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"><path fill="#6262EA" d="M0 0h30v10H0zM20 10h10v10H20zM10 20h10v20H10z"/><path fill="#5DA3EA" d="M20 20h10v10H20zM30 10h10v30H30zM20 40h40v30H20z"/></svg>

After

Width:  |  Height:  |  Size: 216 B

File diff suppressed because one or more lines are too long

3646
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
{"mappings":"AAAA;;;;;AAKA;;;;;;;;;AAAA;;;;AAaA;;;;;;;;AAAA;;;;AAAA;;;;AAcE;;;;;;;AAKE;;;;AAIA;;;;;;;AAOA","sources":["src/levels_editor.less"],"sourcesContent":["body {\n background: black;\n color: white;\n}\n\n#palette {\n position: fixed;\n top: 0;\n right: 0;\n width: 80px;\n bottom: 0;\n overflow: auto;\n\n button.active {\n transform: scale(1.2);\n }\n}\n\n#levels {\n display: flex;\n gap: 40px;\n align-items: flex-start;\n flex-wrap: wrap;\n margin-right: 80px;\n\n .level-bricks-preview {\n position: relative;\n }\n input[type=\"number\"] {\n width: 50px;\n }\n\n & > div {\n display: grid;\n grid-template-columns: auto auto;\n grid-template-areas: \". name\" \"buttons bricks\";\n\n & > *:nth-child(1) {\n grid-area: name;\n }\n\n & > div:nth-child(2) {\n grid-area: buttons;\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n }\n\n & > div:nth-child(3) {\n grid-area: bricks;\n }\n }\n}\n"],"names":[],"version":3,"file":"levels_editor.de5e7f9b.css.map","sourceRoot":"/__parcel_source_root/"}

File diff suppressed because one or more lines are too long

33
dist/sw-b71.js vendored
View file

@ -1,33 +0,0 @@
// The version of the cache.
const VERSION = "29033878";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;
// The static resources that the app needs to function.
const APP_STATIC_RESOURCES = [
"/"
];
// On install, cache the static resources
self.addEventListener("install", (event)=>{
event.waitUntil((async ()=>{
const cache = await caches.open(CACHE_NAME);
cache.addAll(APP_STATIC_RESOURCES);
})());
});
// delete old caches on activate
self.addEventListener("activate", (event)=>{
event.waitUntil((async ()=>{
const names = await caches.keys();
await Promise.all(names.map((name)=>{
if (name !== CACHE_NAME) return caches.delete(name);
}));
await clients.claim();
})());
});
self.addEventListener("fetch", (event)=>{
if (event.request.mode === "navigate" && event.request.url.endsWith("/index.html?isPWA=true")) {
event.respondWith(caches.match("/"));
return;
}
});
//# sourceMappingURL=sw-b71.js.map

1
dist/sw-b71.js.map vendored
View file

@ -1 +0,0 @@
{"mappings":"AAAA,4BAA4B;AAC5B,MAAM,UAAU;AAEhB,wBAAwB;AACxB,MAAM,aAAa,CAAC,YAAY,EAAE,SAAS;AAE3C,uDAAuD;AACvD,MAAM,uBAAuB;IAAC;CAAI;AAElC,yCAAyC;AACzC,KAAK,gBAAgB,CAAC,WAAW,CAAC;IAChC,MAAM,SAAS,CACb,AAAC,CAAA;QACC,MAAM,QAAQ,MAAM,OAAO,IAAI,CAAC;QAChC,MAAM,MAAM,CAAC;IACf,CAAA;AAEJ;AAEA,gCAAgC;AAChC,KAAK,gBAAgB,CAAC,YAAY,CAAC;IACjC,MAAM,SAAS,CACb,AAAC,CAAA;QACC,MAAM,QAAQ,MAAM,OAAO,IAAI;QAC/B,MAAM,QAAQ,GAAG,CACf,MAAM,GAAG,CAAC,CAAC;YACT,IAAI,SAAS,YACX,OAAO,OAAO,MAAM,CAAC;QAEzB;QAEF,MAAM,QAAQ,KAAK;IACrB,CAAA;AAEJ;AAEA,KAAK,gBAAgB,CAAC,SAAS,CAAC;IAC9B,IACE,MAAM,OAAO,CAAC,IAAI,KAAK,cACvB,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,2BAC3B;QACA,MAAM,WAAW,CAAC,OAAO,KAAK,CAAC;QAC/B;IACF;AACF","sources":["src/sw-b71.js"],"sourcesContent":["// The version of the cache.\nconst VERSION = \"29033878\";\n\n// The name of the cache\nconst CACHE_NAME = `breakout-71-${VERSION}`;\n\n// The static resources that the app needs to function.\nconst APP_STATIC_RESOURCES = [\"/\"];\n\n// On install, cache the static resources\nself.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const cache = await caches.open(CACHE_NAME);\n cache.addAll(APP_STATIC_RESOURCES);\n })(),\n );\n});\n\n// delete old caches on activate\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const names = await caches.keys();\n await Promise.all(\n names.map((name) => {\n if (name !== CACHE_NAME) {\n return caches.delete(name);\n }\n }),\n );\n await clients.claim();\n })(),\n );\n});\n\nself.addEventListener(\"fetch\", (event) => {\n if (\n event.request.mode === \"navigate\" &&\n event.request.url.endsWith(\"/index.html?isPWA=true\")\n ) {\n event.respondWith(caches.match(\"/\"));\n return;\n }\n});\n"],"names":[],"version":3,"file":"sw-b71.js.map","sourceRoot":"/__parcel_source_root/"}

View file

@ -9,9 +9,9 @@ app.use(bodyParser.text({
limit:'1MB' limit:'1MB'
})); }));
app.post('/src/levels.json', (req, res) => { app.post('/src/data/levels.json', (req, res) => {
if(req.body?.trim()) { if(req.body?.trim()) {
fs.writeFileSync('src/levels.json', req.body) fs.writeFileSync('src/data/levels.json', req.body)
} }
res.end('OK') res.end('OK')
}) })

View file

@ -6,8 +6,7 @@
"start": "rm -rf .parcel-cache && run-p dev:*", "start": "rm -rf .parcel-cache && run-p dev:*",
"dev:game-fe": "parcel src/*.html --lazy --no-hmr", "dev:game-fe": "parcel src/*.html --lazy --no-hmr",
"dev:editor-be": "nodemon editserver.js --watch editserver.js", "dev:editor-be": "nodemon editserver.js --watch editserver.js",
"test": "jest --watch", "test": "jest --watch"
"build": "npx jest && rm -f dist/* && parcel build src/index.html"
}, },
"browserslist": "since 2009", "browserslist": "since 2009",
"author": "Renan LE CARO", "author": "Renan LE CARO",

View file

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 715 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 445 B

Before After
Before After

8
src/PWA/icon.svg Normal file
View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" height="50" width="50">
<rect x="0" y="0" width="30" height="10" fill="#6262EA"/>
<rect x="20" y="10" width="10" height="10" fill="#6262EA"/>
<rect x="10" y="20" width="10" height="20" fill="#6262EA"/>
<rect x="20" y="20" width="10" height="10" fill="#5DA3EA"/>
<rect x="30" y="10" width="10" height="30" fill="#5DA3EA"/>
<rect x="20" y="40" width="40" height="30" fill="#5DA3EA"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View file

@ -1,5 +1,5 @@
// The version of the cache. // The version of the cache.
const VERSION = "29033878"; const VERSION = "29035725";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -12,7 +12,6 @@ export type AsyncAlertAction<t> = {
className?: string; className?: string;
}; };
export function asyncAlert<t>({ export function asyncAlert<t>({
title, title,
text, text,
@ -44,7 +43,7 @@ export function asyncAlert<t>({
if (allowClose) { if (allowClose) {
const closeButton = document.createElement("button"); const closeButton = document.createElement("button");
closeButton.title = t('play.close_modale_window_tooltip'); closeButton.title = t("play.close_modale_window_tooltip");
closeButton.className = "close-modale"; closeButton.className = "close-modale";
closeButton.addEventListener("click", (e) => { closeButton.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();

View file

@ -8,7 +8,7 @@
}, },
{ {
"name": "Butterfly", "name": "Butterfly",
"bricks": "_________bb_t_t_bbbbb_t_bbbbbbbtbbbb_bbbtbbb____btb____bbbtbbb__bb_t_bb___________________", "bricks": "_________bb_t_t_bbbbb_t_bbbbbbbtbbbb_bbbtbbb____btb____bbbtbbb__bb_t_bb__________",
"size": 9, "size": 9,
"svg": 20, "svg": 20,
"color": "" "color": ""
@ -834,5 +834,12 @@
"size": 6, "size": 6,
"bricks": "_W__W_WW__WW____________WW__WW_W__W_", "bricks": "_W__W_WW__WW____________WW__WW_W__W_",
"svg": null "svg": null
},
{
"name": "icon:concave_puck",
"size": 8,
"bricks": "___________W_______________W_______________W_____________WWWWW__",
"svg": null,
"color": ""
} }
] ]

1
src/data/version.json Normal file
View file

@ -0,0 +1 @@
"29035725"

View file

@ -5,13 +5,13 @@
<title>Level editor</title> <title>Level editor</title>
<link <link
rel="icon" 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>" 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>"
/> />
<link rel="stylesheet" href="./levels_editor.less" /> <link rel="stylesheet" href="./level_editor/levels_editor.less" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="levels_editor.tsx"></script> <script type="module" src="./level_editor/levels_editor.tsx"></script>
</body> </body>
</html> </html>

View file

@ -1,9 +1,22 @@
import { allLevels, appVersion, icons, upgrades } from "./loadGameData"; import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
import {Ball, Coin, GameState, OptionId, PerkId, RunParams, Upgrade,} from "./types"; import {
Ball,
Coin,
GameState,
OptionId,
PerkId,
RunParams,
Upgrade,
} from "./types";
import { getAudioContext } from "./sounds"; import { getAudioContext } from "./sounds";
import {currentLevelInfo, getRowColIndex, max_levels, pickedUpgradesHTMl} from "./game_utils"; import {
currentLevelInfo,
getRowColIndex,
max_levels,
pickedUpgradesHTMl,
} from "./game_utils";
import "./sw_loader"; import "./PWA/sw_loader";
import { getCurrentLang, t } from "./i18n/i18n"; import { getCurrentLang, t } from "./i18n/i18n";
import { getSettingValue, getTotalScore, setSettingValue } from "./settings"; import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import { import {
@ -14,12 +27,29 @@ import {
resetBalls, resetBalls,
resetCombo, resetCombo,
setLevel, setLevel,
setMousePos setMousePos,
} from "./gameStateMutators"; } from "./gameStateMutators";
import {backgroundCanvas, bombSVG, ctx, gameCanvas, render, scoreDisplay} from "./render"; import {
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame} from "./recording"; backgroundCanvas,
bombSVG,
ctx,
gameCanvas,
render,
scoreDisplay,
} from "./render";
import {
pauseRecording,
recordOneFrame,
resumeRecording,
startRecordingGame,
} from "./recording";
import { newGameState } from "./newGameState"; import { newGameState } from "./newGameState";
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal} from "./asyncAlert"; import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options"; import { isOptionOn, options, toggleOption } from "./options";
bombSVG.src = bombSVG.src =
@ -68,7 +98,6 @@ export function pause(playerAskedForPause: boolean) {
} }
} }
export const fitSize = () => { export const fitSize = () => {
const { width, height } = gameCanvas.getBoundingClientRect(); const { width, height } = gameCanvas.getBoundingClientRect();
gameState.canvasWidth = width; gameState.canvasWidth = width;
@ -118,7 +147,6 @@ setInterval(() => {
fitSize(); fitSize();
}, 1000); }, 1000);
export async function openUpgradesPicker(gameState: GameState) { export async function openUpgradesPicker(gameState: GameState) {
const catchRate = const catchRate =
(gameState.score - gameState.levelStartScore) / (gameState.score - gameState.levelStartScore) /
@ -129,64 +157,86 @@ export async function openUpgradesPicker(gameState:GameState) {
let timeGain = "", let timeGain = "",
catchGain = "", catchGain = "",
wallHitsGain = "",
missesGain = ""; missesGain = "";
if (gameState.levelWallBounces == 0) {
repeats++;
choices++;
wallHitsGain = t("level_up.plus_one_upgrade");
} else if (gameState.levelWallBounces < 5) {
choices++;
wallHitsGain = t("level_up.plus_one_choice");
}
if (gameState.levelTime < 30 * 1000) { if (gameState.levelTime < 30 * 1000) {
repeats++; repeats++;
choices++; choices++;
timeGain = t('level_up.plus_one_upgrade'); timeGain = t("level_up.plus_one_upgrade");
} else if (gameState.levelTime < 60 * 1000) { } else if (gameState.levelTime < 60 * 1000) {
choices++; choices++;
timeGain = t('level_up.plus_one_choice'); timeGain = t("level_up.plus_one_choice");
} }
if (catchRate === 1) { if (catchRate === 1) {
repeats++; repeats++;
choices++; choices++;
catchGain = t('level_up.plus_one_upgrade'); catchGain = t("level_up.plus_one_upgrade");
} else if (catchRate > 0.9) { } else if (catchRate > 0.9) {
choices++; choices++;
catchGain = t('level_up.plus_one_choice'); catchGain = t("level_up.plus_one_choice");
} }
if (gameState.levelMisses === 0) { if (gameState.levelMisses === 0) {
repeats++; repeats++;
choices++; choices++;
missesGain = t('level_up.plus_one_upgrade'); missesGain = t("level_up.plus_one_upgrade");
} else if (gameState.levelMisses <= 3) { } else if (gameState.levelMisses <= 3) {
choices++; choices++;
missesGain = t('level_up.plus_one_choice'); missesGain = t("level_up.plus_one_choice");
} }
while (repeats--) { while (repeats--) {
const actions = pickRandomUpgrades(gameState, const actions = pickRandomUpgrades(
gameState,
choices + choices +
gameState.perks.one_more_choice - gameState.perks.one_more_choice -
gameState.perks.instant_upgrade, gameState.perks.instant_upgrade,
); );
if (!actions.length) break; if (!actions.length) break;
let textAfterButtons = ` let textAfterButtons = `
<p>${t('level_up.after_buttons', { <p>${t("level_up.after_buttons", {
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState) max: max_levels(gameState),
})} </p> })} </p>
<p>${pickedUpgradesHTMl(gameState)}</p> <p>${pickedUpgradesHTMl(gameState)}</p>
<div id="level-recording-container"></div> <div id="level-recording-container"></div>
`; `;
const compliment = (timeGain && catchGain && missesGain && t("level_up.compliment_perfect")) || const compliment =
((timeGain || catchGain || missesGain) && t("level_up.compliment_good")) || (timeGain &&
t('level_up.compliment_advice'); catchGain &&
missesGain &&
wallHitsGain &&
t("level_up.compliment_perfect")) ||
((timeGain || catchGain || missesGain || wallHitsGain) &&
t("level_up.compliment_good")) ||
t("level_up.compliment_advice");
const upgradeId = (await asyncAlert<PerkId>({ const upgradeId = (await asyncAlert<PerkId>({
title: t('level_up.pick_upgrade_title') + (repeats ? " (" + (repeats + 1) + ")" : ""), title:
t("level_up.pick_upgrade_title") +
(repeats ? " (" + (repeats + 1) + ")" : ""),
actions, actions,
text: `<p>${t('level_up.before_buttons', { text: `<p>${t("level_up.before_buttons", {
score: gameState.score - gameState.levelStartScore, score: gameState.score - gameState.levelStartScore,
catchGain, catchGain,
levelSpawnedCoins: gameState.levelSpawnedCoins, levelSpawnedCoins: gameState.levelSpawnedCoins,
time: Math.round(gameState.levelTime / 1000), time: Math.round(gameState.levelTime / 1000),
timeGain, timeGain,
levelMisses: gameState.levelMisses, missesGain, levelMisses: gameState.levelMisses,
compliment missesGain,
levelWallBounces: gameState.levelWallBounces,
wallHitsGain,
compliment,
})} })}
</p>`, </p>`,
allowClose: false, allowClose: false,
@ -244,7 +294,8 @@ gameCanvas.addEventListener("touchmove", (e) => {
}); });
export function brickIndex(x: number, y: number) { export function brickIndex(x: number, y: number) {
return getRowColIndex(gameState, return getRowColIndex(
gameState,
Math.floor(y / gameState.brickWidth), Math.floor(y / gameState.brickWidth),
Math.floor((x - gameState.offsetX) / gameState.brickWidth), Math.floor((x - gameState.offsetX) / gameState.brickWidth),
); );
@ -384,24 +435,25 @@ export function bordersHitCheck(
} }
export function tick() { export function tick() {
const currentTick = performance.now(); const currentTick = performance.now();
const timeDeltaMs = currentTick - gameState.lastTick const timeDeltaMs = currentTick - gameState.lastTick;
gameState.lastTick = currentTick; gameState.lastTick = currentTick;
const frames = Math.min(4, (timeDeltaMs) / (1000 / 60)); const frames = Math.min(4, timeDeltaMs / (1000 / 60));
if (gameState.keyboardPuckSpeed) { if (gameState.keyboardPuckSpeed) {
setMousePos(gameState, gameState.puckPosition + gameState.keyboardPuckSpeed); setMousePos(
gameState,
gameState.puckPosition + gameState.keyboardPuckSpeed,
);
} }
normalizeGameState(gameState) normalizeGameState(gameState);
if (gameState.running) { if (gameState.running) {
gameState.levelTime += timeDeltaMs; gameState.levelTime += timeDeltaMs;
gameState.runStatistics.runTime += timeDeltaMs; gameState.runStatistics.runTime += timeDeltaMs;
gameStateTick(gameState, frames) gameStateTick(gameState, frames);
} }
render(gameState); render(gameState);
recordOneFrame(gameState); recordOneFrame(gameState);
@ -428,25 +480,26 @@ document.addEventListener("visibilitychange", () => {
async function openScorePanel() { async function openScorePanel() {
pause(true); pause(true);
const cb = await asyncAlert({ const cb = await asyncAlert({
title: t('score_panel.title', { title: t("score_panel.title", {
score: gameState.score, level: gameState.currentLevel + 1, max: max_levels(gameState) score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels(gameState),
}), }),
text: ` text: `
${gameState.isCreativeModeRun ? "<p>${t('score_panel.test_run}</p>" : ""} ${gameState.isCreativeModeRun ? "<p>${t('score_panel.test_run}</p>" : ""}
<p>${t('score_panel.upgrades_picked')}</p> <p>${t("score_panel.upgrades_picked")}</p>
<p>${pickedUpgradesHTMl(gameState)}</p> <p>${pickedUpgradesHTMl(gameState)}</p>
`, `,
allowClose: true, allowClose: true,
actions: [ actions: [
{ {
text: t('score_panel.resume'), text: t("score_panel.resume"),
help: t('score_panel.resume_help'), help: t("score_panel.resume_help"),
value: () => { value: () => {},
},
}, },
{ {
text: t('score_panel.restart'), text: t("score_panel.restart"),
help: t('score_panel.restart_help'), help: t("score_panel.restart_help"),
value: () => { value: () => {
restart({ levelToAvoid: currentLevelInfo(gameState).name }); restart({ levelToAvoid: currentLevelInfo(gameState).name });
}, },
@ -468,14 +521,13 @@ async function openSettingsPanel() {
const actions: AsyncAlertAction<() => void>[] = [ const actions: AsyncAlertAction<() => void>[] = [
{ {
text: t('main_menu.resume'), text: t("main_menu.resume"),
help: t('main_menu.resume_help'), help: t("main_menu.resume_help"),
value() { value() {},
},
}, },
{ {
text: t("main_menu.unlocks"), text: t("main_menu.unlocks"),
help: t('main_menu.unlocks_help'), help: t("main_menu.unlocks_help"),
value() { value() {
openUnlocksList(); openUnlocksList();
}, },
@ -493,7 +545,7 @@ async function openSettingsPanel() {
help: options[key].help, help: options[key].help,
value: () => { value: () => {
toggleOption(key); toggleOption(key);
if (key === "mobile-mode") fitSize() if (key === "mobile-mode") fitSize();
openSettingsPanel(); openSettingsPanel();
}, },
@ -504,8 +556,8 @@ async function openSettingsPanel() {
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) { if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
if (document.fullscreenElement !== null) { if (document.fullscreenElement !== null) {
actions.push({ actions.push({
text: t('main_menu.fullscreen_exit'), text: t("main_menu.fullscreen_exit"),
help: t('main_menu.fullscreen_exit_help'), help: t("main_menu.fullscreen_exit_help"),
icon: icons["icon:exit_fullscreen"], icon: icons["icon:exit_fullscreen"],
value() { value() {
toggleFullScreen(); toggleFullScreen();
@ -513,9 +565,8 @@ async function openSettingsPanel() {
}); });
} else { } else {
actions.push({ actions.push({
text: t("main_menu.fullscreen"),
text: t('main_menu.fullscreen'), help: t("main_menu.fullscreen_help"),
help: t('main_menu.fullscreen_help'),
icon: icons["icon:fullscreen"], icon: icons["icon:fullscreen"],
value() { value() {
@ -526,22 +577,21 @@ async function openSettingsPanel() {
} }
actions.push({ actions.push({
text: t('sandbox.title'), text: t("sandbox.title"),
help: help:
getTotalScore() < creativeModeThreshold getTotalScore() < creativeModeThreshold
? t('sandbox.unlocks_at', {score: creativeModeThreshold}) ? t("sandbox.unlocks_at", { score: creativeModeThreshold })
: t('sandbox.help'), : t("sandbox.help"),
disabled: getTotalScore() < creativeModeThreshold, disabled: getTotalScore() < creativeModeThreshold,
async value() { async value() {
let creativeModePerks: Partial<{ [id in PerkId]: number }> =
let creativeModePerks: Partial<{ [id in PerkId]: number }> = getSettingValue('creativeModePerks', {}), getSettingValue("creativeModePerks", {}),
choice: "start" | Upgrade | void; choice: "start" | Upgrade | void;
while ( while (
(choice = await asyncAlert<"start" | Upgrade>({ (choice = await asyncAlert<"start" | Upgrade>({
title: t('sandbox.title'), title: t("sandbox.title"),
text: t('sandbox.instructions'), text: t("sandbox.instructions"),
actionsAsGrid: true, actionsAsGrid: true,
actions: [ actions: [
...upgrades.map((u) => ({ ...upgrades.map((u) => ({
@ -554,14 +604,14 @@ async function openSettingsPanel() {
: "grey-out-unless-hovered", : "grey-out-unless-hovered",
})), })),
{ {
text: t('sandbox.start'), text: t("sandbox.start"),
value: "start", value: "start",
}, },
], ],
})) }))
) { ) {
if (choice === "start") { if (choice === "start") {
setSettingValue('creativeModePerks', creativeModePerks) setSettingValue("creativeModePerks", creativeModePerks);
restart({ perks: creativeModePerks }); restart({ perks: creativeModePerks });
break; break;
@ -573,20 +623,20 @@ async function openSettingsPanel() {
}, },
}); });
actions.push({ actions.push({
text: t('main_menu.reset'), text: t("main_menu.reset"),
help: t('main_menu.reset_help'), help: t("main_menu.reset_help"),
async value() { async value() {
if ( if (
await asyncAlert({ await asyncAlert({
title: t('main_menu.reset'), title: t("main_menu.reset"),
text: t('main_menu.reset_instruction'), text: t("main_menu.reset_instruction"),
actions: [ actions: [
{ {
text: t('main_menu.reset_confirm'), text: t("main_menu.reset_confirm"),
value: true, value: true,
}, },
{ {
text: t('main_menu.reset_cancel'), text: t("main_menu.reset_cancel"),
value: false, value: false,
}, },
], ],
@ -600,41 +650,37 @@ async function openSettingsPanel() {
}); });
actions.push({ actions.push({
text: t('main_menu.language'), text: t("main_menu.language"),
help: t('main_menu.language_help'), help: t("main_menu.language_help"),
async value() { async value() {
const pick = await asyncAlert({ const pick = await asyncAlert({
title: t('main_menu.language'), title: t("main_menu.language"),
text: t('main_menu.language_help'), text: t("main_menu.language_help"),
actions: [ actions: [
{ {
text: 'English', text: "English",
value: 'en', value: "en",
}, },
{ {
text: 'Français', text: "Français",
value: 'fr', value: "fr",
}, },
], ],
allowClose: true, allowClose: true,
}) });
if ( if (pick && pick !== getCurrentLang() && (await confirmRestart())) {
pick && pick !== getCurrentLang() && setSettingValue("lang", pick);
await confirmRestart() window.location.reload();
) {
setSettingValue('lang', pick)
window.location.reload()
} }
}, },
}) });
const cb = await asyncAlert<() => void>({ const cb = await asyncAlert<() => void>({
title: t('main_menu.title'), title: t("main_menu.title"),
text: ``, text: ``,
allowClose: true, allowClose: true,
actions, actions,
textAfterButtons: t('main_menu.footer_html', {appVersion}), textAfterButtons: t("main_menu.footer_html", { appVersion }),
}); });
if (cb) { if (cb) {
cb(); cb();
@ -649,7 +695,7 @@ async function openUnlocksList() {
.map(({ name, id, threshold, icon, fullHelp }) => ({ .map(({ name, id, threshold, icon, fullHelp }) => ({
text: name, text: name,
help: help:
ts >= threshold ? fullHelp : t('unlocks.unlocks_at', {threshold}), ts >= threshold ? fullHelp : t("unlocks.unlocks_at", { threshold }),
disabled: ts < threshold, disabled: ts < threshold,
value: { perks: { [id]: 1 } } as RunParams, value: { perks: { [id]: 1 } } as RunParams,
icon, icon,
@ -661,10 +707,11 @@ async function openUnlocksList() {
return { return {
text: l.name, text: l.name,
help: available help: available
? t("unlocks.level_description", {
? size: l.size,
t('unlocks.level_description', {size: l.size, bricks: l.bricks.filter((i) => i).length}) bricks: l.bricks.filter((i) => i).length,
: t('unlocks.unlocks_at', {threshold: l.threshold}), })
: t("unlocks.unlocks_at", { threshold: l.threshold }),
disabled: !available, disabled: !available,
value: { level: l.name } as RunParams, value: { level: l.name } as RunParams,
icon: icons[l.name], icon: icons[l.name],
@ -676,9 +723,9 @@ async function openUnlocksList() {
(actions.filter((a) => !a.disabled).length / actions.length) * 100, (actions.filter((a) => !a.disabled).length / actions.length) * 100,
); );
const tryOn = await asyncAlert<RunParams>({ const tryOn = await asyncAlert<RunParams>({
title: t('unlocks.title', {percentUnlock}), title: t("unlocks.title", { percentUnlock }),
text: `<p>${t('unlocks.intro', {ts})} text: `<p>${t("unlocks.intro", { ts })}
${percentUnlock < 100 ? t('unlocks.greyed_out_help') : ""}</p> ${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}</p>
`, `,
textAfterButtons: `<p> textAfterButtons: `<p>
Your high score is ${gameState.highScore}. Your high score is ${gameState.highScore}.
@ -688,33 +735,29 @@ Click an item above to start a run with it.
allowClose: true, allowClose: true,
}); });
if (tryOn) { if (tryOn) {
if ( if (await confirmRestart()) {
await confirmRestart()
) {
restart(tryOn); restart(tryOn);
} }
} }
} }
export async function confirmRestart() { export async function confirmRestart() {
if (!gameState.currentLevel) return true if (!gameState.currentLevel) return true;
return asyncAlert({ return asyncAlert({
title: t('confirmRestart.title'), title: t("confirmRestart.title"),
text: t('confirmRestart.text'), text: t("confirmRestart.text"),
actions: [ actions: [
{ {
value: true, value: true,
text: t('confirmRestart.yes'), text: t("confirmRestart.yes"),
}, },
{ {
value: false, value: false,
text: t('confirmRestart.no'), text: t("confirmRestart.no"),
}, },
], ],
}) });
} }
export function toggleFullScreen() { export function toggleFullScreen() {
@ -817,4 +860,5 @@ tick();
// @ts-ignore // @ts-ignore
// window.stressTest= ()=>restart({level:'Shark',perks:{base_combo:100, pierce:10, multiball:8}}) // window.stressTest= ()=>restart({level:'Shark',perks:{base_combo:100, pierce:10, multiball:8}})
window.stressTest = () => restart({level: 'Shark', perks: {sapper: 2, pierce: 10, multiball: 3}}) window.stressTest = () =>
restart({ level: "Shark", perks: { sapper: 2, pierce: 10, multiball: 3 } });

View file

@ -14,7 +14,7 @@ export function getUpgraderUnlockPoints() {
if (u.threshold) { if (u.threshold) {
list.push({ list.push({
threshold: u.threshold, threshold: u.threshold,
title: u.name + ' ' + t('level_up.unlocked_perk'), title: u.name + " " + t("level_up.unlocked_perk"),
}); });
} }
}); });
@ -22,7 +22,7 @@ export function getUpgraderUnlockPoints() {
allLevels.forEach((l) => { allLevels.forEach((l) => {
list.push({ list.push({
threshold: l.threshold, threshold: l.threshold,
title: l.name + ' ' + t('level_up.unlocked_level'), title: l.name + " " + t("level_up.unlocked_level"),
}); });
}); });
@ -40,8 +40,7 @@ export function addToTotalPlayTime(ms: number) {
ms, ms,
), ),
); );
} catch (e) { } catch (e) {}
}
} }
export function gameOver(title: string, intro: string) { export function gameOver(title: string, intro: string) {
@ -79,7 +78,9 @@ export function gameOver(title: string, intro: string) {
const total = nextUnlock?.threshold - previousUnlockAt; const total = nextUnlock?.threshold - previousUnlockAt;
const done = endTs - previousUnlockAt; const done = endTs - previousUnlockAt;
intro += t('gameOver.next_unlock', {points: nextUnlock.threshold - endTs}); intro += t("gameOver.next_unlock", {
points: nextUnlock.threshold - endTs,
});
const scaleX = (done / total).toFixed(2); const scaleX = (done / total).toFixed(2);
unlocksInfo += ` unlocksInfo += `
@ -105,8 +106,7 @@ export function gameOver(title: string, intro: string) {
(u) => u.threshold > startTs && u.threshold < endTs, (u) => u.threshold > startTs && u.threshold < endTs,
); );
if (unlockedItems.length) { if (unlockedItems.length) {
unlocksInfo += `<p>${t("gameOver.unlocked_count", { count: unlockedItems.length })} ${unlockedItems.map((u) => u.title).join(", ")}</p>`;
unlocksInfo += `<p>${t('gameOver.unlocked_count', {count: unlockedItems.length})} ${unlockedItems.map((u) => u.title).join(", ")}</p>`;
} }
// Avoid the sad sound right as we restart a new games // Avoid the sad sound right as we restart a new games
@ -116,15 +116,15 @@ export function gameOver(title: string, intro: string) {
allowClose: true, allowClose: true,
title, title,
text: ` text: `
${gameState.isCreativeModeRun ? `<p>${t('gameOver.test_run')}</p> ` : ""} ${gameState.isCreativeModeRun ? `<p>${t("gameOver.test_run")}</p> ` : ""}
<p>${intro}</p> <p>${intro}</p>
<p>${t('gameOver.cumulative_total', {startTs, endTs})}</p> <p>${t("gameOver.cumulative_total", { startTs, endTs })}</p>
${unlocksInfo} ${unlocksInfo}
`, `,
actions: [ actions: [
{ {
value: null, value: null,
text: t('gameOver.restart'), text: t("gameOver.restart"),
help: "", help: "",
}, },
], ],
@ -208,40 +208,65 @@ export function getHistograms() {
`; `;
}; };
runStats += makeHistogram(t('gameOver.stats.total_score'), (r) => r.score, ""); runStats += makeHistogram(
runStats += makeHistogram(t('gameOver.stats.catch_rate'), t("gameOver.stats.total_score"),
(r) => r.score,
"",
);
runStats += makeHistogram(
t("gameOver.stats.catch_rate"),
(r) => Math.round((r.score / r.coins_spawned) * 100), (r) => Math.round((r.score / r.coins_spawned) * 100),
"%", "%",
); );
runStats += makeHistogram(t('gameOver.stats.bricks_broken'), (r) => r.bricks_broken, "");
runStats += makeHistogram( runStats += makeHistogram(
t('gameOver.stats.bricks_per_minute'), t("gameOver.stats.bricks_broken"),
(r) => r.bricks_broken,
"",
);
runStats += makeHistogram(
t("gameOver.stats.bricks_per_minute"),
(r) => Math.round((r.bricks_broken / r.runTime) * 1000 * 60), (r) => Math.round((r.bricks_broken / r.runTime) * 1000 * 60),
"", "",
); );
runStats += makeHistogram( runStats += makeHistogram(
t('gameOver.stats.hit_rate'), t("gameOver.stats.hit_rate"),
(r) => Math.round((1 - r.misses / r.puck_bounces) * 100), (r) => Math.round((1 - r.misses / r.puck_bounces) * 100),
"%", "%",
); );
runStats += makeHistogram( runStats += makeHistogram(
t('gameOver.stats.duration_per_level'), t("gameOver.stats.duration_per_level"),
(r) => Math.round(r.runTime / 1000 / r.levelsPlayed), (r) => Math.round(r.runTime / 1000 / r.levelsPlayed),
"s", "s",
); );
runStats += makeHistogram(t('gameOver.stats.level_reached'), (r) => r.levelsPlayed, "");
runStats += makeHistogram(t('gameOver.stats.upgrades_applied'), (r) => r.upgrades_picked, "");
runStats += makeHistogram(t('gameOver.stats.balls_lost'), (r) => r.balls_lost, "");
runStats += makeHistogram( runStats += makeHistogram(
t('gameOver.stats.combo_avg'), t("gameOver.stats.level_reached"),
(r) => r.levelsPlayed,
"",
);
runStats += makeHistogram(
t("gameOver.stats.upgrades_applied"),
(r) => r.upgrades_picked,
"",
);
runStats += makeHistogram(
t("gameOver.stats.balls_lost"),
(r) => r.balls_lost,
"",
);
runStats += makeHistogram(
t("gameOver.stats.combo_avg"),
(r) => Math.round(r.coins_spawned / r.bricks_broken), (r) => Math.round(r.coins_spawned / r.bricks_broken),
"", "",
); );
runStats += makeHistogram(t('gameOver.stats.combo_max'), (r) => r.max_combo, ""); runStats += makeHistogram(
t("gameOver.stats.combo_max"),
(r) => r.max_combo,
"",
);
if (runStats) { if (runStats) {
runStats = runStats =
`<p>${t('gameOver.stats.intro', {count: runsHistory.length - 1})}</p>` + `<p>${t("gameOver.stats.intro", { count: runsHistory.length - 1 })}</p>` +
runStats; runStats;
} }
} catch (e) { } catch (e) {

View file

@ -3,12 +3,13 @@ import {sounds} from "./sounds";
import { import {
brickCenterX, brickCenterX,
brickCenterY, brickCenterY,
currentLevelInfo, distanceBetween, currentLevelInfo,
distanceBetween,
getMajorityValue, getMajorityValue,
getPossibleUpgrades, getPossibleUpgrades,
getRowColIndex, getRowColIndex,
isTelekinesisActive, isTelekinesisActive,
max_levels max_levels,
} from "./game_utils"; } from "./game_utils";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { icons } from "./loadGameData"; import { icons } from "./loadGameData";
@ -26,7 +27,7 @@ import {
hitsSomething, hitsSomething,
openUpgradesPicker, openUpgradesPicker,
pause, pause,
shouldPierceByColor shouldPierceByColor,
} from "./game"; } from "./game";
import { stopRecording } from "./recording"; import { stopRecording } from "./recording";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
@ -36,6 +37,12 @@ export function setMousePos(gameState: GameState, x: number) {
gameState.puckPosition = x; gameState.puckPosition = x;
} }
function getBallDefaultVx(gameState: GameState) {
return (
(gameState.perks.concave_puck ? 0 : 1) *
(Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed)
);
}
export function resetBalls(gameState: GameState) { export function resetBalls(gameState: GameState) {
const count = 1 + (gameState.perks?.multiball || 0); const count = 1 + (gameState.perks?.multiball || 0);
const perBall = gameState.puckWidth / (count + 1); const perBall = gameState.puckWidth / (count + 1);
@ -48,7 +55,7 @@ export function resetBalls(gameState: GameState) {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const x = const x =
gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
const vx = Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed; const vx = getBallDefaultVx(gameState);
gameState.balls.push({ gameState.balls.push({
x, x,
@ -66,7 +73,6 @@ export function resetBalls(gameState: GameState) {
piercedSinceBounce: 0, piercedSinceBounce: 0,
hitSinceBounce: 0, hitSinceBounce: 0,
hitItem: [], hitItem: [],
bouncesList: [],
sapperUses: 0, sapperUses: 0,
}); });
} }
@ -76,6 +82,7 @@ export function putBallsAtPuck(gameState: GameState) {
// This reset could be abused to cheat quite easily // This reset could be abused to cheat quite easily
const count = gameState.balls.length; const count = gameState.balls.length;
const perBall = gameState.puckWidth / (count + 1); const perBall = gameState.puckWidth / (count + 1);
const vx = getBallDefaultVx(gameState);
gameState.balls.forEach((ball, i) => { gameState.balls.forEach((ball, i) => {
const x = const x =
gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
@ -84,7 +91,7 @@ export function putBallsAtPuck(gameState: GameState) {
ball.previousX = x; ball.previousX = x;
ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize; ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize;
ball.previousY = ball.y; ball.previousY = ball.y;
ball.vx = Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed; ball.vx = vx;
ball.previousVX = ball.vx; ball.previousVX = ball.vx;
ball.vy = -gameState.baseSpeed; ball.vy = -gameState.baseSpeed;
ball.previousVY = ball.vy; ball.previousVY = ball.vy;
@ -111,7 +118,6 @@ export function normalizeGameState(gameState: GameState) {
(gameState.gameZoneWidth / 12) * (gameState.gameZoneWidth / 12) *
(3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck); (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck);
if ( if (
gameState.puckPosition < gameState.puckPosition <
gameState.offsetXRoundedDown + gameState.puckWidth / 2 gameState.offsetXRoundedDown + gameState.puckWidth / 2
@ -152,7 +158,7 @@ export function resetCombo(
} }
if (prev > gameState.combo && gameState.perks.soft_reset) { if (prev > gameState.combo && gameState.perks.soft_reset) {
gameState.combo += Math.floor( gameState.combo += Math.floor(
(prev - gameState.combo) * (gameState.perks.soft_reset*10)/100, ((prev - gameState.combo) * (gameState.perks.soft_reset * 10)) / 100,
); );
} }
const lost = Math.max(0, prev - gameState.combo); const lost = Math.max(0, prev - gameState.combo);
@ -233,7 +239,12 @@ export function spawnExplosion(
} }
} }
export function explodeBrick(gameState: GameState, index: number, ball: Ball, isExplosion: boolean) { export function explodeBrick(
gameState: GameState,
index: number,
ball: Ball,
isExplosion: boolean,
) {
const color = gameState.bricks[index]; const color = gameState.bricks[index];
if (!color) return; if (!color) return;
@ -282,7 +293,8 @@ export function explodeBrick(gameState: GameState, index: number, ball: Ball, is
x, x,
y, y,
}); });
spawnExplosion(gameState, spawnExplosion(
gameState,
7 * (1 + gameState.perks.bigger_explosions), 7 * (1 + gameState.perks.bigger_explosions),
x, x,
y, y,
@ -364,7 +376,7 @@ export function explodeBrick(gameState: GameState, index: number, ball: Ball, is
gameState.perks.left_is_lava + gameState.perks.left_is_lava +
gameState.perks.right_is_lava + gameState.perks.right_is_lava +
gameState.perks.top_is_lava + gameState.perks.top_is_lava +
gameState.perks.picky_eater gameState.perks.picky_eater,
); );
if (!isExplosion) { if (!isExplosion) {
@ -382,7 +394,15 @@ export function explodeBrick(gameState: GameState, index: number, ball: Ball, is
gameState.ballsColor = color; gameState.ballsColor = color;
if (!isOptionOn("basic")) { if (!isOptionOn("basic")) {
gameState.balls.forEach((ball) => { gameState.balls.forEach((ball) => {
spawnExplosion(gameState,7, ball.previousX, ball.previousY, color, 150, 15); spawnExplosion(
gameState,
7,
ball.previousX,
ball.previousY,
color,
150,
15,
);
}); });
} }
} else { } else {
@ -399,7 +419,8 @@ export function explodeBrick(gameState: GameState, index: number, ball: Ball, is
x, x,
y, y,
}); });
spawnExplosion(gameState, spawnExplosion(
gameState,
5 + Math.min(gameState.combo, 30), 5 + Math.min(gameState.combo, 30),
x, x,
y, y,
@ -439,7 +460,11 @@ export function pickRandomUpgrades(gameState: GameState, count: number) {
return list.map((u) => ({ return list.map((u) => ({
text: text:
u.name + u.name +
(gameState.perks[u.id] ? t('level_up.upgrade_perk_to_level', {level: gameState.perks[u.id] + 1}) : ""), (gameState.perks[u.id]
? t("level_up.upgrade_perk_to_level", {
level: gameState.perks[u.id] + 1,
})
: ""),
icon: icons["icon:" + u.id], icon: icons["icon:" + u.id],
value: u.id as PerkId, value: u.id as PerkId,
help: u.help(gameState.perks[u.id] + 1), help: u.help(gameState.perks[u.id] + 1),
@ -487,6 +512,7 @@ export function setLevel(gameState: GameState, l: number) {
gameState.currentLevel = l; gameState.currentLevel = l;
gameState.levelTime = 0; gameState.levelTime = 0;
gameState.levelWallBounces = 0;
gameState.autoCleanUses = 0; gameState.autoCleanUses = 0;
gameState.lastTickDown = gameState.levelTime; gameState.lastTickDown = gameState.levelTime;
gameState.levelStartScore = gameState.score; gameState.levelStartScore = gameState.score;
@ -515,7 +541,8 @@ export function rainbowColor(): colorString {
return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`; return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`;
} }
export function repulse(gameState: GameState, export function repulse(
gameState: GameState,
a: Ball, a: Ball,
b: BallLike, b: BallLike,
power: number, power: number,
@ -622,10 +649,11 @@ export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
}); });
} }
export function gameStateTick(gameState: GameState, export function gameStateTick(
gameState: GameState,
// How many frames to compute at once, can go above 1 to compensate lag // How many frames to compute at once, can go above 1 to compensate lag
frames = 1) { frames = 1,
) {
gameState.runStatistics.max_combo = Math.max( gameState.runStatistics.max_combo = Math.max(
gameState.runStatistics.max_combo, gameState.runStatistics.max_combo,
gameState.combo, gameState.combo,
@ -667,8 +695,8 @@ export function gameStateTick(gameState: GameState,
setLevel(gameState, gameState.currentLevel + 1); setLevel(gameState, gameState.currentLevel + 1);
} else { } else {
gameOver( gameOver(
t('gameOver.win.title'), t("gameOver.win.title"),
t('gameOver.win.summary', {score: gameState.score}), t("gameOver.win.summary", { score: gameState.score }),
); );
} }
} else if (gameState.running || gameState.levelTime) { } else if (gameState.running || gameState.levelTime) {
@ -693,12 +721,10 @@ export function gameStateTick(gameState: GameState,
coin.vy *= ratio; coin.vy *= ratio;
coin.vx *= ratio; coin.vx *= ratio;
if (coin.vx > 7 * gameState.baseSpeed) if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
coin.vx = 7 * gameState.baseSpeed;
if (coin.vx < -7 * gameState.baseSpeed) if (coin.vx < -7 * gameState.baseSpeed)
coin.vx = -7 * gameState.baseSpeed; coin.vx = -7 * gameState.baseSpeed;
if (coin.vy > 7 * gameState.baseSpeed) if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed;
coin.vy = 7 * gameState.baseSpeed;
if (coin.vy < -7 * gameState.baseSpeed) if (coin.vy < -7 * gameState.baseSpeed)
coin.vy = -7 * gameState.baseSpeed; coin.vy = -7 * gameState.baseSpeed;
coin.a += coin.sa; coin.a += coin.sa;
@ -710,8 +736,7 @@ export function gameStateTick(gameState: GameState,
const hitBorder = bordersHitCheck(coin, coin.size / 2, frames); const hitBorder = bordersHitCheck(coin, coin.size / 2, frames);
if ( if (
coin.y > coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy && coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy &&
Math.abs(coin.x - gameState.puckPosition) < Math.abs(coin.x - gameState.puckPosition) <
coinRadius + coinRadius +
@ -933,7 +958,8 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
gameState.puckWidth / 2 + gameState.puckWidth / 2 +
(gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10 (gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10
) { ) {
repulse(gameState, repulse(
gameState,
ball, ball,
{ {
x: gameState.puckPosition, x: gameState.puckPosition,
@ -997,7 +1023,8 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
resetCombo(gameState, ball.x, ball.y + gameState.ballSize); resetCombo(gameState, ball.x, ball.y + gameState.ballSize);
} }
sounds.wallBeep(ball.x); sounds.wallBeep(ball.x);
ball.bouncesList?.push({x: ball.previousX, y: ball.previousY}); gameState.levelWallBounces++;
gameState.runStatistics.wall_bounces++;
} }
// Puck collision // Puck collision
@ -1017,7 +1044,8 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
const angle = Math.atan2( const angle = Math.atan2(
-gameState.puckWidth / 2, -gameState.puckWidth / 2,
ball.x - gameState.puckPosition, (ball.x - gameState.puckPosition) *
(gameState.perks.concave_puck ? -0.5 : 1),
); );
ball.vx = speed * Math.cos(angle); ball.vx = speed * Math.cos(angle);
ball.vy = speed * Math.sin(angle); ball.vy = speed * Math.sin(angle);
@ -1063,7 +1091,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
resetCombo(gameState, ball.x, ball.y); resetCombo(gameState, ball.x, ball.y);
gameState.flashes.push({ gameState.flashes.push({
type: "text", type: "text",
text: t('play.missed_ball'), text: t("play.missed_ball"),
duration: 500, duration: 500,
time: gameState.levelTime, time: gameState.levelTime,
size: gameState.puckHeight * 1.5, size: gameState.puckHeight * 1.5,
@ -1076,12 +1104,6 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
ball.hitSinceBounce = 0; ball.hitSinceBounce = 0;
ball.sapperUses = 0; ball.sapperUses = 0;
ball.piercedSinceBounce = 0; ball.piercedSinceBounce = 0;
ball.bouncesList = [
{
x: ball.previousX,
y: ball.previousY,
},
];
} }
if ( if (
@ -1092,8 +1114,9 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
gameState.runStatistics.balls_lost++; gameState.runStatistics.balls_lost++;
if (!gameState.balls.find((b) => !b.destroyed)) { if (!gameState.balls.find((b) => !b.destroyed)) {
gameOver( gameOver(
t('gameOver.lost.title'), t("gameOver.lost.title"),
t('gameOver.lost.summary', {score: gameState.score})) t("gameOver.lost.summary", { score: gameState.score }),
);
} }
} }
const radius = gameState.ballSize / 2; const radius = gameState.ballSize / 2;

View file

@ -4,7 +4,6 @@ import {
sample, sample,
sumOfKeys, sumOfKeys,
} from "./game_utils"; } from "./game_utils";
import { Upgrade } from "./types";
describe("getMajorityValue", () => { describe("getMajorityValue", () => {
it("returns the most common string", () => { it("returns the most common string", () => {

View file

@ -1,6 +1,6 @@
import { RawLevel } from "./types"; import { RawLevel } from "./types";
import _backgrounds from "./backgrounds.json"; import _backgrounds from "./data/backgrounds.json";
const backgrounds = _backgrounds as string[]; const backgrounds = _backgrounds as string[];
export function getLevelBackground(level: RawLevel) { export function getLevelBackground(level: RawLevel) {

View file

@ -1792,6 +1792,56 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>concave_puck</name>
<children>
<concept_node>
<name>fullHelp</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>name</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node> <folder_node>
<name>extra_levels</name> <name>extra_levels</name>
<children> <children>

View file

@ -25,8 +25,8 @@
"gameOver.win.summary": "You cleared all levels for this run, catching {{score}} coins in total.", "gameOver.win.summary": "You cleared all levels for this run, catching {{score}} coins in total.",
"gameOver.win.title": "Run finished", "gameOver.win.title": "Run finished",
"level_up.after_buttons": "You just finished level {{level}}/{{max}} and picked those upgrades so far :", "level_up.after_buttons": "You just finished level {{level}}/{{max}} and picked those upgrades so far :",
"level_up.before_buttons": "You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds ${timeGain}.\n\nYou missed {{levelMisses}} times {{missesGain}}.\n\n{{compliment}}", "level_up.before_buttons": "You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds {{timeGain}}.\n\nYou missed {{levelMisses}} times {{missesGain}} and hit the walls or ceiling {{levelWallBounces}} times{{wallHitsGain}}.\n\n{{compliment}}",
"level_up.compliment_advice": "Try to catch all coins, never miss the bricks or clear the level under 30s to gain additional choices and upgrades.", "level_up.compliment_advice": "Try to catch all coins, never miss the bricks, never hit the walls/ceiling or clear the level under 30s to gain additional choices and upgrades.",
"level_up.compliment_good": "Well done !", "level_up.compliment_good": "Well done !",
"level_up.compliment_perfect": "Impressive, keep it up !", "level_up.compliment_perfect": "Impressive, keep it up !",
"level_up.pick_upgrade_title": "Pick an upgrade", "level_up.pick_upgrade_title": "Pick an upgrade",
@ -74,7 +74,7 @@
"sandbox.instructions": "Select perks below and press \"start run\" to try them out in a test run. Scores and stats are not recorded.", "sandbox.instructions": "Select perks below and press \"start run\" to try them out in a test run. Scores and stats are not recorded.",
"sandbox.start": "Start test run", "sandbox.start": "Start test run",
"sandbox.title": "Sandbox mode", "sandbox.title": "Sandbox mode",
"sandbox.unlocks_at": "Unlocks at total score ${{score}}", "sandbox.unlocks_at": "Unlocks at total score {{score}}",
"score_panel.restart": "Restart", "score_panel.restart": "Restart",
"score_panel.restart_help": "Start a brand new run", "score_panel.restart_help": "Start a brand new run",
"score_panel.resume": "Resume", "score_panel.resume": "Resume",
@ -111,6 +111,9 @@
"upgrades.compound_interest.fullHelp": "Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. \n\nBe sure however to catch every one of those coins with your puck, as any lost coin will reset your combo. \n\nOnce your combo is above the minimum, the bottom of the play area will have a red line to remind you that coins should not go there.\n\nThis perk combines with other combo perks, the combo will rise faster but reset more easily.", "upgrades.compound_interest.fullHelp": "Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. \n\nBe sure however to catch every one of those coins with your puck, as any lost coin will reset your combo. \n\nOnce your combo is above the minimum, the bottom of the play area will have a red line to remind you that coins should not go there.\n\nThis perk combines with other combo perks, the combo will rise faster but reset more easily.",
"upgrades.compound_interest.help": "+1 combo per brick broken, resets on coin lost", "upgrades.compound_interest.help": "+1 combo per brick broken, resets on coin lost",
"upgrades.compound_interest.name": "Compound interest", "upgrades.compound_interest.name": "Compound interest",
"upgrades.concave_puck.fullHelp": "Balls starts the level going straight up, and bounces with less angle.",
"upgrades.concave_puck.help": " Helps with aiming straight up",
"upgrades.concave_puck.name": "Concave puck",
"upgrades.extra_levels.fullHelp": "The default run can last a max of 7 levels, after which the game is over and whatever score you reached is your run score. \n\nEach level of this perk lets you go one level higher. The last levels are often the ones where you make the most score, so the difference can be dramatic.", "upgrades.extra_levels.fullHelp": "The default run can last a max of 7 levels, after which the game is over and whatever score you reached is your run score. \n\nEach level of this perk lets you go one level higher. The last levels are often the ones where you make the most score, so the difference can be dramatic.",
"upgrades.extra_levels.help": "Play {{count}} levels instead of 7", "upgrades.extra_levels.help": "Play {{count}} levels instead of 7",
"upgrades.extra_levels.name": "+1 level", "upgrades.extra_levels.name": "+1 level",
@ -187,7 +190,7 @@
"upgrades.telekinesis.name": "Telekinesis", "upgrades.telekinesis.name": "Telekinesis",
"upgrades.top_is_lava.fullHelp": "Whenever you break a brick, your combo will increase by one. However, your combo will reset as soon as your ball hit the top of the screen. \n\nWhen your combo is above the minimum, a red bar will appear at the top to remind you that you should avoid hitting it. \n\nThe effect stacks with other combo perks.", "upgrades.top_is_lava.fullHelp": "Whenever you break a brick, your combo will increase by one. However, your combo will reset as soon as your ball hit the top of the screen. \n\nWhen your combo is above the minimum, a red bar will appear at the top to remind you that you should avoid hitting it. \n\nThe effect stacks with other combo perks.",
"upgrades.top_is_lava.help": "More coins if you don't touch the top.", "upgrades.top_is_lava.help": "More coins if you don't touch the top.",
"upgrades.top_is_lava.name": "Icarus", "upgrades.top_is_lava.name": "Sky is the limit",
"upgrades.viscosity.fullHelp": "Coins normally accelerate with gravity and explosions to pretty high speeds. \n\nThis perk constantly makes them slow down, as if they were in some sort of viscous liquid. \n\nThis makes catching them easier, and combines nicely with perks that influence the coin's movement.", "upgrades.viscosity.fullHelp": "Coins normally accelerate with gravity and explosions to pretty high speeds. \n\nThis perk constantly makes them slow down, as if they were in some sort of viscous liquid. \n\nThis makes catching them easier, and combines nicely with perks that influence the coin's movement.",
"upgrades.viscosity.help": "Slower coin fall", "upgrades.viscosity.help": "Slower coin fall",
"upgrades.viscosity.name": "Viscosity", "upgrades.viscosity.name": "Viscosity",

View file

@ -25,8 +25,8 @@
"gameOver.win.summary": "Vous avez nettoyé tous les niveaux pour cette partie, en attrapant {{score}} pièces au total.", "gameOver.win.summary": "Vous avez nettoyé tous les niveaux pour cette partie, en attrapant {{score}} pièces au total.",
"gameOver.win.title": "Partie terminée", "gameOver.win.title": "Partie terminée",
"level_up.after_buttons": "Vous venez de terminer le niveau {{level}}/{{max}} et vous avez choisi ces améliorations jusqu'à présent :", "level_up.after_buttons": "Vous venez de terminer le niveau {{level}}/{{max}} et vous avez choisi ces améliorations jusqu'à présent :",
"level_up.before_buttons": "Vous avez attrapé {{score}} pièces {{catchGain}} sur {{levelSpawnedCoins}} en {{time}} secondes ${timeGain}.\n\nVous avez raté les briques {{levelMisses}} fois {{missesGain}}.\n\n{{compliment}}", "level_up.before_buttons": "Vous avez attrapé {{score}} pièces {{catchGain}} sur {{levelSpawnedCoins}} en {{time}} secondes {{timeGain}}.\n\nVous avez raté les briques {{levelMisses}} fois {{missesGain} et touché les cotés et le haut de la zone de jeu {{levelWallBounces}} fois {{wallHitsGain}}.\n\n{{compliment}}",
"level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.", "level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques, de ne pas toucher les murs ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.",
"level_up.compliment_good": "Bravo !", "level_up.compliment_good": "Bravo !",
"level_up.compliment_perfect": "Impressionnant, continuez comme ça !", "level_up.compliment_perfect": "Impressionnant, continuez comme ça !",
"level_up.pick_upgrade_title": "Choisir une amélioration", "level_up.pick_upgrade_title": "Choisir une amélioration",
@ -74,7 +74,7 @@
"sandbox.instructions": "Sélectionnez les amélioration ci-dessous et appuyez sur \"Démarrer la partie de test\" pour les tester. Les scores et les statistiques ne seront pas enregistrés.", "sandbox.instructions": "Sélectionnez les amélioration ci-dessous et appuyez sur \"Démarrer la partie de test\" pour les tester. Les scores et les statistiques ne seront pas enregistrés.",
"sandbox.start": "Démarrer la partie de test", "sandbox.start": "Démarrer la partie de test",
"sandbox.title": "Mode bac à sable", "sandbox.title": "Mode bac à sable",
"sandbox.unlocks_at": "Déverrouillé à partir d'un score total de ${{score}}", "sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}",
"score_panel.restart": "Redémarrer", "score_panel.restart": "Redémarrer",
"score_panel.restart_help": "Commencer une nouvelle partie", "score_panel.restart_help": "Commencer une nouvelle partie",
"score_panel.resume": "Continuer la partie", "score_panel.resume": "Continuer la partie",
@ -111,6 +111,9 @@
"upgrades.compound_interest.fullHelp": "Votre combo augmentera d'une unité à chaque fois que vous casserez une brique, générant de plus en plus de pièces à chaque fois que vous casserez une brique. Veillez cependant à attraper chacune de ces pièces avec votre palet, car toute pièce perdue remettra votre combo à zéro. \n \nSi votre combinaison est supérieure au minimum, une ligne rouge s'affichera au bas de la zone de jeu pour vous le rappeler que les pièces ne doivent pas aller à cet endroit.\n\nCet avantage se combine avec d'autres avantages de combo, le combo augmentera plus rapidement mais se réinitialisera plus souvent.", "upgrades.compound_interest.fullHelp": "Votre combo augmentera d'une unité à chaque fois que vous casserez une brique, générant de plus en plus de pièces à chaque fois que vous casserez une brique. Veillez cependant à attraper chacune de ces pièces avec votre palet, car toute pièce perdue remettra votre combo à zéro. \n \nSi votre combinaison est supérieure au minimum, une ligne rouge s'affichera au bas de la zone de jeu pour vous le rappeler que les pièces ne doivent pas aller à cet endroit.\n\nCet avantage se combine avec d'autres avantages de combo, le combo augmentera plus rapidement mais se réinitialisera plus souvent.",
"upgrades.compound_interest.help": "+1 combo par brique cassée, remise à zéro quand une pièce est perdu", "upgrades.compound_interest.help": "+1 combo par brique cassée, remise à zéro quand une pièce est perdu",
"upgrades.compound_interest.name": "Intérêts", "upgrades.compound_interest.name": "Intérêts",
"upgrades.concave_puck.fullHelp": " Les balles démarrent verticalement en début de niveau, et rebondi sur le palet de manière plus verticale et inversée.",
"upgrades.concave_puck.help": "Aide à éviter les bords.",
"upgrades.concave_puck.name": "Palet concave",
"upgrades.extra_levels.fullHelp": "La partie dure normalement 7 niveaux, après quoi le jeu est terminé et le score que vous avez atteint est votre score de partie.\n\nChoisir cette amélioration vous permet de prolonger la partie d'un niveau. Les derniers niveaux sont souvent ceux où vous faites le plus de points, la différence peut donc être spectaculaire.", "upgrades.extra_levels.fullHelp": "La partie dure normalement 7 niveaux, après quoi le jeu est terminé et le score que vous avez atteint est votre score de partie.\n\nChoisir cette amélioration vous permet de prolonger la partie d'un niveau. Les derniers niveaux sont souvent ceux où vous faites le plus de points, la différence peut donc être spectaculaire.",
"upgrades.extra_levels.help": "Jouer {{count}} niveaux au lieu de 7", "upgrades.extra_levels.help": "Jouer {{count}} niveaux au lieu de 7",
"upgrades.extra_levels.name": "+1 niveau", "upgrades.extra_levels.name": "+1 niveau",
@ -186,7 +189,7 @@
"upgrades.telekinesis.help_plural": "Effet plus fort sur la balle", "upgrades.telekinesis.help_plural": "Effet plus fort sur la balle",
"upgrades.telekinesis.name": "Télékinésie", "upgrades.telekinesis.name": "Télékinésie",
"upgrades.top_is_lava.fullHelp": "Chaque fois que vous cassez une brique, votre combo augmente d'une unité. Cependant, votre combo sera réinitialisé dès que votre balle atteindra le haut de l'écran.\n\nLorsque votre combo est supérieur au minimum, une barre rouge apparaît en haut de l'écran pour vous rappeler que vous devez éviter de la frapper.\n\nCet effet s'ajoute aux autres avantages du combo.", "upgrades.top_is_lava.fullHelp": "Chaque fois que vous cassez une brique, votre combo augmente d'une unité. Cependant, votre combo sera réinitialisé dès que votre balle atteindra le haut de l'écran.\n\nLorsque votre combo est supérieur au minimum, une barre rouge apparaît en haut de l'écran pour vous rappeler que vous devez éviter de la frapper.\n\nCet effet s'ajoute aux autres avantages du combo.",
"upgrades.top_is_lava.help": "Plus de pièces si vous ne touchez pas le sommet.", "upgrades.top_is_lava.help": "Plus de pièces si vous ne touchez pas le haut de la zone de jeu",
"upgrades.top_is_lava.name": "Icare ", "upgrades.top_is_lava.name": "Icare ",
"upgrades.viscosity.fullHelp": "Les pièces accélèrent normalement avec la gravité et les explosions pour atteindre des vitesses assez élevées. \n\nCette compétence les ralentit constamment, comme si elles se trouvaient dans une sorte de liquide visqueux.\n\nCela permet de les attraper plus facilement et se combine bien avec les améliorations qui influencent le mouvement de la pièce.", "upgrades.viscosity.fullHelp": "Les pièces accélèrent normalement avec la gravité et les explosions pour atteindre des vitesses assez élevées. \n\nCette compétence les ralentit constamment, comme si elles se trouvaient dans une sorte de liquide visqueux.\n\nCela permet de les attraper plus facilement et se combine bien avec les améliorations qui influencent le mouvement de la pièce.",
"upgrades.viscosity.help": "Chute plus lente des pièces", "upgrades.viscosity.help": "Chute plus lente des pièces",

View file

@ -1,32 +1,31 @@
import fr from './fr.json' import fr from "./fr.json";
import en from './en.json' import en from "./en.json";
import { getSettingValue } from "../settings"; import { getSettingValue } from "../settings";
type translationKeys = keyof typeof en type translationKeys = keyof typeof en;
type translation= { [key in translationKeys] : string } type translation = { [key in translationKeys]: string };
const languages:Record<string, translation>= {fr,en} const languages: Record<string, translation> = { fr, en };
export function getCurrentLang() { export function getCurrentLang() {
return getSettingValue('lang',getFirstBrowserLanguage()) return getSettingValue("lang", getFirstBrowserLanguage());
} }
export function t(key: translationKeys, params: {[key:string]:any} = {}):string { export function t(
const lang = getCurrentLang() key: translationKeys,
let template=languages[lang]?.[key] || languages.en[key] params: { [key: string]: any } = {},
): string {
const lang = getCurrentLang();
let template = languages[lang]?.[key] || languages.en[key];
for (let key in params) { for (let key in params) {
template=template.split('{{'+key+'}}').join(`${params[key]}`) template = template.split("{{" + key + "}}").join(`${params[key]}`);
} }
return template return template;
} }
function getFirstBrowserLanguage() { function getFirstBrowserLanguage() {
const preferred_languages = [ const preferred_languages = [...navigator.languages, navigator.language, "en"]
...navigator.languages, .filter((i) => i)
navigator.language, .map((i) => i.slice(0, 2).toLowerCase());
'en' const supported = Object.keys(languages);
].filter(i => i)
.map(i => i.slice(0, 2).toLowerCase())
const supported = Object.keys(languages)
return preferred_languages.find(k=>supported.includes(k)) || 'en' return preferred_languages.find((k) => supported.includes(k)) || "en";
}
};

View file

@ -12,15 +12,12 @@
name="description" name="description"
content="A breakout game with roguelite mechanics. Break bricks, catch coins, pick upgrades, repeat. Play for free on mobile and desktop." content="A breakout game with roguelite mechanics. Break bricks, catch coins, pick upgrades, repeat. Play for free on mobile and desktop."
/> />
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="./PWA/manifest.json" />
<style> <style>
@import "game.less"; @import "game.less";
</style> </style>
<link <link rel="icon" href="./PWA/icon.svg" />
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> </head>
<body> <body>
<button id="menu"><span id="menuLabel">menu</span></button> <button id="menu"><span id="menuLabel">menu</span></button>

46
src/levelIcon.ts Normal file
View file

@ -0,0 +1,46 @@
let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx =
process.env.NODE_ENV !== "test" &&
(levelIconHTMLCanvas.getContext("2d", {
antialias: false,
alpha: true,
}) as CanvasRenderingContext2D);
export function levelIconHTML(
bricks: string[],
levelSize: number,
color: string,
) {
const size = 40;
const c = levelIconHTMLCanvas;
const ctx = levelIconHTMLCanvasCtx;
if (!ctx) return "";
c.width = size;
c.height = size;
if (color) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
} else {
ctx.clearRect(0, 0, size, size);
}
const pxSize = size / levelSize;
for (let x = 0; x < levelSize; x++) {
for (let y = 0; y < levelSize; y++) {
const c = bricks[y * levelSize + x];
if (c) {
ctx.fillStyle = c;
ctx.fillRect(
Math.floor(pxSize * x),
Math.floor(pxSize * y),
Math.ceil(pxSize),
Math.ceil(pxSize),
);
}
}
}
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
}

View file

@ -1,8 +1,8 @@
import { Palette, RawLevel } from "./types"; import { Palette, RawLevel } from "../types";
import _backgrounds from "./backgrounds.json"; import _backgrounds from "../data/backgrounds.json";
import _palette from "./palette.json"; import _palette from "../data/palette.json";
import _allLevels from "./levels.json"; import _allLevels from "../data/levels.json";
import { getLevelBackground, hashCode } from "./getLevelBackground"; import { getLevelBackground, hashCode } from "../getLevelBackground";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util"; import { moveLevel, resizeLevel, setBrick } from "./levels_editor_util";
@ -34,7 +34,7 @@ function App() {
useEffect(() => { useEffect(() => {
const timoutId = setTimeout(() => { const timoutId = setTimeout(() => {
return fetch("http://localhost:4400/src/levels.json", { return fetch("http://localhost:4400/src/data/levels.json", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "text/plain", "Content-Type": "text/plain",

View file

@ -1,4 +1,4 @@
import { RawLevel } from "./types"; import { RawLevel } from "../types";
export function resizeLevel(level: RawLevel, sizeDelta: number) { export function resizeLevel(level: RawLevel, sizeDelta: number) {
const { size, bricks } = level; const { size, bricks } = level;

View file

@ -1,6 +1,6 @@
import _palette from "./palette.json"; import _palette from "./data/palette.json";
import _rawLevelsList from "./levels.json"; import _rawLevelsList from "./data/levels.json";
import _appVersion from "./version.json"; import _appVersion from "./data/version.json";
describe("json data checks", () => { describe("json data checks", () => {
it("_rawLevelsList has icon levels", () => { it("_rawLevelsList has icon levels", () => {

View file

@ -1,53 +1,17 @@
import { Level, Palette, RawLevel, Upgrade } from "./types"; import { Level, Palette, RawLevel, Upgrade } from "./types";
import _palette from "./palette.json"; import _palette from "./data/palette.json";
import _rawLevelsList from "./levels.json"; import _rawLevelsList from "./data/levels.json";
import _appVersion from "./version.json"; import _appVersion from "./data/version.json";
import { rawUpgrades } from "./rawUpgrades"; import { rawUpgrades } from "./rawUpgrades";
import { getLevelBackground } from "./getLevelBackground"; import { getLevelBackground } from "./getLevelBackground";
import { levelIconHTML } from "./levelIcon";
const palette = _palette as Palette; const palette = _palette as Palette;
const rawLevelsList = _rawLevelsList as RawLevel[]; const rawLevelsList = _rawLevelsList as RawLevel[];
export const appVersion = _appVersion as string; export const appVersion = _appVersion as string;
let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
antialias: false,
alpha: true,
}) as CanvasRenderingContext2D;
function levelIconHTML(bricks: string[], levelSize: number, color: string) {
const size = 40;
const c = levelIconHTMLCanvas;
const ctx = levelIconHTMLCanvasCtx;
c.width = size;
c.height = size;
if (color) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
} else {
ctx.clearRect(0, 0, size, size);
}
const pxSize = size / levelSize;
for (let x = 0; x < levelSize; x++) {
for (let y = 0; y < levelSize; y++) {
const c = bricks[y * levelSize + x];
if (c) {
ctx.fillStyle = c;
ctx.fillRect(
Math.floor(pxSize * x),
Math.floor(pxSize * y),
Math.ceil(pxSize),
Math.ceil(pxSize),
);
}
}
}
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
}
export const icons = {} as { [k: string]: string }; export const icons = {} as { [k: string]: string };
export const allLevels = rawLevelsList export const allLevels = rawLevelsList

View file

@ -1,7 +1,11 @@
import { GameState, RunParams } from "./types"; import { GameState, RunParams } from "./types";
import { getTotalScore } from "./settings"; import { getTotalScore } from "./settings";
import { allLevels, upgrades } from "./loadGameData"; import { allLevels, upgrades } from "./loadGameData";
import {getPossibleUpgrades, makeEmptyPerksMap, sumOfKeys} from "./game_utils"; import {
getPossibleUpgrades,
makeEmptyPerksMap,
sumOfKeys,
} from "./game_utils";
import { dontOfferTooSoon, resetBalls } from "./gameStateMutators"; import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
@ -77,6 +81,7 @@ export function newGameState(params: RunParams): GameState {
misses: 0, misses: 0,
balls_lost: 0, balls_lost: 0,
puck_bounces: 0, puck_bounces: 0,
wall_bounces: 0,
upgrades_picked: 1, upgrades_picked: 1,
max_combo: 1, max_combo: 1,
max_level: 0, max_level: 0,

View file

@ -6,39 +6,39 @@ import {getSettingValue, setSettingValue} from "./settings";
export const options = { export const options = {
sound: { sound: {
default: true, default: true,
name: t('main_menu.sounds'), name: t("main_menu.sounds"),
help: t('main_menu.sounds_help'), help: t("main_menu.sounds_help"),
disabled: () => false, disabled: () => false,
}, },
"mobile-mode": { "mobile-mode": {
default: window.innerHeight > window.innerWidth, default: window.innerHeight > window.innerWidth,
name: t('main_menu.mobile'), name: t("main_menu.mobile"),
help: t('main_menu.mobile_help'), help: t("main_menu.mobile_help"),
disabled: () => false, disabled: () => false,
}, },
basic: { basic: {
default: false, default: false,
name: t('main_menu.basic'), name: t("main_menu.basic"),
help: t('main_menu.basic_help'), help: t("main_menu.basic_help"),
disabled: () => false, disabled: () => false,
}, },
pointerLock: { pointerLock: {
default: false, default: false,
name: t('main_menu.pointer_lock'), name: t("main_menu.pointer_lock"),
help: t('main_menu.pointer_lock_help'), help: t("main_menu.pointer_lock_help"),
disabled: () => !document.body.requestPointerLock, disabled: () => !document.body.requestPointerLock,
}, },
easy: { easy: {
default: false, default: false,
name: t('main_menu.kid'), name: t("main_menu.kid"),
help: t('main_menu.kid_help'), help: t("main_menu.kid_help"),
disabled: () => false, disabled: () => false,
}, },
// 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 // 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: { record: {
default: false, default: false,
name: t('main_menu.record'), name: t("main_menu.record"),
help: t('main_menu.record_help'), help: t("main_menu.record_help"),
disabled() { disabled() {
return window.location.search.includes("isInWebView=true"); return window.location.search.includes("isInWebView=true");
}, },
@ -46,9 +46,12 @@ export const options = {
} as const satisfies { [k: string]: OptionDef }; } as const satisfies { [k: string]: OptionDef };
export function isOptionOn(key: OptionId) { export function isOptionOn(key: OptionId) {
return getSettingValue("breakout-settings-enable-" + key, options[key]?.default) return getSettingValue(
"breakout-settings-enable-" + key,
options[key]?.default,
);
} }
export function toggleOption(key: OptionId) { export function toggleOption(key: OptionId) {
setSettingValue("breakout-settings-enable-" +key, !isOptionOn(key)) setSettingValue("breakout-settings-enable-" + key, !isOptionOn(key));
} }

View file

@ -7,9 +7,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "extra_life", id: "extra_life",
max: 7, max: 7,
name: t('upgrades.extra_life.name'), name: t("upgrades.extra_life.name"),
help: (lvl: number) => lvl === 1 ? t('upgrades.extra_life.help'): t('upgrades.extra_life.help_plural',{lvl}), help: (lvl: number) =>
fullHelp: t('upgrades.extra_life.fullHelp'), lvl === 1
? t("upgrades.extra_life.help")
: t("upgrades.extra_life.help_plural", { lvl }),
fullHelp: t("upgrades.extra_life.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -17,9 +20,9 @@ export const rawUpgrades = [
id: "streak_shots", id: "streak_shots",
giftable: true, giftable: true,
max: 1, max: 1,
name: t('upgrades.streak_shots.name'), name: t("upgrades.streak_shots.name"),
help: (lvl: number) => t('upgrades.streak_shots.help',{lvl}) , help: (lvl: number) => t("upgrades.streak_shots.help", { lvl }),
fullHelp: t('upgrades.streak_shots.fullHelp'), fullHelp: t("upgrades.streak_shots.fullHelp"),
}, },
{ {
@ -28,9 +31,10 @@ export const rawUpgrades = [
id: "base_combo", id: "base_combo",
giftable: true, giftable: true,
max: 7, max: 7,
name: t('upgrades.base_combo.name'), name: t("upgrades.base_combo.name"),
help: (lvl: number) => t('upgrades.base_combo.help',{coins:1 + lvl * 3}), help: (lvl: number) =>
fullHelp: t('upgrades.base_combo.fullHelp'), t("upgrades.base_combo.help", { coins: 1 + lvl * 3 }),
fullHelp: t("upgrades.base_combo.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -38,9 +42,9 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "slow_down", id: "slow_down",
max: 2, max: 2,
name: t('upgrades.slow_down.name'), name: t("upgrades.slow_down.name"),
help: () => t('upgrades.slow_down.help' ), help: () => t("upgrades.slow_down.help"),
fullHelp: t('upgrades.slow_down.fullHelp'), fullHelp: t("upgrades.slow_down.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -48,9 +52,9 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "bigger_puck", id: "bigger_puck",
max: 2, max: 2,
name: t('upgrades.bigger_puck.name'), name: t("upgrades.bigger_puck.name"),
help: () => t('upgrades.bigger_puck.help' ), help: () => t("upgrades.bigger_puck.help"),
fullHelp: t('upgrades.bigger_puck.fullHelp'), fullHelp: t("upgrades.bigger_puck.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -59,9 +63,9 @@ export const rawUpgrades = [
id: "viscosity", id: "viscosity",
max: 3, max: 3,
name: t('upgrades.viscosity.name'), name: t("upgrades.viscosity.name"),
help: () => t('upgrades.viscosity.help' ), help: () => t("upgrades.viscosity.help"),
fullHelp: t('upgrades.viscosity.fullHelp'), fullHelp: t("upgrades.viscosity.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -70,10 +74,9 @@ export const rawUpgrades = [
giftable: true, giftable: true,
max: 1, max: 1,
name: t('upgrades.left_is_lava.name'), name: t("upgrades.left_is_lava.name"),
help: () => t('upgrades.left_is_lava.help' ), help: () => t("upgrades.left_is_lava.help"),
fullHelp: t('upgrades.left_is_lava.fullHelp'), fullHelp: t("upgrades.left_is_lava.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -81,10 +84,9 @@ export const rawUpgrades = [
id: "right_is_lava", id: "right_is_lava",
giftable: true, giftable: true,
max: 1, max: 1,
name: t('upgrades.right_is_lava.name'), name: t("upgrades.right_is_lava.name"),
help: () => t('upgrades.right_is_lava.help' ), help: () => t("upgrades.right_is_lava.help"),
fullHelp: t('upgrades.right_is_lava.fullHelp'), fullHelp: t("upgrades.right_is_lava.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -92,10 +94,9 @@ export const rawUpgrades = [
id: "top_is_lava", id: "top_is_lava",
giftable: true, giftable: true,
max: 1, max: 1,
name: t('upgrades.top_is_lava.name'), name: t("upgrades.top_is_lava.name"),
help: () => t('upgrades.top_is_lava.help' ), help: () => t("upgrades.top_is_lava.help"),
fullHelp: t('upgrades.top_is_lava.fullHelp'), fullHelp: t("upgrades.top_is_lava.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -103,10 +104,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "skip_last", id: "skip_last",
max: 7, max: 7,
name: t('upgrades.skip_last.name'), name: t("upgrades.skip_last.name"),
help: (lvl: number) => lvl==1 ? t('upgrades.skip_last.help' ) : t('upgrades.skip_last.help_plural', {lvl} ), help: (lvl: number) =>
fullHelp: t('upgrades.skip_last.fullHelp'), lvl == 1
? t("upgrades.skip_last.help")
: t("upgrades.skip_last.help_plural", { lvl }),
fullHelp: t("upgrades.skip_last.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -114,9 +117,12 @@ export const rawUpgrades = [
id: "telekinesis", id: "telekinesis",
giftable: true, giftable: true,
max: 2, max: 2,
name: t('upgrades.telekinesis.name'), name: t("upgrades.telekinesis.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.telekinesis.help'): t('upgrades.telekinesis.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.telekinesis.fullHelp'), lvl == 1
? t("upgrades.telekinesis.help")
: t("upgrades.telekinesis.help_plural"),
fullHelp: t("upgrades.telekinesis.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -124,9 +130,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "coin_magnet", id: "coin_magnet",
max: 3, max: 3,
name: t('upgrades.coin_magnet.name'), name: t("upgrades.coin_magnet.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.coin_magnet.help'): t('upgrades.coin_magnet.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.coin_magnet.fullHelp'), lvl == 1
? t("upgrades.coin_magnet.help")
: t("upgrades.coin_magnet.help_plural"),
fullHelp: t("upgrades.coin_magnet.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -134,11 +143,9 @@ export const rawUpgrades = [
id: "multiball", id: "multiball",
giftable: true, giftable: true,
max: 6, max: 6,
name: t('upgrades.multiball.name'), name: t("upgrades.multiball.name"),
help: (lvl: number) => t('upgrades.multiball.help',{count:lvl+1}) , help: (lvl: number) => t("upgrades.multiball.help", { count: lvl + 1 }),
fullHelp: t('upgrades.multiball.fullHelp'), fullHelp: t("upgrades.multiball.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -146,11 +153,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "smaller_puck", id: "smaller_puck",
max: 2, max: 2,
name: t('upgrades.smaller_puck.name'), name: t("upgrades.smaller_puck.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.smaller_puck.help'): t('upgrades.smaller_puck.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.smaller_puck.fullHelp'), lvl == 1
? t("upgrades.smaller_puck.help")
: t("upgrades.smaller_puck.help_plural"),
fullHelp: t("upgrades.smaller_puck.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -158,9 +166,9 @@ export const rawUpgrades = [
id: "pierce", id: "pierce",
giftable: true, giftable: true,
max: 3, max: 3,
name: t('upgrades.pierce.name'), name: t("upgrades.pierce.name"),
help: (lvl: number) => t('upgrades.pierce.help',{count:3 * lvl}) , help: (lvl: number) => t("upgrades.pierce.help", { count: 3 * lvl }),
fullHelp: t('upgrades.pierce.fullHelp'), fullHelp: t("upgrades.pierce.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -168,10 +176,9 @@ export const rawUpgrades = [
id: "picky_eater", id: "picky_eater",
giftable: true, giftable: true,
max: 1, max: 1,
name: t('upgrades.picky_eater.name'), name: t("upgrades.picky_eater.name"),
help: (lvl: number) => t('upgrades.picky_eater.help') , help: (lvl: number) => t("upgrades.picky_eater.help"),
fullHelp: t('upgrades.picky_eater.fullHelp'), fullHelp: t("upgrades.picky_eater.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -179,11 +186,9 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "metamorphosis", id: "metamorphosis",
max: 1, max: 1,
name: t('upgrades.metamorphosis.name'), name: t("upgrades.metamorphosis.name"),
help: (lvl: number) => t('upgrades.metamorphosis.help'), help: (lvl: number) => t("upgrades.metamorphosis.help"),
fullHelp: t('upgrades.metamorphosis.fullHelp'), fullHelp: t("upgrades.metamorphosis.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -191,9 +196,9 @@ export const rawUpgrades = [
id: "compound_interest", id: "compound_interest",
giftable: true, giftable: true,
max: 1, max: 1,
name: t('upgrades.compound_interest.name'), name: t("upgrades.compound_interest.name"),
help: (lvl: number) => t('upgrades.compound_interest.help') , help: (lvl: number) => t("upgrades.compound_interest.help"),
fullHelp: t('upgrades.compound_interest.fullHelp'), fullHelp: t("upgrades.compound_interest.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -201,13 +206,13 @@ export const rawUpgrades = [
id: "hot_start", id: "hot_start",
giftable: true, giftable: true,
max: 3, max: 3,
name: t('upgrades.hot_start.name'), name: t("upgrades.hot_start.name"),
help: (lvl: number) => t('upgrades.hot_start.help',{ help: (lvl: number) =>
t("upgrades.hot_start.help", {
start: lvl * 15 + 1, start: lvl * 15 + 1,
lvl lvl,
}), }),
fullHelp: t('upgrades.hot_start.fullHelp'), fullHelp: t("upgrades.hot_start.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -215,9 +220,12 @@ export const rawUpgrades = [
id: "sapper", id: "sapper",
giftable: true, giftable: true,
max: 7, max: 7,
name: t('upgrades.sapper.name'), name: t("upgrades.sapper.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.sapper.help'): t('upgrades.sapper.help_plural',{lvl}), help: (lvl: number) =>
fullHelp: t('upgrades.sapper.fullHelp'), lvl == 1
? t("upgrades.sapper.help")
: t("upgrades.sapper.help_plural", { lvl }),
fullHelp: t("upgrades.sapper.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -225,9 +233,9 @@ export const rawUpgrades = [
id: "bigger_explosions", id: "bigger_explosions",
giftable: false, giftable: false,
max: 1, max: 1,
name: t('upgrades.bigger_explosions.name'), name: t("upgrades.bigger_explosions.name"),
help: (lvl: number) => t('upgrades.bigger_explosions.help'), help: (lvl: number) => t("upgrades.bigger_explosions.help"),
fullHelp: t('upgrades.bigger_explosions.fullHelp'), fullHelp: t("upgrades.bigger_explosions.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -235,9 +243,9 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "extra_levels", id: "extra_levels",
max: 3, max: 3,
name: t('upgrades.extra_levels.name'), name: t("upgrades.extra_levels.name"),
help: (lvl: number) => t('upgrades.extra_levels.help',{count:lvl + 7}) , help: (lvl: number) => t("upgrades.extra_levels.help", { count: lvl + 7 }),
fullHelp: t('upgrades.extra_levels.fullHelp'), fullHelp: t("upgrades.extra_levels.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -245,10 +253,9 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "pierce_color", id: "pierce_color",
max: 1, max: 1,
name: t('upgrades.pierce_color.name'), name: t("upgrades.pierce_color.name"),
help: (lvl: number) => t('upgrades.pierce_color.help') , help: (lvl: number) => t("upgrades.pierce_color.help"),
fullHelp: t('upgrades.pierce_color.fullHelp'), fullHelp: t("upgrades.pierce_color.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -256,11 +263,9 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "soft_reset", id: "soft_reset",
max: 9, max: 9,
name: t('upgrades.soft_reset.name'), name: t("upgrades.soft_reset.name"),
help: (lvl: number) => t('upgrades.soft_reset.help',{percent:10*lvl}), help: (lvl: number) => t("upgrades.soft_reset.help", { percent: 10 * lvl }),
fullHelp: t('upgrades.soft_reset.fullHelp'), fullHelp: t("upgrades.soft_reset.fullHelp"),
}, },
{ {
requires: "multiball", requires: "multiball",
@ -268,11 +273,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "ball_repulse_ball", id: "ball_repulse_ball",
max: 3, max: 3,
name: t('upgrades.ball_repulse_ball.name'), name: t("upgrades.ball_repulse_ball.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.ball_repulse_ball.help'): t('upgrades.ball_repulse_ball.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.ball_repulse_ball.fullHelp'), lvl == 1
? t("upgrades.ball_repulse_ball.help")
: t("upgrades.ball_repulse_ball.help_plural"),
fullHelp: t("upgrades.ball_repulse_ball.fullHelp"),
}, },
{ {
requires: "multiball", requires: "multiball",
@ -280,9 +286,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "ball_attract_ball", id: "ball_attract_ball",
max: 3, max: 3,
name: t('upgrades.ball_attract_ball.name'), name: t("upgrades.ball_attract_ball.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.ball_attract_ball.help'): t('upgrades.ball_attract_ball.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.ball_attract_ball.fullHelp'), lvl == 1
? t("upgrades.ball_attract_ball.help")
: t("upgrades.ball_attract_ball.help_plural"),
fullHelp: t("upgrades.ball_attract_ball.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -290,10 +299,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "puck_repulse_ball", id: "puck_repulse_ball",
max: 2, max: 2,
name: t('upgrades.puck_repulse_ball.name'), name: t("upgrades.puck_repulse_ball.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.puck_repulse_ball.help'): t('upgrades.puck_repulse_ball.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.puck_repulse_ball.fullHelp'), lvl == 1
? t("upgrades.puck_repulse_ball.help")
: t("upgrades.puck_repulse_ball.help_plural"),
fullHelp: t("upgrades.puck_repulse_ball.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -301,11 +312,10 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "wind", id: "wind",
max: 3, max: 3,
name: t('upgrades.wind.name'), name: t("upgrades.wind.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.wind.help'): t('upgrades.wind.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.wind.fullHelp'), lvl == 1 ? t("upgrades.wind.help") : t("upgrades.wind.help_plural"),
fullHelp: t("upgrades.wind.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -313,10 +323,12 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "sturdy_bricks", id: "sturdy_bricks",
max: 4, max: 4,
name: t('upgrades.telekinesis.name'), name: t("upgrades.telekinesis.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.telekinesis.help'): t('upgrades.telekinesis.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.telekinesis.fullHelp'), lvl == 1
? t("upgrades.telekinesis.help")
: t("upgrades.telekinesis.help_plural"),
fullHelp: t("upgrades.telekinesis.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -324,11 +336,10 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "respawn", id: "respawn",
max: 4, max: 4,
name: t('upgrades.respawn.name'), name: t("upgrades.respawn.name"),
help: (lvl: number) => lvl == 1 ? t('upgrades.respawn.help'): t('upgrades.respawn.help_plural'), help: (lvl: number) =>
fullHelp: t('upgrades.respawn.fullHelp'), lvl == 1 ? t("upgrades.respawn.help") : t("upgrades.respawn.help_plural"),
fullHelp: t("upgrades.respawn.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -336,10 +347,9 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "one_more_choice", id: "one_more_choice",
max: 3, max: 3,
name: t('upgrades.one_more_choice.name'), name: t("upgrades.one_more_choice.name"),
help: (lvl: number) => t('upgrades.one_more_choice.help'), help: (lvl: number) => t("upgrades.one_more_choice.help"),
fullHelp: t('upgrades.one_more_choice.fullHelp'), fullHelp: t("upgrades.one_more_choice.fullHelp"),
}, },
{ {
requires: "", requires: "",
@ -347,9 +357,18 @@ export const rawUpgrades = [
giftable: false, giftable: false,
id: "instant_upgrade", id: "instant_upgrade",
max: 2, max: 2,
name: t('upgrades.instant_upgrade.name'), name: t("upgrades.instant_upgrade.name"),
help: (lvl: number) => t('upgrades.instant_upgrade.help') , help: (lvl: number) => t("upgrades.instant_upgrade.help"),
fullHelp: t('upgrades.instant_upgrade.fullHelp'), fullHelp: t("upgrades.instant_upgrade.fullHelp"),
},
{
requires: "",
threshold: 60000,
giftable: false,
id: "concave_puck",
max: 1,
name: t("upgrades.concave_puck.name"),
help: (lvl: number) => t("upgrades.concave_puck.help"),
fullHelp: t("upgrades.concave_puck.fullHelp"),
}, },
] as const; ] as const;

View file

@ -123,8 +123,8 @@ export function startRecordingGame(gameState:GameState) {
a.download = captureFileName("webm"); a.download = captureFileName("webm");
a.target = "_blank"; a.target = "_blank";
a.href = video.src; a.href = video.src;
a.textContent = t('main_menu.record_download', { a.textContent = t("main_menu.record_download", {
size: (blob.size / 1000000).toFixed(2) size: (blob.size / 1000000).toFixed(2),
}); });
targetDiv.appendChild(video); targetDiv.appendChild(video);
targetDiv.appendChild(a); targetDiv.appendChild(a);

View file

@ -1,5 +1,11 @@
import { baseCombo } from "./gameStateMutators"; import { baseCombo } from "./gameStateMutators";
import {brickCenterX, brickCenterY, currentLevelInfo, isTelekinesisActive, max_levels} from "./game_utils"; import {
brickCenterX,
brickCenterY,
currentLevelInfo,
isTelekinesisActive,
max_levels,
} from "./game_utils";
import { colorString, GameState } from "./types"; import { colorString, GameState } from "./types";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { gameState } from "./game"; import { gameState } from "./game";
@ -14,18 +20,17 @@ export const background = document.createElement("img");
export const backgroundCanvas = document.createElement("canvas"); export const backgroundCanvas = document.createElement("canvas");
export function render(gameState: GameState) { export function render(gameState: GameState) {
const level = currentLevelInfo(gameState); const level = currentLevelInfo(gameState);
const { width, height } = gameCanvas; const { width, height } = gameCanvas;
if (!width || !height) return; if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) { if (gameState.currentLevel || gameState.levelTime) {
menuLabel.innerText = t('play.current_lvl', { menuLabel.innerText = t("play.current_lvl", {
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState) max: max_levels(gameState),
}); });
} else { } else {
menuLabel.innerText = t('play.menu_label') menuLabel.innerText = t("play.menu_label");
} }
scoreDisplay.innerText = `$${gameState.score}`; scoreDisplay.innerText = `$${gameState.score}`;
@ -130,7 +135,7 @@ export function render(gameState: GameState) {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5; const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5;
const shaked = lastExplosionDelay < 200 && !isOptionOn('basic'); const shaked = lastExplosionDelay < 200 && !isOptionOn("basic");
if (shaked) { if (shaked) {
const amplitude = const amplitude =
((gameState.perks.bigger_explosions + 1) * 50) / lastExplosionDelay; ((gameState.perks.bigger_explosions + 1) * 50) / lastExplosionDelay;
@ -139,11 +144,12 @@ export function render(gameState: GameState) {
Math.sin(Date.now() + 36) * amplitude, Math.sin(Date.now() + 36) * amplitude,
); );
} }
if (gameState.perks.bigger_explosions && !isOptionOn('basic')) { if (gameState.perks.bigger_explosions && !isOptionOn("basic")) {
if (shaked) { if (shaked) {
gameCanvas.style.filter = 'brightness(' + (1 + 100 / (1 + lastExplosionDelay)) + ')'; gameCanvas.style.filter =
"brightness(" + (1 + 100 / (1 + lastExplosionDelay)) + ")";
} else { } else {
gameCanvas.style.filter = '' gameCanvas.style.filter = "";
} }
} }
// Coins // Coins
@ -247,9 +253,23 @@ export function render(gameState: GameState) {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
if (gameState.perks.streak_shots && gameState.combo > baseCombo(gameState)) { if (gameState.perks.streak_shots && gameState.combo > baseCombo(gameState)) {
drawPuck(ctx, "red", gameState.puckWidth, gameState.puckHeight, -2); drawPuck(
ctx,
"red",
gameState.puckWidth,
gameState.puckHeight,
-2,
!!gameState.perks.concave_puck,
);
} }
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight); drawPuck(
ctx,
gameState.puckColor,
gameState.puckWidth,
gameState.puckHeight,
0,
!!gameState.perks.concave_puck,
);
if (gameState.combo > 1) { if (gameState.combo > 1) {
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
@ -328,7 +348,7 @@ export function render(gameState: GameState) {
if (!gameState.running) { if (!gameState.running) {
drawText( drawText(
ctx, ctx,
t('play.mobile_press_to_play'), t("play.mobile_press_to_play"),
gameState.puckColor, gameState.puckColor,
gameState.puckHeight, gameState.puckHeight,
gameState.canvasWidth / 2, gameState.canvasWidth / 2,
@ -348,7 +368,6 @@ export function render(gameState: GameState) {
if (shaked) { if (shaked) {
ctx.resetTransform(); ctx.resetTransform();
} }
} }
let cachedBricksRender = document.createElement("canvas"); let cachedBricksRender = document.createElement("canvas");
@ -358,7 +377,9 @@ export function renderAllBricks() {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
const redBorderOnBricksWithWrongColor = const redBorderOnBricksWithWrongColor =
gameState.combo > baseCombo(gameState) && gameState.perks.picky_eater && !isOptionOn('basic'); gameState.combo > baseCombo(gameState) &&
gameState.perks.picky_eater &&
!isOptionOn("basic");
const newKey = const newKey =
gameState.gameZoneWidth + gameState.gameZoneWidth +
@ -415,8 +436,10 @@ export function drawPuck(
puckWidth: number, puckWidth: number,
puckHeight: number, puckHeight: number,
yOffset = 0, yOffset = 0,
flipped: boolean,
) { ) {
const key = "puck" + color + "_" + puckWidth + "_" + puckHeight; const key =
"puck" + color + "_" + puckWidth + "_" + puckHeight + "_" + flipped;
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
const can = document.createElement("canvas"); const can = document.createElement("canvas");
@ -427,6 +450,19 @@ export function drawPuck(
canctx.beginPath(); canctx.beginPath();
canctx.moveTo(0, puckHeight * 2); canctx.moveTo(0, puckHeight * 2);
if (flipped) {
canctx.lineTo(0, puckHeight * 0.75);
canctx.bezierCurveTo(
puckWidth / 2,
puckHeight,
puckWidth / 2,
puckHeight * 1,
puckWidth,
puckHeight * 0.75,
);
canctx.lineTo(puckWidth, puckHeight * 2);
} else {
canctx.lineTo(0, puckHeight * 1.25); canctx.lineTo(0, puckHeight * 1.25);
canctx.bezierCurveTo( canctx.bezierCurveTo(
0, 0,
@ -437,6 +473,8 @@ export function drawPuck(
puckHeight * 1.25, puckHeight * 1.25,
); );
canctx.lineTo(puckWidth, puckHeight * 2); canctx.lineTo(puckWidth, puckHeight * 2);
}
canctx.fill(); canctx.fill();
cachedGraphics[key] = can; cachedGraphics[key] = can;
} }
@ -713,5 +751,7 @@ export function drawText(
); );
} }
export const scoreDisplay = document.getElementById("score") as HTMLButtonElement; export const scoreDisplay = document.getElementById(
"score",
) as HTMLButtonElement;
const menuLabel = document.getElementById("menuLabel") as HTMLButtonElement; const menuLabel = document.getElementById("menuLabel") as HTMLButtonElement;

View file

@ -13,11 +13,11 @@ export function getSettingValue<T>(key: string, defaultValue: T) {
console.warn(e); console.warn(e);
} }
} }
return cachedSettings[key] as T ?? defaultValue; return (cachedSettings[key] as T) ?? defaultValue;
} }
export function setSettingValue<T>(key: string, value: T) { export function setSettingValue<T>(key: string, value: T) {
cachedSettings[key] = value cachedSettings[key] = value;
try { try {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
} catch (e) { } catch (e) {
@ -26,12 +26,10 @@ export function setSettingValue<T>(key: string, value: T) {
} }
export function getTotalScore() { export function getTotalScore() {
return getSettingValue('breakout_71_total_score', 0) return getSettingValue("breakout_71_total_score", 0);
} }
export function addToTotalScore(gameState: GameState, points: number) { export function addToTotalScore(gameState: GameState, points: number) {
if (gameState.isCreativeModeRun) return; if (gameState.isCreativeModeRun) return;
setSettingValue('breakout_71_total_score', getTotalScore() + points) setSettingValue("breakout_71_total_score", getTotalScore() + points);
} }

View file

@ -1,6 +1,5 @@
import { gameState } from "./game"; import { gameState } from "./game";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
export const sounds = { export const sounds = {

3
src/types.d.ts vendored
View file

@ -98,7 +98,6 @@ export type Ball = {
piercedSinceBounce: number; piercedSinceBounce: number;
hitSinceBounce: number; hitSinceBounce: number;
hitItem: { index: number; color: string }[]; hitItem: { index: number; color: string }[];
bouncesList: { x: number; y: number }[];
sapperUses: number; sapperUses: number;
destroyed?: boolean; destroyed?: boolean;
}; };
@ -141,6 +140,7 @@ export type RunStats = {
misses: number; misses: number;
balls_lost: number; balls_lost: number;
puck_bounces: number; puck_bounces: number;
wall_bounces: number;
upgrades_picked: number; upgrades_picked: number;
max_combo: number; max_combo: number;
max_level: number; max_level: number;
@ -233,6 +233,7 @@ export type GameState = {
runStatistics: RunStats; runStatistics: RunStats;
lastOffered: Partial<{ [k in PerkId]: number }>; lastOffered: Partial<{ [k in PerkId]: number }>;
levelTime: number; levelTime: number;
levelWallBounces: number;
autoCleanUses: number; autoCleanUses: number;
}; };

View file

@ -1 +0,0 @@
"29033878"